From 80379b991c29976da74538a841618b2044ed82ca Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 29 Oct 2026 22:19:06 +0100 Subject: [PATCH] feat(editor): the work of the editor sessions not committed yet, as it stands: a curve traced on from the end of the diverging track of a switch goes on at its radius with no transition in, or from a straight right at the switch (alignment start_held, fit_going_on), the plan taking an end adjoining R 10 km or more for a straight; the switches and slips laid into curves, slid along them and laid alone on one straight (frame_path, switch slide); the route plan reading curves going on from the tracks adjoining, the kept designs taken or forgotten; the orthophoto and terrain model settings, downloads and layers, the tracks onto the terrain; the curvature of a path in the plan (bezier::plan_curvature); the Polish texts of all of it. Not built as committed --- application/editormode.cpp | 531 ++++++++++++++++-- application/editormode.h | 174 +++++- application/editormodelay.cpp | 626 ++++++++++++++------- application/editormodenmt.cpp | 431 +++++++++----- application/editormodeprofile.cpp | 50 +- application/editormoderecords.cpp | 16 +- application/editormoderoute.cpp | 95 +++- application/editormodeselftest.cpp | 6 + application/editormodeselftestjoin.cpp | 7 +- application/editormodeselftestswitch.cpp | 90 ++- application/editormodeselftestterrain.cpp | 186 ++++++- application/editormodeselftesttrack.cpp | 8 + application/editormodeswitchplace.cpp | 650 ++++++++++++++++++---- application/editormodeswitchslide.cpp | 214 ++++++- application/editormodeswitchtool.cpp | 9 +- application/editormodetrack.cpp | 251 +++++++-- application/editormodetrackshared.h | 26 +- application/editormodetrackux.cpp | 28 +- application/editormodeviewport.cpp | 10 +- application/editoruilayer.h | 7 + application/editoruipanels.cpp | 13 + application/editoruipanels.h | 5 + editor/editorAlignment.cpp | 632 ++++++++++++++++++--- editor/editorAlignment.hpp | 21 +- editor/editorAlignmentStore.cpp | 8 + editor/editorGeometry.hpp | 20 + editor/editorOrthophoto.cpp | 526 +++++++++-------- editor/editorOrthophoto.hpp | 38 +- editor/editorProfile.cpp | 6 +- editor/editorSettings.cpp | 122 +++- editor/editorSettings.hpp | 39 +- editor/editorSwitchCurve.cpp | 18 +- editor/editorSwitchCurve.hpp | 5 +- editor/editorSwitchStore.cpp | 6 + editor/editorSwitchStore.hpp | 2 + editor/editorTerrainStreamer.cpp | 10 +- editor/editorTrack.cpp | 182 +++++- editor/editorTrack.hpp | 5 + editor/editorTrackShape.cpp | 4 +- lang/editor_pl.po | 394 ++++++++++--- launcher/scenery_wizard.cpp | 14 +- rendering/opengl33renderer.cpp | 40 +- rendering/openglrenderer.cpp | 39 +- tests/unit/test_alignment.cpp | 195 +++++++ tests/unit/test_switch_curve.cpp | 46 ++ utilities/Globals.h | 1 - utilities/utilities.cpp | 18 + utilities/utilities.h | 3 + world/Segment.cpp | 5 + 49 files changed, 4718 insertions(+), 1114 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 6832aa49..121af750 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -80,6 +80,38 @@ namespace using vec3 = glm::vec3; using dvec2 = glm::dvec2; + // the steps of a frame of the editor timed: a frame whose update takes longer than kSlow tells in the log which of them took + // the time, at most twice a second + class frame_steps + { + public: + void step(char const *Name) + { + auto const now{std::chrono::steady_clock::now()}; + m_steps.emplace_back(Name, std::chrono::duration(now - m_last).count()); + m_last = now; + } + ~frame_steps() + { + constexpr double kSlow{50.0}; // ms + auto const total{std::chrono::duration(std::chrono::steady_clock::now() - m_start).count()}; + static auto told{std::chrono::steady_clock::time_point{}}; + if (total < kSlow || m_start - told < std::chrono::milliseconds{500}) + return; + told = m_start; + std::string text{"Editor: slow frame, update " + std::to_string(static_cast(total)) + " ms:"}; + for (auto const &[name, time] : m_steps) + if (time >= 3.0) + text += " " + std::string{name} + " " + std::to_string(static_cast(time)); + WriteLog(text, logtype::generic); + } + + private: + std::chrono::steady_clock::time_point const m_start{std::chrono::steady_clock::now()}; + std::chrono::steady_clock::time_point m_last{m_start}; + std::vector> m_steps; + }; + inline bool is_release(int state) { return state == GLFW_RELEASE; @@ -552,14 +584,21 @@ editor_mode::editor_mode() { }); m_roadtool.settings.normalize(); road_select(nullptr); - // the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included + ground_sources(m_orthophoto); + ui()->set_base_layers([this]() { render_base_layers(); }); + editor_track::observe(this); + } + +void editor_mode::ground_sources(editor_orthophoto &Layer) +{ + // the imagery is fitted onto the same ground as the area fill uses, terrain tile models included // the roads built in the editor aren't ground: the imagery goes under them - m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Heightmap, std::vector &Out) { + Layer.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Heightmap, std::vector &Out) { gather_ground_triangles(Min, Max, true, 50.0f, Out, true, Heightmap); }); - m_orthophoto.height_source([](double const X, double const Z, double &Height) { return EditorTerrain.height_at(X, Z, Height); }); + Layer.height_source([](double const X, double const Z, double &Height) { return EditorTerrain.height_at(X, Z, Height); }); // the imagery often comes before the terrain made from the model: it lies where the terrain will be - m_orthophoto.guess_source([this](double const X, double const Z, double &Height) { + Layer.guess_source([this](double const X, double const Z, double &Height) { if (nmt_ground(X, Z, Height)) return true; // asked for, the tile goes onto it once it comes @@ -567,8 +606,7 @@ editor_mode::editor_mode() { m_nmt.request({X - 1.0, Z - 1.0}, {X + 1.0, Z + 1.0}); return false; }); - editor_track::observe(this); - } +} editor_mode::~editor_mode() { @@ -657,6 +695,13 @@ bool editor_mode::init() { EditorSettings.load(); Global.EditorOrthoTracks = EditorSettings.top_tracks(); + // the terrain model as the user left it; the settings are read only here, after the mode was made + auto const &model{EditorSettings.terrain_model()}; + m_nmt_state.grow = EditorSettings.terrain_while_laying(); + m_nmt_state.band = std::clamp(model.band, 50.0, 3000.0); + m_nmt_state.shape = model.shape; + m_nmt_state.follow = model.follow; + m_terrain_add_count = std::clamp(model.around, 1, 8); EditorModelSets.load(); EditorTrackPresets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); @@ -1344,8 +1389,10 @@ void editor_mode::redo_one() bool editor_mode::update() { + frame_steps steps; Global.editor_tracks = (current_work_area() == work_area::tracks); selftest_step(); + steps.step("selftest"); // the drives of the switches go to the tips of their blades once, as the templates of the scenery are known if (false == m_drives_checked) { @@ -1374,7 +1421,9 @@ bool editor_mode::update() return true; } + steps.step("start"); Timer::UpdateTimers(true); + move_thrown_switches(); simulation::State.update_clocks(); simulation::Environment.update(); @@ -1389,6 +1438,9 @@ bool editor_mode::update() } auto const deltarealtime = Timer::GetDeltaRenderTime(); + // the whole frame before, drawing included: one slow where its update wasn't took the time drawing + if (deltarealtime > 0.15 && simulation::is_ready) + WriteLog("Editor: slow frame before, " + std::to_string(static_cast(deltarealtime * 1000.0)) + " ms in all", logtype::generic); // reconcile camera fly-mode with the real right-button state. ImGui is always fed the button // events (even when it captures the mouse), so io.MouseDown[1] is authoritative; if a release @@ -1415,6 +1467,7 @@ bool editor_mode::update() m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE); } terrain_history_step(); + steps.step("simulation"); // fixed step render time routines (50 Hz) fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable @@ -1445,8 +1498,10 @@ bool editor_mode::update() } + steps.step("ui"); // variable step routines update_camera(deltarealtime); + steps.step("camera"); // drop the selection if its layer was hidden or locked in the meantime if (m_node != nullptr && false == scene::Layers.editable(m_node)) @@ -1483,6 +1538,7 @@ bool editor_mode::update() audio::renderer.update(Global.iPause ? 0.0 : deltarealtime); GfxRenderer->Update(deltarealtime); + steps.step("renderer"); simulation::is_ready = true; @@ -1498,24 +1554,26 @@ bool editor_mode::update() draw_terrain_overlay(); // --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays --- - // far out the top view shows the base map in place of the imagery, and the ground isn't drawn under it - { - auto const &io{ImGui::GetIO()}; - auto const metres{Global.EditorOrtho && io.DisplaySize.y > 0.0f ? 2.0 * static_cast(Global.EditorOrthoExtent) / io.DisplaySize.y : 0.0}; - auto const aspect{io.DisplaySize.y > 0.0f ? static_cast(io.DisplaySize.x / io.DisplaySize.y) : 1.0}; - Global.EditorOrthoMap = m_orthophoto.enabled() && metres >= (m_orthophoto.overview() ? 1.6 : 2.0); - m_orthophoto.overview(Global.EditorOrthoMap ? metres : 0.0, static_cast(Global.EditorOrthoExtent) * std::sqrt(1.0 + aspect * aspect)); - } // chunks of the terrain come from the files and from the model: the imagery lying at guessed heights goes onto them // and so do the squares of the model it lies on meanwhile auto const resident{m_streamer.resident()}; auto const waiting{m_nmt.waiting()}; if (resident > m_ortho_resident || waiting < m_ortho_waiting) + { m_orthophoto.ground_arrived(); + for (auto &layer : m_wms) + layer->ground_arrived(); + } m_ortho_resident = resident; m_ortho_waiting = waiting; + steps.step("terrain"); m_orthophoto.update(Camera.Pos); + steps.step("orthophoto"); + update_wms_layers(); + steps.step("wms"); nmt_update(); + keep_nmt_settings(); + steps.step("nmt"); // what's drawn over the 3d view stays in it, the docks around it are see-through a little auto *background{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; background->PushClipRect(ImVec2(editor_ui::view_min().x, editor_ui::view_min().y), ImVec2(editor_ui::view_max().x, editor_ui::view_max().y), true); @@ -1523,6 +1581,7 @@ bool editor_mode::update() draw_nmt_pending(); area_update(); draw_download_area(); + steps.step("imagery"); // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); @@ -1530,13 +1589,14 @@ bool editor_mode::update() render_viewport_overlays(); render_track_inspector(); + steps.step("inspector"); if (ui()->mode() == nodebank_panel::TRACK) { draw_track_lines(); draw_track_labels(); draw_track_legend(); } - else if (Global.EditorOrtho && (Global.EditorOrthoTracks > 1 || Global.EditorOrthoMap)) + else if (Global.EditorOrtho && Global.EditorOrthoTracks > 1) draw_track_lines(); if (selected_track()) draw_track_overlay(); @@ -1544,11 +1604,13 @@ bool editor_mode::update() draw_route_overlay(); if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights) draw_straights_overlay(); + steps.step("track lines"); if (ui()->mode() == nodebank_panel::TRACK) { if (m_track_box.active && false == ImGui::GetIO().MouseDown[0]) finish_track_box(); update_track_hover(); + steps.step("hover"); point_drag_update(); switch_slide_update(); draw_switch_slide(); @@ -1568,21 +1630,29 @@ bool editor_mode::update() m_vehicle.draw_overlay(); } render_track_context(); + steps.step("context"); update_build_tools(); + steps.step("laying"); tape_update(); update_track_intent(); + steps.step("intent"); draw_track_hover(); draw_track_set(); draw_track_spread(); draw_build_overlay(); + steps.step("laying drawn"); draw_blade_tips(); draw_track_intent(); draw_tape(); + steps.step("track drawn"); m_speed.update(); + steps.step("speed"); m_joints.update(); + steps.step("joints"); m_srk.update(); kilometrage::follow(m_track_edits); m_km.update(); + steps.step("srk km"); draw_track_hints(); draw_empty_scenery_hint(); draw_profile_overlay(); @@ -1591,6 +1661,7 @@ bool editor_mode::update() m_joints.draw_overlay(); m_srk.draw_overlay(); m_km.draw_overlay(); + steps.step("overlays"); } else { @@ -1608,6 +1679,7 @@ bool editor_mode::update() // the roads and the terrain are fields of work, their tools are drawn in the tool options window in place of the edit mode ones ui()->set_workspace(m_terrain_open ? editor_ui::workspace::terrain : m_roadtool.window ? editor_ui::workspace::roads : editor_ui::workspace::none); + steps.step("roads"); // --- array: the one made last is kept in line with its settings --- update_array(); @@ -1623,11 +1695,11 @@ bool editor_mode::update() render_first_steps(); render_nmt_prompt(); render_nmt_banner(); - render_orthophoto_window(); render_switch_designer(); render_environment_window(); draw_selection_overlay(); render_context_menu(); + steps.step("windows"); // --- ImGui: Editor Settings & History windows --- if(m_settings_open) @@ -1796,6 +1868,10 @@ void editor_mode::render_terrain_ui() ImGui::SameLine(); if (ImGui::Button(STR_C("Clear the selection"))) m_terrain_selection.clear(); + if (ImGui::Button(STR_C("Remove the selected (Del)"))) + remove_selected_chunks(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Takes the selected chunks out of the terrain of the scenery, and out of its files on save; Ctrl+Z brings them back")); ImGui::Checkbox(STR_C("Paint every 0.5 m with 0.5 m spacing"), &m_terrain_convert_paint); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Chunks converted to 0.5 m get their paint samples every 0.5 m as well (every metre otherwise),\n" @@ -2276,6 +2352,12 @@ void editor_mode::load_orthophoto_settings() if (scenery == m_orthophoto_scenery) return; m_orthophoto_scenery = scenery; + // a scenery of its own: nobody chose for it yet, unless the user or the scenery keeps the orthophoto on or off + m_orthophoto_chosen = false; + // the height of the terrain model against the tracks of the scenery, as it was fitted or typed in last + m_nmt_state.offset = 0.0; + m_nmt_state.fitted = false; + std::as_const(EditorSettings).terrain_model_offset(scenery, m_nmt_state.offset, m_nmt_state.fitted); auto const &stored = EditorSettings.orthophoto(); editor_orthophoto::config config = m_orthophoto.settings(); @@ -2302,8 +2384,17 @@ void editor_mode::load_orthophoto_settings() { m_orthophoto_mark = marked.front(); m_orthophoto.enabled(enabled); + m_orthophoto_chosen = true; WriteLog(format("Editor: the map settings of the scenery, orthophoto %s", enabled ? "on" : "off")); } + // the user's choice stands over the scenery's; turned on only where the scenery lies on the map, a fictional one has nothing to + // show, and there it's left to the terrain model once the scenery is placed + if (auto const on{stored.on}; on.has_value() && (false == *on || config.north != 0.0 || config.east != 0.0)) + { + m_orthophoto.enabled(*on); + m_orthophoto_chosen = true; + WriteLog(format("Editor: the orthophoto %s, as the user left it", *on ? "on" : "off")); + } m_orthophoto.settings(config); m_orthophoto_origin_edit = {config.north, config.east}; // the area the imagery and the terrain model are downloaded in, none in a scenery which has none @@ -2330,6 +2421,42 @@ void editor_mode::save_orthophoto_settings() EditorSettings.save(); } +void editor_mode::choose_orthophoto(bool const On) +{ + m_orthophoto.enabled(On); + m_orthophoto_chosen = true; + EditorSettings.orthophoto().on = On; + EditorSettings.save(); +} + +void editor_mode::keep_nmt_settings() +{ + // not while a number is being typed in, which changes it at every key + if (ImGui::IsAnyItemActive()) + return; + auto const &state{m_nmt_state}; + auto &stored{EditorSettings.terrain_model()}; + auto changed{stored.band != state.band || stored.around != m_terrain_add_count || stored.shape != state.shape || stored.follow != state.follow}; + stored.band = state.band; + stored.around = m_terrain_add_count; + stored.shape = state.shape; + stored.follow = state.follow; + // the offset of the model is the scenery's, fitted to its tracks or typed in + if (false == m_orthophoto_scenery.empty()) + { + double offset{0.0}; + bool fitted{false}; + std::as_const(EditorSettings).terrain_model_offset(m_orthophoto_scenery, offset, fitted); + if (offset != state.offset || fitted != state.fitted) + { + EditorSettings.terrain_model_offset(m_orthophoto_scenery, state.offset, state.fitted); + changed = true; + } + } + if (changed) + EditorSettings.save(); +} + void editor_mode::render_orthophoto_ui() { load_orthophoto_settings(); @@ -2338,11 +2465,6 @@ void editor_mode::render_orthophoto_ui() bool changed = false; // apply to the layer bool persist = false; // and store in the editor settings - bool enabled = m_orthophoto.enabled(); - if (ImGui::Checkbox(STR_C("Show orthophoto (geoportal.gov.pl)"), &enabled)) - m_orthophoto.enabled(enabled); - - ImGui::Separator(); ImGui::TextUnformatted(STR_C("Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("PUWG 1992 (EPSG:2180): the coordinate system of the maps of Poland, in metres; the numbers as on geoportal.gov.pl")); @@ -2438,6 +2560,8 @@ void editor_mode::render_orthophoto_ui() ground_tiles.clear(); forget_ground_shapes(); m_orthophoto.refit(); + for (auto &layer : m_wms) + layer->refit(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("Samples the ground again, after it was edited")); @@ -2532,7 +2656,7 @@ void editor_mode::render_orthophoto_ui() void editor_mode::draw_orthophoto() { - if (!m_orthophoto.enabled()) + if (false == m_orthophoto.enabled() && std::none_of(m_wms.begin(), m_wms.end(), [](auto const &Layer) { return Layer->enabled(); })) return; // the camera was just published to the renderer (update_camera), so build the view from it rather than // from the renderer's previous frame; camera-relative rotation, same projection as the other overlays @@ -2541,16 +2665,11 @@ void editor_mode::draw_orthophoto() glm::mat4 const view = glm::mat4(glm::mat3(viewmatrix)); ImGuiIO const &io = ImGui::GetIO(); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; - m_orthophoto.draw(editor_mode::projection_matrix(aspect) * view, Camera.Pos, io.DisplaySize.x, io.DisplaySize.y); - if (false == m_orthophoto.overview()) - return; - char const *credit{STR_C("(c) OpenStreetMap contributors, geoportal.gov.pl")}; - auto const size{ImGui::CalcTextSize(credit)}; - auto const corner{editor_ui::view_max()}; - ImVec2 const from{corner.x - size.x - 12.0f, corner.y - size.y - 44.0f}; - auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; - drawlist->AddRectFilled(ImVec2(from.x - 4.0f, from.y - 2.0f), ImVec2(from.x + size.x + 4.0f, from.y + size.y + 2.0f), IM_COL32(255, 255, 255, 190), 3.0f); - drawlist->AddText(from, IM_COL32(40, 40, 40, 255), credit); + auto const projection{editor_mode::projection_matrix(aspect) * view}; + m_orthophoto.draw(projection, Camera.Pos, io.DisplaySize.x, io.DisplaySize.y); + // the WMS layers over it, the top one of the list last + for (auto layer{m_wms.rbegin()}; layer != m_wms.rend(); ++layer) + (*layer)->draw(projection, Camera.Pos, io.DisplaySize.x, io.DisplaySize.y); } void editor_mode::draw_ortho_compass() const @@ -2604,6 +2723,18 @@ void editor_mode::forget_ground() forget_ground_shapes(); } +void editor_mode::forget_ground_in(glm::dvec2 const &Min, glm::dvec2 const &Max) +{ + for (auto tile{ground_tiles.begin()}; tile != ground_tiles.end();) + { + glm::dvec2 const low{tile->first.first * ground_tile_size, tile->first.second * ground_tile_size}; + if (low.x <= Max.x && low.x + ground_tile_size >= Min.x && low.y <= Max.y && low.y + ground_tile_size >= Min.y) + tile = ground_tiles.erase(tile); + else + ++tile; + } +} + editor_terrain *editor_mode::terrain_at(double X, double Z) { return m_streamer.terrain_at(X, Z); @@ -3422,6 +3553,25 @@ void editor_mode::fill_terrain_area() m_paint_outline.clear(); } +void editor_mode::remove_selected_chunks() +{ + // one step of the history, as the terrain changed in a frame goes + std::size_t removed{0}; + for (auto const &key : m_terrain_selection) + if (m_streamer.exists(key)) + { + m_streamer.remove_chunk(key.first, key.second); + ++removed; + } + m_terrain_selection.clear(); + if (removed > 0) + { + ground_tiles.clear(); + forget_ground_shapes(); + } + m_terrain_status = format(STR_P("%zu chunk removed, Ctrl+Z brings it back", "%zu chunks removed, Ctrl+Z brings them back", removed), removed); +} + void editor_mode::convert_selected_chunks() { // with 0.5 m spacing the paint can be as fine, the other spacings keep a sample per metre @@ -3666,21 +3816,286 @@ void editor_mode::render_map_menu() return; bool enabled = m_orthophoto.enabled(); if (ImGui::MenuItem(STR_C("Orthophoto"), nullptr, enabled)) - m_orthophoto.enabled(false == enabled); + choose_orthophoto(false == enabled); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery")); - ImGui::MenuItem(STR_C("Orthophoto settings..."), nullptr, &m_orthophoto_window); + // the settings among the base layers in the layers window + if (ImGui::MenuItem(STR_C("Orthophoto settings..."), nullptr, orthophoto_settings_open())) + { + ui()->show_layers(); + m_orthophoto_window = true; + } ImGui::EndMenu(); } -void editor_mode::render_orthophoto_window() +bool editor_mode::orthophoto_settings_open() const { - if (false == m_orthophoto_window) - return; - ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver); - if (ImGui::Begin((std::string(STR_C("Orthophoto")) + "###orthophoto").c_str(), &m_orthophoto_window)) + // drawn open in the layers window in this frame or in the last one + return ImGui::GetCurrentContext() != nullptr && ImGui::GetFrameCount() - m_orthophoto_frame <= 1; +} + +void editor_mode::render_base_layers() +{ + auto &stored{EditorSettings.wms_layers()}; + bool save{false}; + // the WMS layers of the user's, the top one first, each drawn over the ones under it + int moved{-1}; + int removed{-1}; + for (int i = 0; i < static_cast(stored.size()); ++i) + { + auto &entry{stored[static_cast(i)]}; + ImGui::PushID(i + 1); + save |= ImGui::Checkbox("##visible", &entry.visible); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Show or hide the layer")); + ImGui::SameLine(); + if (ImGui::TreeNode("##wms", "%s", entry.title.empty() ? entry.layers.c_str() : entry.title.c_str())) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", entry.url.c_str()); + ImGui::TextDisabled(STR_C("Layers: %s"), entry.layers.c_str()); + ImGui::PopTextWrapPos(); + ImGui::SetNextItemWidth(160.0f); + ImGui::SliderFloat(STR_C("Opacity"), &entry.opacity, 0.0f, 1.0f, "%.2f"); + save |= ImGui::IsItemDeactivatedAfterEdit(); + if (false == entry.terms.empty()) + { + ImGui::TextUnformatted(STR_C("Terms of the service:")); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", entry.terms.c_str()); + ImGui::PopTextWrapPos(); + } + if (i > 0) + { + if (ImGui::SmallButton(STR_C("Up"))) + moved = i - 1; + ImGui::SameLine(); + } + if (i + 1 < static_cast(stored.size())) + { + if (ImGui::SmallButton(STR_C("Down"))) + moved = i; + ImGui::SameLine(); + } + if (ImGui::SmallButton(STR_C("Remove"))) + removed = i; + ImGui::TreePop(); + } + ImGui::PopID(); + } + // a layer swapped with the one under it, or one taken away; the ones of the view go along + if (moved >= 0 && moved + 1 < static_cast(stored.size())) + { + std::swap(stored[static_cast(moved)], stored[static_cast(moved) + 1]); + if (moved + 1 < static_cast(m_wms.size())) + std::swap(m_wms[static_cast(moved)], m_wms[static_cast(moved) + 1]); + save = true; + } + if (removed >= 0) + { + stored.erase(stored.begin() + removed); + if (removed < static_cast(m_wms.size())) + { + m_wms[static_cast(removed)]->enabled(false); + m_wms[static_cast(removed)]->detach_scene(); + m_wms.erase(m_wms.begin() + removed); + } + save = true; + } + if (save) + EditorSettings.save(); + + // the orthophoto at the bottom, its settings opened here from the map menu too + ImGui::PushID("orthophoto"); + bool enabled{m_orthophoto.enabled()}; + if (ImGui::Checkbox("##visible", &enabled)) + choose_orthophoto(enabled); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery")); + ImGui::SameLine(); + if (m_orthophoto_window) + { + ImGui::SetNextItemOpen(true); + m_orthophoto_window = false; + } + if (ImGui::TreeNode("##settings", "%s", STR_C("Orthophoto (geoportal.gov.pl)"))) + { + m_orthophoto_frame = ImGui::GetFrameCount(); render_orthophoto_ui(); - ImGui::End(); + ImGui::TreePop(); + } + ImGui::PopID(); + + // the terrain of the model under the tracks laid, as the terrain panel has it too + if (ImGui::Checkbox(STR_C("Terrain of the model right away while laying"), &m_nmt_state.grow)) + { + EditorSettings.terrain_while_laying(m_nmt_state.grow); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Where the terrain of the scenery ends, the terrain model is made under the tracks as they're laid, around the cursor from the first point placed; " + "the chunks there are stay as they are. Off, it's made only when asked for")); + + render_wms_adding(); +} + +void editor_mode::render_wms_adding() +{ + auto &adding{m_wms_adding}; + ImGui::PushID("wmsadding"); + if (false == ImGui::TreeNode("##add", "%s", STR_C("Add a WMS layer"))) + { + ImGui::PopID(); + return; + } + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("The address of a WMS service drawing in PUWG 1992 (EPSG:2180). Its tiles are kept in memory only and asked for only around the camera; " + "what the service allows is up to you, its terms are shown below")); + ImGui::PopTextWrapPos(); + ImGui::SetNextItemWidth(-1.0f); + auto const entered{ImGui::InputTextWithHint("##url", "https://...", adding.url.data(), adding.url.size(), ImGuiInputTextFlags_EnterReturnsTrue)}; + std::string const url{adding.url.data()}; + if ((ImGui::Button(STR_C("Read the layers")) || entered) && false == adding.reading.valid() && false == url.empty()) + { + adding.info.reset(); + adding.ticked.clear(); + adding.service = url; + adding.reading = std::async(std::launch::async, [url]() { return editor_orthophoto::describe(url); }); + } + if (adding.reading.valid()) + { + if (adding.reading.wait_for(std::chrono::seconds(0)) == std::future_status::ready) + { + adding.info = adding.reading.get(); + adding.ticked.assign(adding.info->layers.size(), 0); + } + else + ImGui::TextDisabled("%s", STR_C("Reading the capabilities of the service...")); + } + if (adding.info.has_value()) + { + auto const &info{*adding.info}; + ImGui::PushTextWrapPos(0.0f); + if (false == info.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), STR_C("The service can't be read: %s"), info.error.c_str()); + if (false == info.title.empty()) + ImGui::TextUnformatted(info.title.c_str()); + if (false == info.terms.empty()) + { + ImGui::TextUnformatted(STR_C("Terms of the service:")); + ImGui::TextDisabled("%s", info.terms.c_str()); + } + if (false == info.puwg && false == info.layers.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), "%s", STR_C("The service doesn't draw in PUWG 1992 (EPSG:2180), the editor can't lay it out")); + ImGui::PopTextWrapPos(); + if (false == info.layers.empty()) + { + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##filter", STR_C("Filter the layers"), adding.filter.data(), adding.filter.size()); + auto const filter{ToLower(adding.filter.data())}; + ImGui::BeginChild("##layers", ImVec2(0.0f, std::min(200.0f, ImGui::GetFrameHeightWithSpacing() * static_cast(info.layers.size()) + 4.0f)), true); + for (std::size_t i = 0; i < info.layers.size(); ++i) + { + auto const &[name, title]{info.layers[i]}; + auto const label{title.empty() || title == name ? name : title + " (" + name + ")"}; + if (false == filter.empty() && ToLower(label).find(filter) == std::string::npos) + continue; + bool ticked{adding.ticked[i] != 0}; + ImGui::PushID(static_cast(i)); + if (ImGui::Checkbox(label.c_str(), &ticked)) + adding.ticked[i] = ticked ? 1 : 0; + ImGui::PopID(); + } + ImGui::EndChild(); + std::string layers; + for (std::size_t i = 0; i < info.layers.size(); ++i) + if (adding.ticked[i] != 0) + layers += (layers.empty() ? "" : ",") + info.layers[i].first; + auto const ready{info.puwg && false == layers.empty()}; + if (false == ready) + { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); + } + auto const add{ImGui::Button(STR_C("Add")) && ready}; + if (false == ready) + { + ImGui::PopStyleVar(); + ImGui::PopItemFlag(); + } + if (add) + { + editorSettings::wms_layer entry; + entry.url = adding.service; + entry.layers = layers; + entry.title = info.title.empty() ? layers : info.title; + entry.terms = info.terms; + // on top of the others + EditorSettings.wms_layers().insert(EditorSettings.wms_layers().begin(), entry); + EditorSettings.save(); + adding.info.reset(); + adding.ticked.clear(); + adding.url.fill('\0'); + adding.filter.fill('\0'); + } + } + } + ImGui::TreePop(); + ImGui::PopID(); +} + +void editor_mode::update_wms_layers() +{ + auto const &stored{EditorSettings.wms_layers()}; + // a layer added or removed: the ones of the view made again, as the settings have them + auto same{m_wms.size() == stored.size()}; + for (std::size_t i = 0; same && i < stored.size(); ++i) + same = m_wms[i]->settings().service == stored[i].url && m_wms[i]->settings().layers == stored[i].layers; + if (false == same) + { + for (auto &layer : m_wms) + { + layer->enabled(false); + layer->detach_scene(); + } + m_wms.clear(); + for (auto const &entry : stored) + { + auto layer{std::make_unique()}; + auto config{layer->settings()}; + config.service = entry.url; + config.layers = entry.layers; + config.hires = false; + config.year = 0; + config.in_scene = false; + layer->settings(config); + ground_sources(*layer); + m_wms.push_back(std::move(layer)); + } + } + // laid out by the origin of the scenery and onto the ground as the orthophoto is, around the camera as far as it + load_orthophoto_settings(); + auto const &base{m_orthophoto.settings()}; + auto const placed{base.north != 0.0 || base.east != 0.0}; + for (std::size_t i = 0; i < m_wms.size(); ++i) + { + auto &layer{*m_wms[i]}; + auto config{layer.settings()}; + if (config.north != base.north || config.east != base.east || config.radius != base.radius || config.drape != base.drape || config.lift != base.lift || config.height != base.height || + config.opacity != stored[i].opacity) + { + config.north = base.north; + config.east = base.east; + config.radius = base.radius; + config.drape = base.drape; + config.lift = base.lift; + config.height = base.height; + config.opacity = stored[i].opacity; + layer.settings(config); + } + layer.enabled(stored[i].visible && placed); + layer.update(Camera.Pos); + } } void editor_mode::render_view_menu() @@ -3954,7 +4369,7 @@ void editor_mode::terrain_workspace(bool const Open) void editor_mode::render_terrain_tool_options() { - // the orthophoto has a window of its own, see render_orthophoto_window() + // the orthophoto has its settings among the base layers in the layers window, see render_base_layers() render_terrain_ui(); } @@ -5182,15 +5597,15 @@ void editor_mode::render_first_steps() {"L", "The lay tool"}, {"LMB", "A click on the ground or on a free end: where the track starts"}, {"LMB", "Each next click: a bend of the line, the curve at it made by itself. The line shown isn't a track yet"}, - {"Enter", "Lays the track clicked; the clicks after it go on from its end"}, - {"Backspace", "Takes the last point back, then the last piece laid"}, + {"Enter", "Lays the track clicked; to go on from its end, click the end"}, + {"Backspace", "Takes the last point back"}, {"Esc", "Ends laying; the points not laid yet go only with the second Esc"}}, {{first_step::tracks, "Open the Tracks field of work"}, {first_step::lay, "Turn the lay tool on"}, {first_step::laid, "Click a few points and lay them with Enter"}}, trial::lay}, {"Curves", "curves", {{"LMB", "Click the bends of the line: each gets a curve as large as fits; one too sharp for the speed set in the panel has its label in orange"}, - {"Tab", "A curve seen on the photo: click points along its middle, the last two on the straight after it; one of two or three radii comes out a compound curve"}, + {"Tab", "A curve seen on the photo: click points along its middle, the curve ending at the last one; one of two or three radii comes out a compound curve"}, {"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 straight, Enter puts the point"}, {"wheel", "Alone it zooms, as everywhere"}}, @@ -5273,7 +5688,7 @@ void editor_mode::render_first_steps() {"F5", "The Terrain field of work: the terrain model (NMT) is in its tool options"}, {"1", "Location: where the scenery lies, a click on the map or the place typed"}, {"2", "Load along the tracks, or around the camera: the model comes from geoportal.gov.pl"}, - {"3", "Tracks onto the terrain: all the tracks go onto the model; the terrain to some tracks only: select them, Terrain in the inspector"}, + {"3", "Tracks onto the terrain: the tracks not fitted yet go onto the model, the ones fitted stay; the terrain to some tracks only: select them, Terrain in the inspector"}, {"Ctrl + Z", "Takes the changes back"}}, {{first_step::terrain, "Open the Terrain field of work"}, {first_step::nmt, "Load terrain from the model"}}, trial::terrain, @@ -5537,6 +5952,7 @@ void editor_mode::render_shortcuts_window() {"Tracks", {{"Esc", "Select tool; cancels what's going on first"}, {"L / T / U", "Lay track / switch / turntable"}, + {"H", "Throw the selected switch over to its other path, in the editor only"}, {"G (C)", "Plan: a straight, its ends and the middle; a curve, its vertex and radius"}, {"I / B", "Signalling / objects along the track"}, {"P / V / J", "Profile / speed / joints"}, @@ -5544,8 +5960,8 @@ void editor_mode::render_shortcuts_window() {"Shift+LMB", "Add a track to the selection, drag: a box"}, {"RMB on a track", "What can be done with it"}, {"K", "Split the selected track at the cursor, a break of the grade at the height of the ground"}, - {"L, LMB, LMB...", "Lay track: each click a bend of the straights, eased with a large curve; Enter lays them and goes on from the end"}, - {"Tab while laying", "The points clicked next go along the middle of a curve seen on the photo, the last two on the straight after it; Tab again: back to the bends"}, + {"L, LMB, LMB...", "Lay track: each click a bend of the straights, eased with a large curve; Enter lays them; a click on a free end goes on from it"}, + {"Tab while laying", "The points clicked next go along the middle of a curve seen on the photo, which ends at the last one; Tab again: back to the bends"}, {"Drag from a free end", "Lead a track out of it: a straight, an arc and a straight where a curve is traced (Tab); onto another free end: join the two"}, {"Ctrl+wheel", "Dragging from a free end: radius"}, {"Shift+wheel", "Dragging over another free end: another way to join"}, @@ -5553,7 +5969,7 @@ void editor_mode::render_shortcuts_window() {"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 the points clicked / take back the last point, then the last piece laid"}, + {"Enter / Backspace", "Laying track: lay the points clicked / take back the last point"}, {"Esc twice while laying", "Drop the points clicked and not laid yet"}, {"Del", "Delete the selected tracks"}}}, {"Plan of a curve", @@ -6245,6 +6661,11 @@ void editor_mode::exit() m_fill_last.clear(); m_orthophoto.cancel_pending(); m_orthophoto.detach_scene(); // the layer is an editor aid, keep it out of the other modes + for (auto &layer : m_wms) + { + layer->cancel_pending(); + layer->detach_scene(); + } // the download of the area held while the scenery is driven, it goes on once the editor is back m_orthophoto.area_paused(true); m_nmt.prefetch_paused(true); @@ -6513,7 +6934,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_DELETE: - if (is_press(Action)) + // the chunks of the terrain selected, where the terrain is the field of work; else what's selected in the scenery + if (is_press(Action) && m_terrain_open && false == m_terrain_selection.empty()) + remove_selected_chunks(); + else if (is_press(Action)) delete_selected(); break; @@ -6563,6 +6987,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_TAB: + // a switch dragged from a free end and bent: its diverging track the other way than the main one, or the same way + if (is_press(Action) && false == anyModifier && m_switch.placing && m_switch.free && false == ImGui::GetIO().WantTextInput) + { + m_switch.one_sided = false == m_switch.one_sided; + break; + } // laying: the bends of straights or a curve traced, for the points clicked from now on if (is_press(Action) && false == anyModifier && ui()->mode() == nodebank_panel::TRACK && m_lay.active && false == ImGui::GetIO().WantTextInput) lay_switch_mode(); @@ -6866,7 +7296,8 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const finish_extend(); if (m_switch.placing) { - // a press let go with no drag places no switch, a click on the ground with no track there selects nothing + // a press let go with no drag places no switch: the track it took is only selected, the ground with no track there + // selects nothing auto const &io{ImGui::GetIO()}; if (glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - m_switch.pressed_at) <= 10.0f) { diff --git a/application/editormode.h b/application/editormode.h index aeb5377c..270266cc 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -53,6 +53,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -346,6 +347,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri double slope{1.5}; double reach{60.0}; std::vector pending; // chunks to make once their model is in + // of the chunks waiting, those there already with flat ground, made without the model: the model fills their ground + std::set refill; std::set given_up; // chunks the growing gave up, next to terrain not from the model: not asked for again std::size_t made{0}; std::size_t empty{0}; // chunks not made: the model has no data there @@ -358,7 +361,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t shaped{0}; std::string status; std::string error; - // the tracks of a scenery built at any height put onto the terrain model: the whole scenery lifted to it, then the grade + // the tracks of a scenery built at any height put onto the terrain model: the model offset to meet the scenery, then the grade // line of each line of tracks fitted to the model, the longest first, then the terrain made along them enum class automatic { @@ -368,6 +371,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri }; automatic stage{automatic::idle}; std::vector probes; // places of the tracks whose model is waited for + std::size_t probes_kept{0}; // the last of the probes, on tracks fitted already: they tell the offset the model had std::size_t tracks{0}; // rail tracks when the grade lines began, and the paths the grade lines split off since std::size_t split{0}; std::vector left; // tracks whose grade line is still to be fitted @@ -392,10 +396,20 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::map, std::tuple> loose; // the points of the lines over a bridge or a viaduct, the model falling away under them: no formation is laid there std::map, std::vector> bridged; + // a turntable lies level in its pit, which the model has, at the height of the tracks running up to it: it isn't put onto + // the model, and a line ending on its rim ends there level at its height, turning to the model away from it + struct turntable + { + TTrack *table{nullptr}; + glm::dvec3 centre{0.0}; // at the height of its rails + double rim{0.0}; // m from the centre + std::vector> ends; // of the paths running up to it: track, path, at its end + }; + std::vector turntables; + std::size_t levelled{0}; // turntables moved to the height of their tracks std::size_t lines{0}; // grade lines fitted and applied std::size_t refused{0}; // grade lines the checks didn't let apply std::size_t departures{0}; // grade lines parting from the terrain model by much - double lifted{0.0}; // m the scenery went up or down // the terrain made under the tracks as they are: the model drawn to them by the difference found at the places along them, // fading out away from them into the offset of the whole scenery bool under{false}; @@ -432,6 +446,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri }; // Offset: the heights of the model go up or down by it in the whole chunk, in place of the offsets of the fit nmt_chunk add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional const Offset = std::nullopt); + // a chunk there with its ground level all over, as made where there was no ground and no model: none of the shape of the land + bool nmt_flat_chunk(heightmap::chunk_key const &Key) const; // how much the terrain lies over the model at the points: none where there's too little terrain at them, nor where it // doesn't follow the model (Foreign), as one made by hand std::optional nmt_terrain_offset(std::vector const &Points, bool &Foreign) const; @@ -497,8 +513,12 @@ class editor_mode : public application_mode, private editor_track::observer, pri void nmt_place_scenery(glm::dvec2 const &Point); void nmt_accept_offer(); // the whole scenery: tracks, includes and models, moved up or down as one undo step - void nmt_lift_scenery(double const Offset); std::size_t nmt_join_ends(); + // the turntables as they lie, each with the ends of the tracks running up to it on its rim + std::vector nmt_turntables() const; + // each turntable level at the height of the tracks running up to it, its include with it, before the grade lines are fitted: + // at the height of the ones fitted already, else of the terrain model beside its rim; the number of them moved + std::size_t nmt_level_turntables(); // Unfitted: only the tracks whose grade line isn't fitted yet, the scenery left at its height void nmt_auto_start(bool const Unfitted = false); void nmt_auto_step(); @@ -536,6 +556,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri } // converts the point spacing of the selected chunks to m_terrain_convert_spacing void convert_selected_chunks(); + // the chunks selected with the spacing tool taken out of the terrain, in one step of the history + void remove_selected_chunks(); // takes the height of whatever is under the cursor (terrain, track, model) for the target of the level brush. returns: false if there's nothing bool pick_terrain_target(); bool terrain_clicking() const { return m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::water || m_terrain_tool == terrain_tool::fill; } @@ -545,6 +567,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_terrain_overlay(); // forgets what's known about the ground, after the terrain changed void forget_ground(); + // the ground gathered where the terrain changed in the rectangle (x, z) gathered again when asked for, the rest kept + void forget_ground_in(glm::dvec2 const &Min, glm::dvec2 const &Max); // conversion of the ground of the terrain files of the scenery (terrain directive, .txtf/.btf) to heightmap terrain struct ground_conversion { @@ -690,8 +714,16 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector m_openscenery_list; std::string m_openscenery_choice; void render_open_scenery_popup(); + // the settings of the orthophoto, among the base layers in the layers window: asked to be opened there, the frame they were last + // drawn open in bool m_orthophoto_window{false}; - void render_orthophoto_window(); + // the user turned the orthophoto on or off, or the scenery keeps it so: the terrain model made, or the area drawn, leaves it as it + // is then, it turns it on only where nobody chose + bool m_orthophoto_chosen{false}; + // the user turns the orthophoto on or off: kept with the editor, for every scenery + void choose_orthophoto(bool const On); + int m_orthophoto_frame{-10}; + bool orthophoto_settings_open() const; // view menu: the top view, the lanes of the roads, the structure gauge, the environment void render_view_menu(); // time of day, day of the year, visibility, clouds and air temperature, as the scenery is seen in the editor @@ -718,10 +750,32 @@ class editor_mode : public application_mode, private editor_track::observer, pri void read_georeference(); void draw_orthophoto(); void draw_ortho_compass() const; - // picks up the stored settings and the origin of the current scenery + // picks up the stored settings and the origin of the current scenery, and the offset of the terrain model the scenery has void load_orthophoto_settings(); void save_orthophoto_settings(); + // the settings of the terrain model changed, kept with the editor, and the offset of the model with the scenery's + void keep_nmt_settings(); editor_orthophoto m_orthophoto; + // the WMS layers of the user's over the orthophoto, the top one first, as the settings of the editor keep them; their tiles are + // kept in memory only + std::vector> m_wms; + // a WMS layer being added: the address typed, what its capabilities tell, read on a thread of its own, the layers ticked + struct wms_adding + { + std::array url{}; + std::array filter{}; + std::future reading; + std::optional info; + std::string service; // the address the capabilities were read of + std::vector ticked; + } m_wms_adding; + // the orthophoto and the WMS layers of the user's at the bottom of the layers window, with their settings + void render_base_layers(); + void render_wms_adding(); + // the WMS layers as the settings have them, laid out by the origin and on the ground of the orthophoto, around the camera + void update_wms_layers(); + // the ground the imagery is fitted onto, the same for the orthophoto and the WMS layers + void ground_sources(editor_orthophoto &Layer); std::size_t m_ortho_resident{0}; // chunks of the terrain in memory, as the imagery last heard of them std::size_t m_ortho_waiting{0}; // squares of the terrain model asked for and not in yet, the same std::string m_orthophoto_scenery; // scenery whose origin is loaded @@ -1086,6 +1140,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void route_bind_ends(); bool route_from_curve(TTrack &Track); bool route_from_line(TTrack &Track); + // the first curve of the design and the last one going on from the curves of the tracks adjoining, where they turn their way: a + // curve cut by a switch bent into it, say. Lays the design + void route_curved_ends(); alignment::vertex route_vertex_of(editor_track::curve const &Curve) const; bool route_load(TTrack &Track); void route_read_stored(TTrack &Track); @@ -1107,7 +1164,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri switch_store::record record; std::vector switches; std::vector parts; - glm::dvec2 crossing{0.0}; std::array ends{}; std::array into{}; }; @@ -1288,6 +1344,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool free{false}; editor_track::snap_target free_end; std::optional free_heading; + // from a free end, dragged on past its end: bent, its diverging track turning the other way than the main one, the same way with + // Tab (one-sided) + bool one_sided{false}; std::vector frame; std::vector frame_tracks; std::vector templates; @@ -1334,6 +1393,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri switch_branch branch; }; std::vector branches; + // a switch bent into a curve slides along it: the curve its main track and the tracks next to it make, where the switch + // starts on it; low and high the distances it may go back and on + std::vector frame; + double station{0.0}; }; slide_state slide; double slide_typed{0.0}; @@ -1402,6 +1465,12 @@ class editor_mode : public application_mode, private editor_track::observer, pri }; bool slip_room(slip_set const &Slip, slip_slide &Slide, std::string &Reason) const; bool slide_slip(slip_set const &Slip, double const Distance); + // a switch bent into a curve, its main track going on into a plain track at each end: the curve the three make, the two tracks, + // where the switch starts on it; false for any other + bool bent_switch_frame(TTrack const &Track, std::vector &Frame, std::array &Sides, double &Station) const; + // the paths of the switch moved along the curve by the distance, shaped as the curve is there + std::vector bent_switch_paths(TTrack const &Track, std::vector const &Frame, double const Station, double const Distance) const; + bool slide_bent_switch(TTrack &Track, double const Distance); void switch_slide_start(TTrack &Track); void switch_slide_update(); void switch_slide_finish(); @@ -1499,7 +1568,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri }; extend_tool m_extend; // what a point clicked while laying is: a bend of the straights, eased with a large curve, or a point along the middle of a curve - // seen on the photo, the last two of these on the straight after it + // seen on the photo, the curve ending at the last of these enum class lay_mode { straights, @@ -1552,13 +1621,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri double status_time{-100.0}; std::string skew; // what Alt turned the straight to double escape_time{-100.0}; // when Esc was pressed over points not laid: the second Esc drops them - struct step - { - editor_track::snap_target start; - glm::dvec3 point{0.0}; - }; - std::vector chain; - std::size_t history{0}; // the step of the history the chain was begun at + std::size_t history{0}; // the step of the history the track laid last was begun at }; lay_tool m_lay; alignment::limits laying_norms() const; @@ -1589,7 +1652,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri // the curves of a stretch traced, the ones fitted lately kept for the same points // none while the stretch the cursor moves along is still being fitted; Meanwhile: the fit done last of the same points but the one // at the cursor until then - std::optional traced_stretch(alignment::design const &Base, std::vector const &Points, int const Radii, bool const Straights, bool const Meanwhile = true) const; + // Exact receives whether the fit is the one of these points, not one shown meanwhile; Going: the curve going on from the curve of the + // free end Base starts at, alignment::fit_going_on + std::optional traced_stretch(alignment::design const &Base, std::vector const &Points, int const Radii, bool const Straights, bool const Meanwhile = true, + bool *Exact = nullptr, bool const Going = false) const; // the curve through a row of bends clicked close along one, by traced_stretch alignment::stretch_fit bends_fit() const { @@ -1609,9 +1675,49 @@ class editor_mode : public application_mode, private editor_track::observer, pri void start_free_switch(glm::dvec3 const &Ground); // the start of the switch, the way its main track goes and the side of its diverging one, as the drag gives them so far void free_switch_pose(glm::dvec3 &Start, glm::dvec2 &Heading, int &Side, double &Grade) const; + // a switch laid on from a free end, dragged on past its end: bent, its main track turning toward the cursor + struct free_bend + { + double radius{0.0}; // of the main track + int turn{1}; // the way the main track turns, 1 to the left + int side{1}; // of the diverging track + std::vector frame; // the curve the switch is bent into + std::vector paths; + switch_curve::switch_report report; + // a free end the cursor is at, which the main track leads into, the diverging one with Shift + editor_track::snap_target target; + bool diverging{false}; + editor_track::branch_link link; + std::string problem; // why it can't lead into the target + }; + // false where it isn't bent: near the switch, along the straight, or not from a free end + bool free_switch_bend(free_bend &Bend) const; + bool free_switch_bend_now(free_bend &Bend) const; + // laid on from an end of a switch: what the norms say of the change of the curvature from that switch to the tracks of this one + std::vector free_switch_exits(std::vector const &Paths) const; std::vector free_switch_paths() const; void insert_free_switch(); std::vector switch_preview() const; + std::vector switch_preview_now() const; + // a preview worked out once a frame for the drawing and the readouts which all show it: the frame and the state of the tool it + // was worked out for + template struct frame_memo + { + int frame{-1}; + std::vector key; + Result result{}; + bool fresh(std::vector const &Key) const { return frame == ImGui::GetFrameCount() && key == Key; } + void keep(std::vector Key, Result const &Value) + { + frame = ImGui::GetFrameCount(); + key = std::move(Key); + result = Value; + } + }; + std::vector switch_memo_key() const; + mutable frame_memo> m_switch_memo; + mutable frame_memo> m_bend_memo; + mutable frame_memo> m_extend_memo; void insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side); void switch_placed(std::vector const &Created); static std::vector sample_chain(editor_track::chain const &Chain); @@ -1624,12 +1730,13 @@ class editor_mode : public application_mode, private editor_track::observer, pri // keeps 6 m off it; What receives which double curved_switch_start(int const Direction, std::string *What = nullptr) const; // the switch made of the paths, laid into the curve at the station, as ST-T1-A6 sees it - switch_curve::switch_report curved_switch_report(std::vector const &Paths, double const Station, int const Direction) const; + switch_curve::switch_report curved_switch_report(std::vector const &Paths, std::vector const &Frame, double const Station, int const Direction) const; static double nearest_station(std::vector const &Frame, glm::dvec3 const &Point); static std::vector curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side); // the path of a switch template laid into the curve: x of the template across the main track to the Side, z along it from the - // Station on, going Direction along the curve. The heights and the cant are those of the line where the point is along it - static segment_data frame_path(segment_data const &Local, std::vector const &Frame, double const Station, int const Direction, int const Side); + // Station on, going Direction along the curve; past Bent along it going on straight as the curve heads there. The heights and the + // cant are those of the line where the point is along it + static segment_data frame_path(segment_data const &Local, std::vector const &Frame, double const Station, int const Direction, int const Side, std::optional const &Bent = std::nullopt); void insert_curved_switch(int const Direction, int const Side); // a crossover between two tracks running side by side along a curve (editormodeconnectioncurve.cpp) struct curve_mapping; @@ -1689,29 +1796,38 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector> &States, std::vector &Created); bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style); // where a slip or a crossing goes for the cursor at Point on the straight Line: the crossing of Line with the rail track - // nearest the cursor, or the double slip there to be replaced + // nearest the cursor, or the double slip there to be replaced. With no track crossing there and Side given (1 to the left, -1 to + // the right of Line), the slip brings the crossing track along at its own angle, turning off to that side ahead struct slip_plan { std::string problem; std::string note; + bool lone{false}; // no track crosses Line by the cursor TTrack *replaces{nullptr}; - TTrack *other{nullptr}; + TTrack *other{nullptr}; // the crossing track, none where the slip brings it along glm::dvec2 crossing{0.0}; glm::dvec2 first{0.0, 1.0}; glm::dvec2 direction{0.0, 1.0}; double angle{0.0}; double alongfirst{0.0}; double half{0.0}; + // where the slip brings its crossing track along: from the crossing to the joints of its template, past the frogs of the diamond, + // the straights of both tracks laid out that far; the ends of the straights there, in the order of the points + double reach{0.0}; + std::array ends{}; editor_track::slip_curve curve; std::array points{}; }; - bool plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan) const; + bool plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan, int const Side = 0) const; // a slip is made for its skos: where the tracks cross at another one, the refusal, which tells the template made for theirs; empty // where it fits, and for a crossing std::string slip_skew(editor_track::switch_template const &Shape, double const Angle) const; // the cursor where it points at the straight, at the height of the straight there glm::dvec3 cursor_on_straight(editor_track::straight const &Line) const; - bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape); + bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, int const Side = 0); + // the side a slip dragged out on a straight with no track crossing it brings its crossing track to: the one through the quarter of + // the cursor + int lone_slip_side() const; bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape); // Bend: what a path of the slip laid out as on straights turns into where it's bent into a curve std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type = "Rkpd", @@ -1871,6 +1987,11 @@ class editor_mode : public application_mode, private editor_track::observer, pri void render_straight_ui(); track_tab m_track_tab{track_tab::path}; void delete_selected_track(); + // the switch thrown over to its other path in the editor, its blades going over as in the simulation; the scenery keeps no state of it + void throw_switch(TTrack &Switch); + // the blades of the switches thrown in the editor moved on by the time of the frame: the time of the simulation stands still in it + void move_thrown_switches(); + std::vector m_thrown_switches; std::string designs_trimmed(std::vector const &Removed); void start_straight_gesture(int const Tool); void finish_straight_gesture(); @@ -2542,11 +2663,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool show_terrain{true}; bool show_model{true}; bool show_track{true}; - bool camera_follows{true}; - // the part of the route the plot shows: as last seen and when, and as the camera last showed it - std::pair view_seen{0.0, 0.0}; - double view_seen_time{0.0}; - std::pair view_framed{0.0, 0.0}; bool show_plan{true}; int selected{-1}; int dragging{-1}; @@ -2646,9 +2762,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri // the terrain under the tracks changed by the last Edits steps of the history which changed tracks void profile_follow_terrain(std::size_t const Edits); void profile_fit_view(); - // the camera shows the part of the route the plot shows, once the plot stops moving - void profile_follow_view(); + // the camera to the part of the route the plot shows; the plot to the part of the route the camera shows, its zoom kept void profile_frame_view(); + void profile_to_camera(); void profile_after_undo(); void profile_follow_edits(); struct profile_view; diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 4b3e6d04..f66a831e 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -52,6 +52,56 @@ namespace // the curve of the track of a free end the line goes on from or into: the one of a plain track always, the one of the diverging track // of a switch where a curve is traced from it, Traced; the bends of straights begin there with a straight, the curvature jumping as at // the start of the switch +// the way a line through the points heads at the last of them: along the circle through the last three, along the last two where they +// keep straight +glm::dvec2 heading_at_end(std::vector const &Points) +{ + auto const count{Points.size()}; + auto const chord{Points[count - 1] - Points[count - 2]}; + if (count < 3) + return chord; + auto const &first{Points[count - 3]}; + auto const ab{Points[count - 2] - first}; + auto const ac{Points[count - 1] - first}; + auto const twice{2.0 * cross(ab, ac)}; + if (std::abs(twice) < 1e-6 * glm::length(ab) * glm::length(ac)) + return chord; + auto const centre{first + glm::dvec2{ac.y * glm::dot(ab, ab) - ab.y * glm::dot(ac, ac), ab.x * glm::dot(ac, ac) - ac.x * glm::dot(ab, ab)} / twice}; + auto const radial{Points[count - 1] - centre}; + glm::dvec2 tangent{-radial.y, radial.x}; + return glm::dot(tangent, chord) < 0.0 ? -tangent : tangent; +} + +// a stretch fitted from its far end back, its points the other way round and the first one last, as the line runs: the arcs of its curve +// and their transitions in the other order, the offsets of the points but the first, the heading it leaves the first point with +alignment::trace_stretch turned_round(alignment::trace_stretch const &Fit) +{ + auto result{Fit}; + auto const &curve{Fit.vertices.front()}; + auto &turned{result.vertices.front()}; + turned.transition_in = curve.transition_out; + turned.transition_out = curve.transition_in; + if (curve.compound && false == curve.arcs.empty()) + { + // the arcs from the far end: the first one of the vertex, then its arcs, each with the transition before it + std::vector arcs{{curve.radius, 0.0, curve.share, curve.first_cant}}; + arcs.insert(arcs.end(), curve.arcs.begin(), curve.arcs.end()); + std::reverse(arcs.begin(), arcs.end()); + // the transition between two arcs moves to the arc after it the other way round + for (std::size_t i = arcs.size(); i-- > 1;) + arcs[i].transition = arcs[i - 1].transition; + turned.radius = arcs.front().radius; + turned.share = arcs.front().share; + turned.first_cant = arcs.front().cant; + turned.arcs.assign(arcs.begin() + 1, arcs.end()); + } + if (false == result.offsets.empty()) + result.offsets.pop_back(); + std::reverse(result.offsets.begin(), result.offsets.end()); + result.leaving = -Fit.leaving; + return result; +} + bool goes_on_curved(editor_track::snap_target const &End, bool const Traced) { return End.track != nullptr && (End.track->eType == tt_Normal || (End.track->eType == tt_Switch && Traced)); @@ -73,6 +123,8 @@ void adjoining_curve(editor_track::snap_target const &End, bool const Atend, boo (Atend ? Design.end_turn : Design.start_turn) = curvature > 0.0 ? 1 : -1; (Atend ? Design.end_cant : Design.start_cant) = std::max(0.0, cant); (Atend ? Design.end_curved : Design.start_curved) = true; + // the curve of a switch goes on at its radius, with no transition to another one in the switch's way + (Atend ? Design.end_held : Design.start_held) = End.track->eType == tt_Switch; } } // namespace @@ -121,16 +173,27 @@ void editor_mode::update_build_tools() lay.preview_length = 0.0; if (false == lay.active) return; + // a slow preview tells in the log where the time went: the cursor on the ground, the snap, the height, the plan of the line + auto stamp{std::chrono::steady_clock::now()}; + auto const lap = [&]() { + auto const now{std::chrono::steady_clock::now()}; + auto const time{std::chrono::duration(now - stamp).count()}; + stamp = now; + return time; + }; // geometric ray, same as extend: Mouse_Position() is a delayed depth pick and lags the cursor 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)); + auto const oncursor{lap()}; lay.skew.clear(); if (pointing && ImGui::GetIO().KeyAlt && m_typed.empty() && false == lay.points.empty()) 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, {}); + auto const snapping{lap()}; if (lay.mouse_snap.track == nullptr) lay.mouse = lay_bed(lay.mouse); + auto const bedding{lap()}; if (false == m_extend.active) joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); lay_trim(); @@ -158,6 +221,17 @@ void editor_mode::update_build_tools() m_follow_preview = true; lay.preview = lay_pieces(points, modes, end, lay.preview_length, lay.preview_error, &lay.plan); m_follow_preview = false; + auto const planning{lap()}; + if (oncursor + snapping + bedding + planning > 30.0) + { + std::string kinds; + for (auto const mode : modes) + kinds += mode == lay_mode::curve ? 'c' : 's'; + WriteLog(format("Editor: slow lay preview: cursor on the ground %.0f ms, snap %.0f ms, height %.0f ms, plan %.0f ms; points %s, from a free end %d, to one %d, %zu pieces, L %.0f m%s", oncursor, snapping, bedding, + planning, kinds.c_str(), lay.start.track != nullptr ? 1 : 0, end.track != nullptr ? 1 : 0, lay.preview.size(), lay.preview_length, + lay.preview_error.empty() ? "" : (", " + lay.preview_error).c_str()), + logtype::generic); + } } bool editor_mode::free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool) @@ -535,6 +609,9 @@ std::vector editor_mode::extend_pieces() const return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error); } m_join_last = {}; + auto radius{m_lay.radius}; + if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) + radius = std::abs(values[0]); if (m_track_layout != track_layout::straight && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) { auto const &diverging{tool.track->m_paths[1]}; @@ -542,24 +619,21 @@ std::vector editor_mode::extend_pieces() const auto const begin{plan_of(curve.first(0.0))}; auto const finish{plan_of(curve.first(1.0))}; auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; - if (auto const radius{editor_track::branch_radius(diverging)}; radius > 0.0) + if (auto const branch{editor_track::branch_radius(diverging)}; branch > 0.0) { int const side{turn > 0.0 ? 1 : -1}; auto const &direction{tool.direction}; - auto const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * radius)}; + auto const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * branch)}; auto const angle{geometry::signed_angle(plan_of(tool.point) - centre, plan_of(tool.mouse) - centre) * side}; - if (angle * radius < 0.5) + if (angle * branch < 0.5) return result; - auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side)}; + auto pieces{arc_pieces(tool.point, direction, tool.grade, branch, std::min(angle, glm::pi()), side)}; for (auto &piece : pieces) piece.rolls = {diverging.rolls[1], diverging.rolls[1]}; follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); return pieces; } } - auto radius{m_lay.radius}; - if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) - radius = std::abs(values[0]); auto const eased{eased_layout(tool.track, tool.point, tool.direction, tool.mouse, radius)}; auto pieces{eased.has_value() && false == eased->pieces.empty() ? eased->pieces : editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); @@ -568,10 +642,19 @@ std::vector editor_mode::extend_pieces() const std::vector editor_mode::extend_preview() const { - m_follow_preview = true; - auto pieces{extend_pieces()}; - m_follow_preview = false; - return pieces; + auto const &tool{m_extend}; + auto const &io{ImGui::GetIO()}; + std::vector key{tool.mouse.x, tool.mouse.y, tool.mouse.z, tool.point.x, tool.point.z, io.MousePos.x, io.MousePos.y, tool.snap.position.x, tool.snap.position.z, + static_cast(reinterpret_cast(tool.track)), static_cast(reinterpret_cast(tool.snap.track)), static_cast(tool.path), + static_cast(tool.active) + 2.0 * tool.atend + 4.0 * io.KeyShift + 8.0 * io.KeyCtrl + 16.0 * io.KeyAlt, m_join_radius, static_cast(m_join_variant), + static_cast(m_track_layout), static_cast(m_history.size())}; + if (false == m_extend_memo.fresh(key)) + { + m_follow_preview = true; + m_extend_memo.keep(std::move(key), extend_pieces()); + m_follow_preview = false; + } + return m_extend_memo.result; } TTrack const *editor_mode::speed_track(TTrack const *From, TTrack const *To) const @@ -619,13 +702,7 @@ std::vector editor_mode::switch_exit_warnings(TTrack const *From, g return {}; auto const &joint{switched == From ? Start : End}; // 1/m, positive to the left, as the piece runs - auto const curvature_at = [](segment_data const &Piece, double const T) { - bezier const curve{Piece}; - auto const first{curve.first(T)}; - auto const second{curve.second(T)}; - auto const speed{std::hypot(first.x, first.z)}; - return speed < 1e-9 ? 0.0 : (first.x * second.z - first.z * second.x) / (speed * speed * speed); - }; + auto const curvature_at = [](segment_data const &Piece, double const T) { return bezier{Piece}.plan_curvature(T); }; // the path of the switch ending at the joint, its pieces from the joint back into the switch, each with its curvature at its ends as // the line runs on beyond the joint std::vector> back; @@ -687,7 +764,6 @@ std::vector editor_mode::join_pieces(TTrack const *From, editor_tr request.end.roll = End.track != nullptr ? editor_track::joint_roll(*End.track, End.point) : 0.0; request.radius = m_join_radius; request.variant = m_join_variant; - request.transitions = true; request.speed = design_speed(From, End.track); request.kink = m_joints.kink_angle() * 0.5; request.shape = m_route.design.shape; @@ -1017,12 +1093,12 @@ std::vector editor_mode::lay_pieces(std::vector const return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); }; std::vector pieces; - // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them; at - // the end of the diverging track of a switch where a curve is traced from it, the curve of the switch going on, the bends of - // straights beginning there with a straight + // the curvature of the track laid has to go on from the one of the plain track it joins, a jump gets a transition curve between them auto const grade{(Points.back().y - Points.front().y) / std::max(1.0, plan_distance(Points.front(), Points.back()))}; + // out of the curve of a plain track a line which doesn't go on with it begins by a transition; at the end of the diverging track of a + // switch it goes on with the curve of the switch or begins with a straight right there, as lay_line decided, never eased auto const eased = [&](editor_track::snap_target const &Joint, double const Chainage, double const Sign) { - if (false == goes_on_curved(Joint, traced_from(Points, Modes, Sign < 0.0))) + if (Joint.track == nullptr || Joint.track->eType != tt_Normal) return editor_track::easing{}; auto result{editor_track::ease_out_of(*Joint.track, Joint.position, grade * Sign, design.speed, design.norms)}; auto const own{Sign * curvature_at(shape, Chainage)}; @@ -1087,15 +1163,6 @@ std::vector editor_mode::lay_pieces(std::vector const auto const after = [&](editor_track::easing const &Easing, glm::dvec3 const &Joint, glm::dvec3 const &Own) { return false == Easing.pieces.empty() && plan_distance(Joint, Own) > 0.01; }; auto const &before{after(in, Points.front(), design.start) ? in : editor_track::easing{}}; auto const &beyond{after(out, Points.back(), design.end) ? out : editor_track::easing{}}; - // at the diverging track of a switch the curve goes on, a line jumping from it isn't laid - auto const jumps = [](editor_track::snap_target const &Joint, editor_track::easing const &Needed, editor_track::easing const &Laid) { - return Joint.track != nullptr && Joint.track->eType == tt_Switch && false == Needed.pieces.empty() && Laid.pieces.empty(); - }; - if (jumps(tool.start, in, before) || jumps(End, out, beyond)) - { - Error = STR("At the end of the switch the line goes on with the curve of the switch, and there's no room for the transition out of it here") + (Error.empty() ? std::string{} : "\n" + Error); - return {}; - } Length = editor_track::eased_length(before, Length, beyond); pieces = editor_track::eased_pieces(before, pieces, beyond); } @@ -1131,8 +1198,9 @@ bool editor_mode::lay_line(std::vector const &Points, std::vector 1e-6}; // from a free end on a curve, or to one, the curve laid next to it turning its way goes on from its curvature: it begins right at the - // end, not on a straight, its transition going from that curvature. Where no such curve fits, the line is laid from a straight there, - // which lay_pieces eases out of the curve. At a switch only a curve traced from it, the bends of straights from a straight there + // end, not on a straight, its transition going from that curvature. A line which doesn't turn its way begins from a straight there: out + // of the curve of a plain track by the transition lay_pieces lays, right at the end of the diverging track of a switch, as the switch + // goes on with a straight. At a switch the curve goes on only with a curve traced from it, the bends of straights begin with a straight if (Adjoining) { if (tool.start.track != nullptr && glm::length(plan_of(tool.start.direction)) > 1e-6) @@ -1178,12 +1246,8 @@ bool editor_mode::lay_line(std::vector const &Points, std::vector const &Points, std::vector 0.0 && tool.start.track != nullptr && tool.start.track->eType == tt_Switch) + // the stretch right from the free end the line starts at, and whether the curve of the end goes on into it: where it turns its way + auto const fromend{bends.empty() && Plan.traces.empty() && along.has_value()}; + auto const goes_on = [&](glm::dvec2 const &Heading) { return fromend && design.start_curved && cross(*along, Heading) * design.start_turn > 1e-9; }; + // a line from the end of the diverging track of a switch which doesn't go on with its curve begins with a straight right there + auto const switched = [](editor_track::snap_target const &Joint) { return Joint.track != nullptr && Joint.track->eType == tt_Switch; }; + auto const straight_from_end = [&]() { + if (fromend && switched(tool.start)) + design.start_curved = false; + }; + // one point of a curve right after a free end on a curve: the curve of the end goes on as far as abreast of the point, seen from + // its centre, the line ending on it there; the curve fitted to the points once more of them are clicked + if (i == 1 && last == 2 && false == reaching && fromend && design.start_curved && design.start_radius > 0.0) { auto const radius{design.start_radius}; auto const side{design.start_turn > 0 ? 1.0 : -1.0}; @@ -1237,6 +1310,7 @@ bool editor_mode::lay_line(std::vector const &Points, std::vector const &Points, std::vector const &Points, std::vector const &Points, std::vector chain{plan_of(entry)}; + chain.insert(chain.end(), traced.begin(), traced.end()); + leaving = heading_at_end(chain); + if (false == apart(leaving)) + return false; + leaving = glm::normalize(leaving); + } + auto base{design}; + base.start = entry; + base.start_direction = entering; + base.end = exit; + base.end_direction = leaving; + // the curve may begin before the point the straight was clicked at; it ends where the last point is + base.start_reserve = behind; + base.end_reserve = 0.0; + // the curve of the free end it starts at goes on into it where it turns its way, as the one of the free end it finishes at; + // otherwise it begins, or ends, from a straight there: right at a switch, out of the curve of a plain track by a transition + std::vector fitted_points{traced}; + auto const easing = [&](editor_track::snap_target const &Joint) { return switched(Joint) ? editor_track::easing{} : editor_track::ease_out_of(*Joint.track, Joint.position, 0.0, design.speed, design.norms); }; + base.start_curved = goes_on(leaving); + if (fromend && design.start_curved && false == base.start_curved) + { + design.start_curved = false; + if (auto const in{easing(tool.start)}; false == in.pieces.empty()) + { + base.start = in.end; + base.start_direction = in.direction; + easedstart = std::make_pair(in.end, in.direction); + std::erase_if(fitted_points, [&](glm::dvec2 const &Point) { return glm::distance(Point, plan_of(entry)) <= in.length; }); + } + } + base.end_curved = reaching && design.end_curved && cross(entering, leaving) * design.end_turn > 1e-9; + if (reaching && design.end_curved && false == base.end_curved) + { + design.end_curved = false; + if (auto const out{easing(End)}; false == out.pieces.empty()) + { + base.end = out.end; + base.end_direction = -out.direction; + easedend = std::make_pair(out.end, -out.direction); + std::erase_if(fitted_points, [&](glm::dvec2 const &Point) { return glm::distance(Point, plan_of(exit)) <= out.length; }); + } + } + { + auto const fit_to = [&](glm::dvec2 const &Leaving, bool const Meanwhile, bool &Exact) { + auto trial{base}; + trial.end_direction = Leaving; + return fitted_points.empty() ? std::optional{alignment::trace_stretch{}} : traced_stretch(trial, fitted_points, tool.radii, tool.straights_between, Meanwhile, &Exact); + }; + // a curve going on from the curve of the free end the line starts at: fitted as its curvature runs from that of the end to the + // last point, where it ends on the straight it comes out to; between the straights only where no such curve follows the points + bool exact{true}; + std::optional fitted; + std::optional going; + if (base.start_curved && false == reaching && false == headed && false == fitted_points.empty()) + if (auto const along{traced_stretch(base, fitted_points, tool.radii, tool.straights_between, true, &exact, true)}; along.has_value() && along->valid) + going = along; + // from a switch the curve goes on at its radius; where that doesn't keep to the points, m, and they show a straight right at the + // switch, as the switch goes on with one, the curve fitted from it beginning a few metres on at least, not a curve of another + // radius right there, and keeping to them much closer, that one + constexpr double kGoingClose{0.08}; + constexpr double kGoingWithin{0.3}; + constexpr double kStraightFirst{5.0}; + if (going.has_value() && base.start_held && (going->rms > kGoingClose || going->worst > kGoingWithin)) + { + auto straight{base}; + straight.start_curved = false; + auto const straight_first = [&](alignment::trace_stretch const &Fit) { + if (Fit.vertices.empty()) + return true; + auto trial{straight}; + trial.vertices = Fit.vertices; + trial.norms.checked = false; + auto const laid{alignment::compute(trial)}; + return laid.valid && false == laid.elements.empty() && laid.elements.front().kind == alignment::element_kind::straight && laid.elements.front().length >= kStraightFirst; + }; + if (auto const other{traced_stretch(straight, fitted_points, tool.radii, tool.straights_between, true)}; other.has_value() && other->valid && other->rms < 0.75 * going->rms && straight_first(*other)) + { + // the jump of the curvature there the switch has as it has one at its start: nothing to warn of + going.reset(); + base.start_curved = false; + design.start_curved = false; + design.start_radius = 0.0; + design.start_turn = 0; + } + } + // the same into the curve of the free end the line finishes at, from the first point clicked: fitted from that end back, the + // line starting from the first point on the straight the curve comes out of + std::optional into; + if (false == going.has_value() && reaching && base.end_curved && loose && false == fitted_points.empty()) + { + auto back{base}; + back.start = base.end; + back.start_direction = -leaving; + back.start_curved = true; + back.start_held = base.end_held; + back.start_radius = base.end_radius; + back.start_turn = -base.end_turn; + back.start_cant = base.end_cant; + back.end = entry; + back.end_direction = -entering; + back.end_curved = false; + back.end_held = false; + back.start_reserve = back.end_reserve = 0.0; + std::vector points{fitted_points.rbegin(), fitted_points.rend()}; + points.push_back(plan_of(entry)); + if (auto const fit{traced_stretch(back, points, tool.radii, tool.straights_between, true, &exact, true)}; fit.has_value() && fit->valid && false == fit->vertices.empty()) + into = turned_round(*fit); + } + if (going.has_value()) + { + fitted = going; + leaving = going->leaving; + base.end_direction = leaving; + // the arc of a switch going on ends abreast of the last point + exit = lifted(going->ending, exit.y); + } + else if (into.has_value()) + { + fitted = into; + entering = into->leaving; + base.start_direction = entering; + } + else + fitted = fit_to(leaving, true, exact); + // the curve ends at the last point: where the one fitted ends short of it, the straight after it going on to the point, the line + // leaves the point turning more, by bisection where it turns too much for a curve to reach the point. A few fits more, done on + // the thread from frame to frame while the cursor moves + if (false == going.has_value() && false == reaching && false == headed && exact && fitted.has_value() && fitted->valid && false == fitted->vertices.empty()) + { + // how far along the line laid its curves reach + auto const curves_reach = [](alignment::result const &Laid) { + double reach{0.0}; + for (auto const &element : Laid.elements) + if (element.kind != alignment::element_kind::straight) + reach = std::max(reach, element.chainage + element.length); + return reach; + }; + // the radius the last curve ends with + auto const end_radius = [](alignment::vertex const &Vertex) { return std::max(10.0, Vertex.arcs.empty() ? Vertex.radius : Vertex.arcs.back().radius); }; + auto const short_of = [&](alignment::trace_stretch const &Fit, glm::dvec2 const &Leaving) -> std::optional { + if (false == Fit.valid) + return std::nullopt; + auto trial{base}; + trial.end_direction = Leaving; + trial.vertices = Fit.vertices; + trial.norms.checked = false; + auto const laid{alignment::compute(trial)}; + if (false == laid.valid) + return std::nullopt; + return laid.length - curves_reach(laid); + }; + auto const turn{cross(entering, leaving) >= 0.0 ? 1.0 : -1.0}; + auto const turned = [&](double const Angle) { return geometry::turned(leaving, turn * Angle); }; + auto const ending{end_radius(fitted->vertices.back())}; + // the last two points clicked on the straight after the curve, as the mode asks for: the straight goes through them, the curve + // fitted with their heading ending before them; before the last one alone, where the last three points show a line much + // flatter than the end of the curve. Only points showing the curve going on draw it on to the last one + auto const clicked_straight = [&]() -> std::optional> { + constexpr double kFlatter{4.0}; + auto const count{fitted_points.size()}; + if (count < 4) + return std::nullopt; + auto const chord{fitted_points[count - 1] - fitted_points[count - 2]}; + if (glm::length(chord) < 1e-6) + return std::nullopt; + auto const fitted_heading = [&](glm::dvec2 const &Heading) -> std::optional { + bool again{true}; + auto const fit{fit_to(Heading, false, again)}; + if (false == fit.has_value() || false == fit->valid || fit->vertices.empty()) + return std::nullopt; + return fit; + }; + auto const chorded{glm::normalize(chord)}; + auto heading{chorded}; + auto fit{fitted_heading(heading)}; + if (false == fit.has_value()) + return std::nullopt; + // the heading of two points clicked a little off is a little off too: turned a bit either way, and to where the closeness + // to all the points says the bottom is, the heading the curve keeps closest to them with. A few fits more, done on the + // thread from frame to frame while the cursor moves + auto const rotated = [&](double const Angle) { return geometry::turned(chorded, Angle); }; + // none for points clicked as closely as this: their heading is good as it is + constexpr double kClose{0.05}; + auto const step{std::clamp(0.5 / glm::length(chord), 0.002, 0.03)}; + auto const minus{fit->rms > kClose ? fitted_heading(rotated(-step)) : std::nullopt}; + auto const plus{fit->rms > kClose ? fitted_heading(rotated(step)) : std::nullopt}; + if (minus.has_value() && plus.has_value()) + { + auto const centre{fit->rms}; + auto angle{0.0}; + if (minus->rms < fit->rms) + { + fit = minus; + angle = -step; + } + if (plus->rms < fit->rms) + { + fit = plus; + angle = step; + } + if (auto const bend{minus->rms - 2.0 * centre + plus->rms}; bend > 1e-12) + { + auto const bottom{std::clamp(0.5 * step * (minus->rms - plus->rms) / bend, -2.0 * step, 2.0 * step)}; + if (auto const there{fitted_heading(rotated(bottom))}; there.has_value() && there->rms < fit->rms) + { + fit = there; + angle = bottom; + } + } + heading = rotated(angle); + } + auto trial{base}; + trial.end_direction = heading; + trial.vertices = fit->vertices; + trial.norms.checked = false; + auto const laid{alignment::compute(trial)}; + if (false == laid.valid) + return std::nullopt; + auto const reach{curves_reach(laid)}; + auto const projections{alignment::project(laid, {fitted_points[count - 2], fitted_points[count - 1]})}; + if (projections.size() < 2 || projections[1].chainage < reach) + return std::nullopt; + if (projections[0].chainage < reach) + { + // the circle through the last three points + auto const before{fitted_points[count - 2] - fitted_points[count - 3]}; + auto const turn{std::abs(cross(before, chord))}; + auto const circle{turn > 1e-12 ? glm::length(before) * glm::length(chord) * glm::length(before + chord) / (2.0 * turn) : std::numeric_limits::max()}; + if (circle < kFlatter * end_radius(fit->vertices.back())) + return std::nullopt; + } + return std::make_pair(*fit, heading); + }; + auto const first{short_of(*fitted, leaving)}; + auto const straight{first.has_value() && *first > 0.5 ? clicked_straight() : std::nullopt}; + if (straight.has_value()) + { + fitted = straight->first; + leaving = straight->second; + base.end_direction = leaving; + } + else if (first.has_value() && *first > 0.5) + { + auto low{0.0}; + auto lowshort{*first}; + std::optional high; + auto best{fitted}; + auto bestangle{0.0}; + // by 15 degrees at most: more is no longer the heading the points show + constexpr double kMost{0.26}; + auto angle{std::min(kMost, lowshort / ending)}; + for (int i = 0; i < 8; ++i) + { + bool again{true}; + auto const trial{fit_to(turned(angle), false, again)}; + if (false == trial.has_value()) + break; + if (auto const left{short_of(*trial, turned(angle))}; left.has_value() && *left >= 0.0) + { + low = angle; + lowshort = *left; + best = trial; + bestangle = angle; + if (lowshort <= 0.5) + break; + } + else + high = angle; + if (false == high.has_value() && low >= kMost) + break; + angle = high.has_value() ? 0.5 * (low + *high) : std::min(kMost, low + lowshort / ending); + } + fitted = best; + leaving = turned(bestangle); + base.end_direction = leaving; + } + } if (false == fitted.has_value()) { Error = STR_C("Fitting the curve to the points..."); return false; } - // next to a free end on a curve the stretch goes on from that curve, or begins from a straight there, eased out of the curve by - // the transition lay_pieces lays; at the end of the diverging track of a switch too, the curve of the switch going on. The - // straight one where the curve next to the free end doesn't go on from it, or where that one doesn't keep to the points and - // this one keeps much closer - if (base.start_curved || base.end_curved) - { - auto plain{base}; - plain.start_curved = plain.end_curved = false; - std::vector kept; - auto const easing = [&](editor_track::snap_target const &Joint, double const Grade) { - return Joint.track != nullptr && (Joint.track->eType == tt_Normal || Joint.track->eType == tt_Switch) ? editor_track::ease_out_of(*Joint.track, Joint.position, Grade, design.speed, design.norms) : editor_track::easing{}; - }; - auto const in{base.start_curved ? easing(tool.start, 0.0) : editor_track::easing{}}; - auto const out{base.end_curved ? easing(End, 0.0) : editor_track::easing{}}; - if (false == in.pieces.empty()) - { - plain.start = in.end; - plain.start_direction = in.direction; - } - if (false == out.pieces.empty()) - { - plain.end = out.end; - plain.end_direction = -out.direction; - } - // the points along the transitions out of the curves left out, the line from the straights after them - for (auto const &point : traced) - if ((in.pieces.empty() || glm::distance(point, plan_of(base.start)) > in.length) && (out.pieces.empty() || glm::distance(point, plan_of(base.end)) > out.length)) - kept.push_back(point); - auto const other{kept.size() >= 2 ? traced_stretch(plain, kept, tool.radii, tool.straights_between) : std::optional{alignment::trace_stretch{}}}; - if (false == other.has_value()) - { - Error = STR_C("Fitting the curve to the points..."); - return false; - } - auto goes_on{false}; - if (fitted->valid) - { - auto laid{base}; - laid.vertices = fitted->vertices; - for (auto const &curve : alignment::compute(laid).curves) - goes_on = goes_on || curve.curvature_in > 0.0 || curve.curvature_out > 0.0; - } - // a curve going on which keeps to the points within 8 cm on average and 30 cm at most, as the fits take it for one keeping to - // them, stays: the straight one fits the points clicked a little off closer with the radii it has more - auto const loose{fitted->valid && (fitted->rms > 0.08 || fitted->worst > 0.3)}; - // at the end of the diverging track of a switch the curve of the switch goes on wherever it turns its way, however loosely the - // points clicked on the photo keep to it - auto const switched = [](editor_track::snap_target const &Joint, bool const Curved) { return Curved && Joint.track != nullptr && Joint.track->eType == tt_Switch; }; - auto const obliged{switched(tool.start, base.start_curved) || switched(End, base.end_curved)}; - if (other->valid && (false == fitted->valid || false == goes_on || (loose && false == obliged && other->rms < 0.75 * fitted->rms))) - { - fitted = other; - // the line laid from the end of the transition out of the curve, which lay_pieces lays before it - if (false == in.pieces.empty()) - easedstart = std::make_pair(in.end, in.direction); - if (false == out.pieces.empty()) - easedend = std::make_pair(out.end, -out.direction); - // from a straight there - if (base.start_curved) - design.start_curved = false; - if (base.end_curved) - design.end_curved = false; - } - } auto const &fit{*fitted}; if (false == fit.valid) { @@ -1500,15 +1771,18 @@ bool editor_mode::lay_line(std::vector const &Points, std::vector editor_mode::traced_stretch(alignment::design const &Base, std::vector const &Points, int const Radii, bool const Straights, bool const Meanwhile) const +std::optional editor_mode::traced_stretch(alignment::design const &Base, std::vector const &Points, int const Radii, bool const Straights, bool const Meanwhile, + bool *Exact, bool const Going) const { + if (Exact != nullptr) + *Exact = true; // the stretches traced before the one the cursor moves along are the same from frame to frame: their fits are kept // the curves of the adjoining tracks it goes on from or into, signed by their turn, and their cants auto const curvature = [](bool const Curved, double const Radius, int const Turn) { return Curved && Radius > 0.0 ? Turn / Radius : 0.0; }; std::vector key{Base.start.x, Base.start.z, Base.start_direction.x, Base.start_direction.y, Base.end.x, Base.end.z, Base.end_direction.x, Base.end_direction.y, Base.speed, static_cast(Base.shape), static_cast(Radii), Straights ? 1.0 : 0.0, Base.start_reserve, Base.end_reserve, curvature(Base.start_curved, Base.start_radius, Base.start_turn), Base.start_curved ? Base.start_cant : 0.0, curvature(Base.end_curved, Base.end_radius, Base.end_turn), - Base.end_curved ? Base.end_cant : 0.0}; + Base.end_curved ? Base.end_cant : 0.0, (Going ? 1.0 : 0.0) + (Base.start_held ? 2.0 : 0.0) + (Base.end_held ? 4.0 : 0.0)}; for (auto const &point : Points) { key.push_back(point.x); @@ -1529,7 +1803,7 @@ std::optional editor_mode::traced_stretch(alignment::d alignment::trace_stretch fit; if (false == m_follow_preview) { - fit = alignment::fit_trace_stretch(Base, Points, Radii, Straights); + fit = Going ? alignment::fit_going_on(Base, Points) : alignment::fit_trace_stretch(Base, Points, Radii, Straights); keep(fit); return fit; } @@ -1540,15 +1814,19 @@ std::optional editor_mode::traced_stretch(alignment::d keep(fit); return fit; } - m_stretch_fitter.ask({key, Base, Points, Radii, Straights}); + m_stretch_fitter.ask({key, Base, Points, Radii, Straights, Going}); std::vector last; - auto const header{std::size_t{18}}; + auto const header{std::size_t{19}}; if (Meanwhile && m_stretch_fitter.latest(last, fit) && last.size() >= header + 2 && key.size() >= header + 2 && std::equal(key.begin(), key.begin() + 4, last.begin()) && std::equal(key.begin() + 8, key.begin() + header, last.begin() + 8)) { auto const common{std::min(last.size(), key.size()) - 2}; if (std::equal(key.begin() + header, key.begin() + common, last.begin() + header)) + { + if (Exact != nullptr) + *Exact = false; return fit; + } } return {}; } @@ -1566,7 +1844,7 @@ void editor_mode::lay_switch_mode() m_track_layout = tool.mode == lay_mode::curve ? track_layout::arc_straight : track_layout::straight; if (tool.mode == lay_mode::curve) first_step_done(first_step::trace); - tool.status = tool.mode == lay_mode::curve ? STR("Curve: click points along the middle of the track, the last two on the straight after the curve. Tab: back to the straights") + tool.status = tool.mode == lay_mode::curve ? STR("Curve: click points along the middle of the track, the curve ends at the last one. Tab: back to the straights") : STR("Straights: each point clicked is a bend of the straights, eased with a large curve. Tab: a curve traced on the photo"); tool.status_time = ImGui::GetTime(); } @@ -1587,7 +1865,6 @@ void editor_mode::lay_click() } if (tool.points.empty()) { - tool.chain.clear(); tool.start = snap; 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}) @@ -1647,8 +1924,7 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) // with the points clicked, which the plan fits the curves to again when their transitions or cants change if (plan.shape.valid && false == created.empty() && false == scene::Layers.empty()) alignment_store::store(plan.design, plan.shape, 0.0, alignment_store::layer_of(*created.front()), "drawn", 0.0, tool.points); - if (tool.chain.empty()) - tool.history = m_history.size(); + tool.history = m_history.size(); push_track_snapshot({}, created); first_step_done(first_step::laid); if (plan.shape.valid && false == plan.shape.curves.empty()) @@ -1719,31 +1995,15 @@ void editor_mode::lay_enter() lay_finish(end); return; } - // with no point clicked since the last Enter the laying of the chain ends + // a single point is no track: Enter drops it if (tool.points.size() < 2) { lay_cancel(); return; } - lay_tool::step const previous{tool.start, tool.points.front()}; + // the laying ends with the track laid: the next click starts anew where it's made, a click on the free end left goes on from + // it. Going on by itself, a click made elsewhere afterwards led a loop out of that end to it lay_finish({}); - if (false == tool.points.empty()) - return; - // the laying goes on from the end of the track just laid, along it - auto *last{dynamic_cast(m_node)}; - if (last == nullptr || last->m_paths.empty()) - return; - auto const &path{last->m_paths.front()}; - editor_track::snap_target next; - next.track = last; - next.point = {0, editor_track::point_kind::end}; - next.position = path.points[segment_data::point::end]; - auto const &control{path.points[segment_data::point::control2]}; - next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position); - tool.chain.push_back(previous); - tool.start = next; - tool.points = {next.position}; - tool.modes = {tool.mode}; } void editor_mode::lay_cancel() @@ -1752,7 +2012,6 @@ void editor_mode::lay_cancel() tool.points.clear(); tool.modes.clear(); tool.start = {}; - tool.chain.clear(); tool.preview.clear(); tool.plan = {}; tool.status.clear(); @@ -1760,9 +2019,7 @@ void editor_mode::lay_cancel() std::size_t editor_mode::lay_pending() const { - // the first point of a chain going on is the end of the track laid before it, not clicked - auto const &tool{m_lay}; - return tool.chain.empty() ? tool.points.size() : tool.points.size() - std::min(1, tool.points.size()); + return m_lay.points.size(); } void editor_mode::lay_escape() @@ -1792,38 +2049,22 @@ void editor_mode::lay_after_history(bool const Undo) lay_cancel(); return; } - while (tool.start.track != nullptr && tool.start.track->m_editorremoved) - { - if (tool.chain.empty()) - { - lay_cancel(); - return; - } - tool.start = tool.chain.back().start; - tool.points = {tool.chain.back().point}; - tool.modes = {tool.mode}; - tool.chain.pop_back(); - } + // the free end the points go on from taken back + if (tool.start.track != nullptr && tool.start.track->m_editorremoved) + lay_cancel(); } void editor_mode::lay_back() { auto &tool{m_lay}; lay_trim(); - // the points not laid yet go first, then the tracks laid in the chain + // the points not laid yet, the last one first; the tracks laid go back with Ctrl+Z if (tool.points.size() > 1) { tool.points.pop_back(); tool.modes.pop_back(); return; } - if (false == tool.chain.empty()) - { - undo_last(); - tool.status = STR_C("The last piece was taken back"); - tool.status_time = ImGui::GetTime(); - return; - } tool.points.clear(); tool.modes.clear(); tool.start = {}; @@ -1948,7 +2189,7 @@ void editor_mode::render_lay_ui() if (int mode{tool.mode == lay_mode::curve ? 1 : 0}; segmented("laymode", mode, {{0, STR_C("Straights Tab"), "Each point clicked is a bend of the straights: the bend eased with two transition curves meeting, with no cant, of a large radius; where they don't fit, an arc with the transitions and the cant of the norms, as large as fits"}, - {1, STR_C("Curve Tab"), "Click points along the middle of a curve seen on the photo, the last two on the straight after it: the curve, compound or with a straight in it, is fitted to them. It comes from the straight before it, of the points clicked before or of the free end"}}) && + {1, STR_C("Curve Tab"), "Click points along the middle of a curve seen on the photo: the curve, compound or with a straight in it, is fitted to them and ends at the last one, heading as they do there. It comes from the straight before it, of the points clicked before or of the free end, and goes on with the curve of a free end, a switch too, where the points turn its way"}}) && (mode == 1) != (tool.mode == lay_mode::curve)) lay_switch_mode(); ImGui::PushItemWidth(120.0f); @@ -1958,8 +2199,11 @@ void editor_mode::render_lay_ui() double high{EditorSettings.corner_high()}; ImGui::TextDisabled("%s", STR_C("Radius of the curves at the bends")); auto changed{ImGui::DragScalar(STR_C("from (m)"), ImGuiDataType_Double, &low, 10.0f, nullptr, nullptr, "%.0f")}; + // the settings file written when a drag ends, not at each frame of it + auto release{ImGui::IsItemDeactivatedAfterEdit()}; item_tooltip("The radius the two transition curves of a bend meet at, at least; where they don't fit at it, an arc of the norms as large as fits"); changed |= ImGui::DragScalar(STR_C("to (m)"), ImGuiDataType_Double, &high, 10.0f, nullptr, nullptr, "%.0f"); + release |= ImGui::IsItemDeactivatedAfterEdit(); item_tooltip("The radius tried first; a bend too slight for transitions long enough at the speed takes a larger one"); ImGui::PushID("bends"); auto first{true}; @@ -1976,6 +2220,7 @@ void editor_mode::render_lay_ui() low = from; high = to; changed = true; + release = true; } if (chosen) ImGui::PopStyleColor(); @@ -1985,8 +2230,9 @@ void editor_mode::render_lay_ui() { low = std::max(10.0, low); EditorSettings.corners(low, std::max(low, high)); - EditorSettings.save(); } + if (release) + EditorSettings.save(); } else { @@ -2006,7 +2252,7 @@ void editor_mode::render_lay_ui() else { ImGui::PushTextWrapPos(0.0f); - ImGui::TextDisabled("%s", STR_C("Click points along the middle of the track on the photo, the last two on the straight after the curve")); + ImGui::TextDisabled("%s", STR_C("Click points along the middle of the track on the photo; the curve ends at the last one")); ImGui::PopTextWrapPos(); } ImGui::PushItemWidth(120.0f); diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 7aa8182b..9285c063 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -147,10 +147,10 @@ bool ground_card(char const *Id, char const *Title, char const *Text, bool const // m, the squares of the plan the points of the lines are kept in double constexpr kPlanCell{50.0}; -// a bridge or a tunnel keeps the model under it as it is +// a bridge or a tunnel keeps the model under it as it is, and a turntable its pit bool shapes_ground(TTrack const &Track) { - return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel; + return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel && Track.eType != tt_Table; } // the name the formation of a track is known by in the terrain, of a track with no name where it lies: two of the same name @@ -184,6 +184,23 @@ loose_end end_of(TTrack &Track, int const Path, bool const Atend) return {&Track, Path, Atend, point, control != glm::dvec3{} ? -control : point - other}; } +// a track runs up to a turntable with its end this near the rim, facing the centre this closely (the cosine), as high as the +// table give or take, the table dropped into its pit too; the model this far past the rim lies beside the pit, past its wall; +// the line keeps level this far off the rim +double constexpr kRimNear{0.3}; // m +double constexpr kRimFacing{0.9}; +double constexpr kRimLevel{3.0}; // m +double constexpr kPitWall{1.5}; // m +double constexpr kRunUp{10.0}; // m + +bool on_rim(loose_end const &End, glm::dvec3 const &Centre, double const Rim) +{ + auto const outward{geometry::plan_of(End.outward)}; + auto const inward{geometry::plan_of(Centre - End.point)}; + return std::abs(glm::length(inward) - Rim) <= kRimNear && std::abs(End.point.y - Centre.y) <= kRimLevel && glm::length(outward) > 1e-6 && + glm::dot(outward, inward) >= kRimFacing * glm::length(outward) * glm::length(inward); +} + // ends which meet in the plan facing each other, laid apart in height by hand, so the game doesn't join them; farther apart // than a track over another can be, they lie on two levels std::vector> loose_pairs() @@ -554,11 +571,19 @@ void editor_mode::nmt_make(std::vector const &Keys, bool c state.given_up.clear(); std::size_t queued{0}; for (auto const &key : Keys) - if (false == m_streamer.exists(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end()) + { + if (std::find(state.pending.begin(), state.pending.end(), key) != state.pending.end()) + continue; + // a chunk of flat ground there, as made with no model or no ground under it, gets the ground of the model as a new one would + if (m_streamer.exists(key)) { - state.pending.push_back(key); - ++queued; + if (false == nmt_flat_chunk(key)) + continue; + state.refill.insert(key); } + state.pending.push_back(key); + ++queued; + } state.made = 0; state.empty = 0; state.foreign = 0; @@ -568,8 +593,8 @@ void editor_mode::nmt_make(std::vector const &Keys, bool c state.grown = false; state.reshape = Reshape; } - // the terrain comes with its photos - if (queued > 0 && false == m_orthophoto.enabled()) + // the terrain comes with its photos, unless the user turned them off + if (queued > 0 && false == m_orthophoto.enabled() && false == m_orthophoto_chosen) m_orthophoto.enabled(true); state.status = queued > 0 ? format(STR_P("%zu chunk to make from the terrain model", "%zu chunks to make from the terrain model", queued), queued) : STR("There's terrain there already"); // with chunks to make the formations are laid once they're made @@ -769,14 +794,42 @@ editor_mode::nmt_chunk editor_mode::add_chunk_from_nmt(heightmap::chunk_key cons return nmt_chunk::empty; // where the model has no data the chunk goes on level with its nearest point fill_from_nearest(heights, found, side); - m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, [&](double const X, double const Z) { + auto const height = [&](double const X, double const Z) { auto const ix{static_cast(std::clamp(std::lround((X - low.x) / step), 0, cells))}; auto const iz{static_cast(std::clamp(std::lround((Z - low.y) / step), 0, cells))}; return static_cast(heights[iz * side + ix]); + }; + if (false == m_streamer.exists(Key)) + { + m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, height); + return nmt_chunk::made; + } + // a chunk of flat ground there already takes the heights of the model under the formations laid on it, which stay: the ground + // under them shaped, not a touch-up over them + auto const touchup{m_streamer.touch_up()}; + m_streamer.touch_up(false); + m_streamer.reshape(low, high, [&](double const X, double const Z, float &Height) { + Height = height(X, Z); + return true; }); + m_streamer.touch_up(touchup); return nmt_chunk::made; } +bool editor_mode::nmt_flat_chunk(heightmap::chunk_key const &Key) const +{ + auto const middle{heightmap::chunk_corner(Key) + glm::dvec2{0.5 * heightmap::chunk_size}}; + auto const *terrain{m_streamer.terrain_at(middle.x, middle.y)}; + if (terrain == nullptr || terrain->key() != Key) + return false; + auto const &data{terrain->data()}; + auto const &ground{data.base.empty() ? data.heights : data.base}; + if (ground.empty()) + return false; + auto const [lowest, highest]{std::minmax_element(ground.begin(), ground.end())}; + return *highest - *lowest < 0.01f; +} + std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) { auto &state{m_nmt_state}; @@ -931,16 +984,18 @@ void editor_mode::nmt_update() } int made{0}; std::size_t apart{0}; + std::vector changed; for (auto it{state.pending.begin()}; it != state.pending.end() && made < 4;) { - if (m_streamer.exists(*it)) + auto const refilled{state.refill.contains(*it)}; + if (false == refilled && m_streamer.exists(*it)) { it = state.pending.erase(it); continue; } // ahead of the track the terrain goes on from the one there is, at its height over the model std::optional offset; - if (state.grown) + if (state.grown && false == refilled) { bool foreign{false}; offset = nmt_seam_offset(*it, foreign); @@ -967,6 +1022,9 @@ void editor_mode::nmt_update() } if (offset.has_value() && chunk == nmt_chunk::made) state.ahead = offset; + if (chunk == nmt_chunk::made) + changed.push_back(*it); + state.refill.erase(*it); it = state.pending.erase(it); ++made; if (chunk == nmt_chunk::made) @@ -980,11 +1038,18 @@ void editor_mode::nmt_update() state.foreign += apart; state.given_up.insert(state.pending.begin(), state.pending.end()); state.pending.clear(); + state.refill.clear(); made = 1; } if (made > 0) { - forget_ground(); + // the ground gathered again only where the chunks were made: the rest of it is as it was, gathering all of it again frame after + // frame as the terrain grows around the cursor was felt + for (auto const &key : changed) + { + auto const low{heightmap::chunk_corner(key)}; + forget_ground_in(low, low + glm::dvec2{heightmap::chunk_size}); + } state.status = format(STR_C("Chunks made from the terrain model: %zu, waiting for it: %zu"), state.made, state.pending.size()); if (state.empty > 0) state.status += format(STR_P("; %zu chunk not made, the model has no data there", "; %zu chunks not made, the model has no data there", state.empty), state.empty); @@ -999,6 +1064,8 @@ void editor_mode::nmt_update() } // the photos laid onto the new ground m_orthophoto.refit(); + for (auto &layer : m_wms) + layer->refit(); first_step_done(first_step::nmt); } } @@ -1013,18 +1080,18 @@ void editor_mode::nmt_grow() if (false == state.grow || now - state.grow_time < 0.5 || false == m_nmt.placed()) return; state.grow_time = now; - // laying, around the cursor; else around the camera, in a scenery whose terrain is made from the model: the terrain is there - // wherever it's looked at - auto const laying{m_lay.active || m_extend.active}; - if (false == laying && false == m_streamer.active()) + // around the cursor of the tool laying the tracks, the terrain under them once they're laid: from the first point placed, or as a + // track is dragged from a free end; not around the camera, nor with the tool merely chosen + if (false == m_lay.active && false == m_extend.active) return; - auto const at{m_extend.active ? m_extend.mouse : m_lay.active ? m_lay.mouse : glm::dvec3{Camera.Pos}}; + auto const at{m_extend.active ? m_extend.mouse : m_lay.mouse}; // nothing asked for outside the download area if (false == in_download_area({at.x - 100.0, at.z - 100.0}, {at.x + 100.0, at.z + 100.0})) return; - // the model under it, for the heights of the points laid where there's no terrain yet, as over the base map + // the model under it meanwhile, for the height of the first point laid where there's no terrain yet, as over the base map m_nmt.request({at.x - 100.0, at.z - 100.0}, {at.x + 100.0, at.z + 100.0}); - if (m_streamer.resident() == 0) + auto const laying{m_extend.active || false == m_lay.points.empty()}; + if (false == laying || m_streamer.resident() == 0) return; glm::dvec2 const cursor{at.x, at.z}; double constexpr grow_reach{400.0}; // m around the cursor @@ -1054,7 +1121,7 @@ void editor_mode::nmt_grow() } for (auto const &chunk : missing) state.pending.push_back(chunk.second); - if (false == m_orthophoto.enabled()) + if (false == m_orthophoto.enabled() && false == m_orthophoto_chosen) m_orthophoto.enabled(true); WriteLog(format("Editor: NMT - %zu chunks around the cursor to make", missing.size()), logtype::generic); } @@ -1080,8 +1147,10 @@ void editor_mode::nmt_stop() state.laid.clear(); state.paths.clear(); state.loose.clear(); + state.turntables.clear(); state.stage = nmt_state::automatic::idle; state.pending.clear(); + state.refill.clear(); m_profile.open = false; state.status = format(STR_P("Stopped: %zu chunk made stays", "Stopped: %zu chunks made stay", state.made), state.made); if (lines) @@ -1253,10 +1322,11 @@ void editor_mode::render_nmt_all_tracks(bool const Terrain_panel) ImGui::TextDisabled("%s", Text); ImGui::PopTextWrapPos(); }}; - ImGui::BeginDisabled(busy || false == placed || all == 0); + // the tracks fitted already stay as they are: a track laid or changed since goes onto the model, all of them only when asked + ImGui::BeginDisabled(busy || false == placed || unfitted == 0); if (main_button(STR_C("Tracks onto the terrain"), ImVec2(-FLT_MIN, tall_button_height()))) - nmt_auto_start(); - item_tooltip("The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported"); + nmt_auto_start(true); + item_tooltip("The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported. A turntable stays level in its pit at the height of the tracks running up to it, which reach it level. The tracks fitted already, by it or with the profile, and not changed since stay as they are, and the new lines meet them"); ImGui::EndDisabled(); if (false == placed) { @@ -1264,17 +1334,21 @@ void editor_mode::render_nmt_all_tracks(bool const Terrain_panel) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("First the place of the scenery on the map: F5 Terrain, Terrain model (NMT), step 1")); ImGui::PopTextWrapPos(); } - told(STR_C("All the tracks go down or up onto the terrain model, which is loaded along them by itself")); + if (unfitted == all) + told(STR_C("All the tracks go down or up onto the terrain model, which is loaded along them by itself")); + else if (unfitted > 0) + told(format(STR_C("The tracks laid or changed since the last time go onto the terrain model (%zu of %zu); the ones fitted stay as they are"), unfitted, all).c_str()); + else + told(STR_C("All the tracks are fitted to the terrain; a track changed since goes onto it again")); toggle_chips("nmtshape", {{STR_C("Formation and slopes along the tracks"), &state.shape, "Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them"}}); ImGui::Spacing(); ImGui::TextDisabled("%s", STR_C("Or:")); - ImGui::BeginDisabled(busy || false == placed || unfitted == 0); - if (ImGui::Button(format(STR_C("Only the ones not fitted (%zu of %zu)"), unfitted, all).c_str(), ImVec2(-FLT_MIN, 0.0f))) - nmt_auto_start(true); - item_tooltip("The same for the tracks whose grade line isn't fitted yet, or was changed since: the ones fitted stay as they are and the new lines meet them; the scenery isn't moved"); + ImGui::BeginDisabled(busy || false == placed || all == 0); + if (ImGui::Button(STR_C("All anew, the fitted ones too"), ImVec2(-FLT_MIN, 0.0f))) + nmt_auto_start(false); + item_tooltip("All the tracks are fitted to the terrain model again, the ones fitted already too: their grade lines may change; the scenery isn't moved"); ImGui::EndDisabled(); - told(STR_C("Only the tracks laid or changed since the last time; the ones fitted stay as they are")); ImGui::BeginDisabled(busy || false == placed || all == 0); if (ImGui::Button(STR_C("Terrain under the tracks"), ImVec2(-FLT_MIN, 0.0f))) nmt_under_start(); @@ -1386,8 +1460,8 @@ void editor_mode::area_start() // the area there is taken up to be changed for (auto const &point : m_orthophoto.area()) drawing.points.push_back(m_orthophoto.to_world(point)); - // drawn on the map of the top view - if (false == m_orthophoto.enabled()) + // drawn over the orthophoto of the top view, unless the user turned it off + if (false == m_orthophoto.enabled() && false == m_orthophoto_chosen) m_orthophoto.enabled(true); if (false == Global.EditorOrtho) toggle_ortho(); @@ -1466,13 +1540,13 @@ void editor_mode::draw_download_area() const else for (auto const &point : m_orthophoto.area()) points.push_back(m_orthophoto.to_world(point)); - // shown while it's drawn, with the settings of the orthophoto open, and over the map of the top view - if (points.empty() || (false == drawing.active && false == m_orthophoto_window && false == Global.EditorOrthoMap)) + // shown while it's drawn and with the settings of the orthophoto open + if (points.empty() || (false == drawing.active && false == orthophoto_settings_open())) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList()}; // the tiles of the area as they are: in the disk cache, failed; the ones waiting left clear - if (false == drawing.active && m_orthophoto_window) + if (false == drawing.active && orthophoto_settings_open()) for (auto const &square : m_orthophoto.area_squares()) { if (square.state == 0) @@ -1737,7 +1811,13 @@ void editor_mode::render_nmt_ui() ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", STR_C("A chunk there is already is never made anew")); ImGui::PopTextWrapPos(); - toggle_chips("nmtgrow", {{STR_C("Make the terrain around the camera and ahead of the laying"), &state.grow, "Where the terrain of the scenery ends, the terrain model is made on around the camera, and around the cursor of a track laid past it; the chunks there are stay as they are"}}); + if (toggle_chips("nmtgrow", {{STR_C("Terrain of the model right away while laying"), &state.grow, + "Where the terrain of the scenery ends, the terrain model is made under the tracks as they're laid, around the cursor from the first point placed; " + "the chunks there are stay as they are. Off, it's made only when asked for"}}) >= 0) + { + EditorSettings.terrain_while_laying(state.grow); + EditorSettings.save(); + } state.band = std::clamp(state.band, 50.0, 3000.0); if (tracks && ImGui::TreeNode(STR_C("Height offset of the model"))) { @@ -1873,57 +1953,6 @@ void editor_mode::render_nmt_prompt() ImGui::EndPopup(); } -void editor_mode::nmt_lift_scenery(double const Offset) -{ - glm::dvec3 const shift{0.0, Offset, 0.0}; - // the tracks, joined again as they were - std::vector> states; - std::vector tracks; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) - continue; - states.emplace_back(track, editor_track::capture(*track)); - editor_track::translate(*track, shift); - tracks.push_back(track); - } - editor_track::commit(tracks); - auto const lifted{states.size()}; - push_track_snapshot(std::move(states)); - // the includes, by the place in their parameters - std::vector> moves; - for (auto const &standing : standing_templates([](std::string const &) { return true; })) - { - template_item item; - item.file = standing.file; - item.location = standing.location + shift; - item.yaw = standing.yaw; - moves.emplace_back(standing, item); - } - // the includes moved by the place in their parameters are made anew there: their models aren't moved on their own, nor are - // the models left behind by earlier changes, deleted a moment later: moved, they'd get into the scene again - std::set remade; - for (auto const &move : moves) - if (scene::Layers.tracked(move.first.instance) && scene::Layers.removable(move.first.instance)) - remade.insert(move.first.instance); - // the models, a group as a whole, ahead of the includes, which retire theirs - std::set groups; - for (auto *instance : simulation::Instances.sequence()) - { - if (instance == nullptr || remade.contains(instance->m_instance) || - std::any_of(m_retired.begin(), m_retired.end(), [&](std::pair const &Retired) { return Retired.first == instance; })) - continue; - if (instance->group() > 1 && false == groups.insert(instance->group()).second) - continue; - push_snapshot(instance, EditorSnapshot::Action::Move); - m_history.back().joined = true; - m_editor.translate(instance, instance->location() + shift, true); - } - if (move_templates(moves) > 0 && m_history.size() >= 2) - m_history.back().joined = true; - WriteLog(format("Editor: NMT - the scenery moved %+.2f m: %zu tracks, %zu includes, the models", Offset, lifted, moves.size()), logtype::generic); -} - std::size_t editor_mode::nmt_join_ends() { // the loose ends are one joint before the grade lines run over them; of two switches, which can't be changed alone, the @@ -1967,12 +1996,125 @@ std::size_t editor_mode::nmt_join_ends() return 0; editor_track::commit(changed); push_track_snapshot(std::move(states)); - if (m_history.size() >= 2 && m_nmt_state.lifted != 0.0) - m_history.back().joined = true; WriteLog(format("Editor: NMT - %zu joints of tracks laid apart in height joined", joined), logtype::generic); return joined; } +std::vector editor_mode::nmt_turntables() const +{ + std::vector result; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->eType != tt_Table || false == rail(*track)) + continue; + auto const &points{track->m_paths.front().points}; + nmt_state::turntable table; + table.table = track; + table.centre = glm::mix(points[segment_data::point::start], points[segment_data::point::end], 0.5); + table.rim = 0.5 * geometry::plan_distance(points[segment_data::point::start], points[segment_data::point::end]); + if (table.rim < 1.0) + continue; + result.push_back(std::move(table)); + } + if (result.empty()) + return result; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->eType == tt_Table || false == rail(*track)) + continue; + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + for (auto const atend : {false, true}) + { + auto const end{end_of(*track, path, atend)}; + for (auto &table : result) + if (on_rim(end, table.centre, table.rim)) + table.ends.emplace_back(track, path, atend); + } + } + return result; +} + +std::size_t editor_mode::nmt_level_turntables() +{ + auto &state{m_nmt_state}; + state.turntables = nmt_turntables(); + std::vector> states; + std::vector changed; + std::vector> moves; + std::vector> models; + std::vector standing; + if (false == state.turntables.empty()) + standing = standing_templates([](std::string const &) { return true; }); + for (auto &table : state.turntables) + { + // the height of the tracks running up to it: of the ones fitted already, which stay as they are, else of the model beside + // the rim past the wall of the pit, out along each of them + std::vector fitted, model; + for (auto const &[track, path, atend] : table.ends) + { + auto const end{end_of(*track, path, atend)}; + if (state.unfitted && grade_fitted(*track)) + fitted.push_back(end.point.y); + auto const away{geometry::plan_of(end.point - table.centre) / std::max(1e-6, geometry::plan_distance(end.point, table.centre))}; + for (auto out{kPitWall}; out <= kPitWall + 6.0; out += 1.0) + if (double height; m_nmt.height(end.point.x + away.x * out, end.point.z + away.y * out, height)) + model.push_back(height + state.offset + track->rail_height()); + } + auto &heights{fitted.empty() ? model : fitted}; + if (heights.empty()) + { + WriteLog(format("Editor: NMT - turntable %s left at %.2f m: %s", table.table->name().c_str(), table.centre.y, table.ends.empty() ? "no tracks run up to it" : "no terrain model beside it"), logtype::generic); + continue; + } + std::nth_element(heights.begin(), heights.begin() + heights.size() / 2, heights.end()); + auto const height{heights[heights.size() / 2]}; + auto const rise{height - table.centre.y}; + WriteLog(format("Editor: NMT - turntable %s level at %.2f m, from %.2f m (%+.2f), the height of its %zu tracks %s", table.table->name().c_str(), height, table.centre.y, rise, table.ends.size(), + fitted.empty() ? "on the model beside its rim" : "fitted already"), + logtype::generic); + auto &points{table.table->m_paths.front().points}; + table.centre.y = height; + if (std::abs(points[segment_data::point::start].y - height) < 0.001 && std::abs(points[segment_data::point::end].y - height) < 0.001) + continue; + states.emplace_back(table.table, editor_track::capture(*table.table)); + points[segment_data::point::start].y = height; + points[segment_data::point::end].y = height; + points[segment_data::point::control1].y = 0.0; + points[segment_data::point::control2].y = 0.0; + table.table->m_paths.front().pieces.clear(); + changed.push_back(table.table); + // its include, the pit and the bridge, by the name of the table in its parameters; a model of its name standing apart + std::set included; + for (auto const &item : standing) + if (false == item.values.empty() && ToLower(item.values.front()) == table.table->name() && geometry::plan_distance(item.location, table.centre) <= 1.0) + { + template_item moved; + moved.file = item.file; + moved.location = item.location + glm::dvec3{0.0, rise, 0.0}; + moved.yaw = item.yaw; + moves.emplace_back(item, moved); + included.insert(item.instance); + } + auto const apart{models.size()}; + if (auto *model{simulation::Instances.find(table.table->name())}; model != nullptr && false == included.contains(model->m_instance) && geometry::plan_distance(model->location(), table.centre) <= table.rim) + models.emplace_back(model, rise); + WriteLog(format("Editor: NMT - turntable %s moved %+.2f m with %zu include(s) and %zu model(s) of its name", table.table->name().c_str(), rise, included.size(), models.size() - apart), logtype::generic); + } + if (changed.empty()) + return 0; + editor_track::commit(changed); + push_track_snapshot(std::move(states)); + if (move_templates(moves) > 0 && m_history.size() >= 2) + m_history.back().joined = true; + for (auto const &[model, rise] : models) + { + push_snapshot(model, EditorSnapshot::Action::Move); + m_history.back().joined = true; + m_editor.translate(model, model->location() + glm::dvec3{0.0, rise, 0.0}, true); + } + return changed.size(); +} + void editor_mode::nmt_auto_start(bool const Unfitted) { auto &state{m_nmt_state}; @@ -1981,18 +2123,26 @@ void editor_mode::nmt_auto_start(bool const Unfitted) state.unfitted = Unfitted; nmt_locate(); state.probes.clear(); + state.probes_kept = 0; + // the tracks fitted already, when they stay as they are, tell the offset of the model they were fitted to, which isn't kept + // from one opening of the scenery to the next: a few hundred of their places are measured too, after the others + std::vector kept; for (auto const &place : nmt_track_places(20.0)) - if (place.shapes && (false == Unfitted || false == grade_fitted(*place.track))) - state.probes.push_back(place); + if (place.shapes) + (false == Unfitted || false == grade_fitted(*place.track) ? state.probes : kept).push_back(place); if (state.probes.empty()) { - state.status = STR("No tracks to put onto the terrain"); + state.status = Unfitted && false == kept.empty() ? STR("All the tracks are fitted to the terrain already") : STR("No tracks to put onto the terrain"); return; } + auto const every{std::max(1, kept.size() / 400)}; + for (std::size_t i = 0; i < kept.size(); i += every) + state.probes.push_back(kept[i]); + state.probes_kept = (kept.size() + every - 1) / every; state.lines = 0; state.refused = 0; state.departures = 0; - state.lifted = 0.0; + state.levelled = 0; state.stage = nmt_state::automatic::fetching; state.status = STR("Fetching the terrain model under the tracks..."); } @@ -2108,6 +2258,31 @@ bool editor_mode::nmt_fit_line(TTrack &Track, std::functioncentre.y}; + auto const level{std::min(kRunUp, 0.5 * (last_chainage - first_chainage))}; + auto const from{atend ? last_chainage - level : first_chainage}; + (atend ? profile.context.end_joined : profile.context.start_joined) = true; + (atend ? profile.context.end_elevation : profile.context.start_elevation) = height; + (atend ? profile.context.end_grade : profile.context.start_grade) = 0.0; + profile.context.switches.push_back({from, from + level, table->table->name()}); + for (auto at{from}; at <= from + level + 1e-6; at += 1.0) + options.bounds.push_back({at, height - kJoin, height + kJoin, false}); + WriteLog(format("Editor: NMT line %s runs up to turntable %s level at %.2f m over %.1f m", Track.name().c_str(), table->table->name().c_str(), height, level), logtype::generic); + } + auto const in_pit = [&](glm::dvec3 const &Point) { + return std::any_of(m_nmt_state.turntables.begin(), m_nmt_state.turntables.end(), [&](nmt_state::turntable const &Table) { return geometry::plan_distance(Point, Table.centre) < Table.rim + kPitWall; }); + }; auto const length_of = [](TTrack const &Track, int const Path) { return Track.eType == tt_Switch ? Track.SwitchExtension->Segments[Path]->GetLength() : Track.Segment->GetLength(); }; std::vector exits; exits.push_back({profile.route.spans.front().track, profile.route.spans.front().path, false == profile.route.spans.front().forward}); @@ -2222,7 +2397,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track, std::functionrail_height(); chainages.push_back(sample.chainage); ground.push_back(model[i]); @@ -2510,6 +2685,9 @@ bool editor_mode::nmt_fit_line(TTrack &Track, std::function, std::vector> &Points) { + // the bridge of a turntable turns: it crosses no line nor lies beside one + if (Track.eType == tt_Table) + return; for (auto const &path : Track.m_paths) { geometry::bezier const curve{path}; @@ -2567,8 +2745,7 @@ void editor_mode::nmt_merge_laid(std::unordered_set &Laid) auto const alike = [](TTrack const &A, TTrack const &B) { return editor_track::velocity(A) == editor_track::velocity(B) && A.eEnvironment == B.eEnvironment && A.iCategoryFlag == B.iCategoryFlag && A.iDamageFlag == B.iDamageFlag; }; - std::vector> states; - std::vector removed; + editor_track::edit edit; for (bool merged{true}; merged;) { merged = false; @@ -2607,18 +2784,17 @@ void editor_mode::nmt_merge_laid(std::unordered_set &Laid) continue; path.points[segment_data::point::end] = second.points[segment_data::point::end]; path.rolls[1] = second.rolls[1]; - if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) - states.emplace_back(track, editor_track::capture(*track)); + edit.capture(*track); editor_track::retire(*next); Laid.erase(next); - removed.push_back(next); + edit.removed.push_back(next); track->m_paths.front() = path; editor_track::commit({track}); merged = true; } } - if (false == removed.empty()) - push_track_snapshot(std::move(states), {}, std::move(removed)); + if (false == edit.removed.empty()) + push_track_snapshot(std::move(edit.states), {}, std::move(edit.removed)); } bool editor_mode::nmt_fit_laid(std::size_t const From, glm::dvec3 const End) @@ -2673,12 +2849,14 @@ bool editor_mode::nmt_fit_laid(std::size_t const From, glm::dvec3 const End) auto fitted_lines{std::exchange(state.laid, around)}; auto paths{std::exchange(state.paths, std::move(around))}; auto loose{std::exchange(state.loose, {})}; + auto turntables{std::exchange(state.turntables, nmt_turntables())}; auto const departures{state.departures}; auto const applied{nmt_fit_line(*last, [&](TTrack const *Other) { return false == laid.contains(Other); }, true)}; auto const error{m_profile.error}; state.laid = std::move(fitted_lines); state.paths = std::move(paths); state.loose = std::move(loose); + state.turntables = std::move(turntables); state.departures = departures; m_profile = profile; // taken back with the laying @@ -2714,12 +2892,14 @@ void editor_mode::nmt_auto_step() state.status = format(STR_P("Fetching the terrain model under the tracks: %zu square left", "Fetching the terrain model under the tracks: %zu squares left", waiting), waiting); if (false == all) return; - // the scenery goes to the height of the model as a whole first, so the lines fitted one after another meet at heights near - // their own; the grade lines then follow the model - std::vector differences; - for (auto const &place : state.probes) - if (double height; m_nmt.height(place.position.x, place.position.z, height)) - differences.push_back(height - place.crown); + // the scenery stays at its own heights, the terrain model meets it: offset by how far the tracks lie from it, so the lines + // fitted one after another meet at heights near their own; the grade lines then follow the model so offset + // the places of the tracks fitted already, the last ones, apart + std::vector differences, kept; + auto const first_kept{state.probes.size() - std::min(state.probes_kept, state.probes.size())}; + for (std::size_t i = 0; i < state.probes.size(); ++i) + if (double height; m_nmt.height(state.probes[i].position.x, state.probes[i].position.z, height)) + (i < first_kept ? differences : kept).push_back(height - state.probes[i].crown); if (differences.empty()) { state.stage = nmt_state::automatic::idle; @@ -2728,38 +2908,30 @@ void editor_mode::nmt_auto_step() } std::nth_element(differences.begin(), differences.begin() + differences.size() / 2, differences.end()); auto const lift{differences[differences.size() / 2]}; - // tracks of files that aren't saved, as of shared includes, would stay where they were: the model comes to the scenery - std::vector scenery; - for (auto *track : simulation::Paths.sequence()) - if (track != nullptr && false == track->m_editorremoved && false == track->m_paths.empty()) - scenery.push_back(track); - std::string reason; - state.lifted = 0.0; - if (state.unfitted) + if (state.unfitted && false == kept.empty()) { - // the tracks fitted already stay where they are, and so does the offset of the model they were fitted to - WriteLog(format("Editor: NMT - the tracks not fitted yet lie %+.2f m against the terrain model, its offset kept at %+.2f m", -lift - state.offset, state.offset), logtype::generic); + // the tracks fitted already stay where they are, and the model keeps the offset they were fitted to, told by them + std::nth_element(kept.begin(), kept.begin() + kept.size() / 2, kept.end()); + auto const offset{-kept[kept.size() / 2]}; + if (std::abs(offset - state.offset) > 0.05) + state.offset = offset; + WriteLog(format("Editor: NMT - the tracks fitted already lie on the terrain model offset %+.2f m, kept; the ones not fitted yet lie %+.2f m against it", state.offset, -lift - state.offset), logtype::generic); } else { - state.offset = 0.0; - if (std::abs(lift) > 0.05 && tracks_editable(scenery, reason)) - { - nmt_lift_scenery(lift); - state.lifted = lift; - } - else if (std::abs(lift) > 0.05) - { - state.offset = -lift; - WriteLog(format("Editor: NMT - the terrain model offset %+.2f m to the scenery, which can't be moved: ", -lift) + reason, logtype::generic); - } + // the offset of the run before kept where the tracks fitted to it lie within 5 cm of it: they don't creep a centimetre a run + auto const offset{std::abs(lift) > 0.05 ? -lift : 0.0}; + if (std::abs(offset - state.offset) > 0.05) + state.offset = offset; + WriteLog(format("Editor: NMT - the terrain model offset %+.2f m to the scenery", state.offset), logtype::generic); } state.fitted = true; nmt_join_ends(); + state.levelled = nmt_level_turntables(); // the longest tracks first: the main lines lead, the sidings join them state.left.clear(); for (auto *track : simulation::Paths.sequence()) - if (track != nullptr && rail(*track) && (false == state.unfitted || false == grade_fitted(*track))) + if (track != nullptr && rail(*track) && track->eType != tt_Table && (false == state.unfitted || false == grade_fitted(*track))) state.left.push_back(track); std::sort(state.left.begin(), state.left.end(), [](TTrack *Left, TTrack *Right) { return Left->Length() > Right->Length(); }); state.tracks = state.left.size(); @@ -2837,9 +3009,11 @@ void editor_mode::nmt_auto_step() } state.left.erase(std::remove_if(state.left.begin(), state.left.end(), [&](TTrack *Track) { return done.contains(Track); }), state.left.end()); } - state.status = format(STR_C("Scenery moved %+.2f m; grade lines fitted to the terrain: %zu, refused by the checks: %zu, tracks left: %zu"), state.lifted, state.lines, state.refused, state.left.size()); + state.status = format(STR_C("Terrain model offset %+.2f m to the scenery; grade lines fitted to the terrain: %zu, refused by the checks: %zu, tracks left: %zu"), state.offset, state.lines, state.refused, state.left.size()); if (state.departures > 0) state.status += format(STR_P("\n%zu line parts from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names it)", "\n%zu lines part from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names them)", state.departures), state.departures); + if (state.levelled > 0) + state.status += format(STR_P("\n%zu turntable set level at the height of the tracks running up to it, which reach it level", "\n%zu turntables set level at the height of the tracks running up to them, which reach them level", state.levelled), state.levelled); if (false == state.left.empty()) return; m_profile.open = false; @@ -2847,6 +3021,7 @@ void editor_mode::nmt_auto_step() state.laid.clear(); state.paths.clear(); state.loose.clear(); + state.turntables.clear(); state.stage = nmt_state::automatic::idle; WriteLog("Editor: NMT - " + state.status, logtype::generic); nmt_make(nmt_keys_along_tracks(), true); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 1f1ee201..cc11fa0b 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -841,8 +841,6 @@ void editor_mode::profile_take(editor_track::route Route, TTrack *From, TTrack * } } profile_fit_view(); - // the camera stays where the route was picked, it follows the plot once that's moved - state.view_seen = state.view_framed = {state.view_from, state.view_to}; } void editor_mode::profile_resample() @@ -1466,21 +1464,32 @@ void editor_mode::profile_fit_view() state.exaggeration = 0.0f; } -void editor_mode::profile_follow_view() +void editor_mode::profile_to_camera() { auto &state{m_profile}; - std::pair const shown{state.view_from, state.view_to}; - auto const now{ImGui::GetTime()}; - if (shown != state.view_seen) + if (state.samples.empty()) + return; + // where the view looks: from above the camera itself, else the middle of the view on the level of the route + glm::dvec3 target{Camera.Pos}; + if (false == Global.EditorOrtho) { - state.view_seen = shown; - state.view_seen_time = now; - return; + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + glm::dvec3 point; + if (screen_projection{}.on_level(ImVec2{(low.x + high.x) * 0.5f, (low.y + high.y) * 0.5f}, state.samples[state.samples.size() / 2].position.y, point)) + target = point; } - if (false == state.camera_follows || shown == state.view_framed || now - state.view_seen_time < 0.25) - return; - state.view_framed = shown; - profile_frame_view(); + auto best{std::numeric_limits::max()}; + double chainage{0.0}; + for (auto const &sample : state.samples) + if (auto const distance{geometry::plan_distance(sample.position, target)}; distance < best) + { + best = distance; + chainage = sample.chainage; + } + auto const span{state.view_to - state.view_from}; + state.view_from = chainage - 0.5 * span; + state.view_to = chainage + 0.5 * span; } void editor_mode::profile_frame_view() @@ -2376,10 +2385,16 @@ void editor_mode::render_profile_toolbar() profile_fit_view(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Shows the whole route")); - next(14.0f, pill(STR_C("Camera follows"))); - if (pill_toggle(STR_C("Camera follows"), kGradeLine, state.camera_follows) && state.camera_follows) - state.view_framed = {}; - item_tooltip("The view shows the part of the route the plot shows, once the plot is zoomed or moved"); + // the camera and the plot brought to one another only when asked: the camera doesn't turn as the plot is zoomed + auto const button = [](char const *Label) { return ImGui::CalcTextSize(Label).x + ImGui::GetStyle().FramePadding.x * 2.0f; }; + next(14.0f, button(STR_C("Camera to the plot"))); + if (ImGui::Button(STR_C("Camera to the plot"))) + profile_frame_view(); + item_tooltip("Moves the camera to the part of the route the plot shows"); + next(ImGui::GetStyle().ItemSpacing.x, button(STR_C("Plot to the camera"))); + if (ImGui::Button(STR_C("Plot to the camera"))) + profile_to_camera(); + item_tooltip("Moves the plot to the part of the route where the camera is, its zoom kept"); } void editor_mode::render_profile_canvas(float const Height) @@ -2398,7 +2413,6 @@ void editor_mode::render_profile_canvas(float const Height) if (state.model_waiting && ImGui::GetTime() - state.model_polled > 0.5) profile_sample_model(); profile_navigate(profile_view{state, corner, size}, hovered); - profile_follow_view(); profile_view const view{state, corner, size}; if (profile_canvas_edit(view, hovered)) profile_edited(); diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp index ee480e92..3e855066 100644 --- a/application/editormoderecords.cpp +++ b/application/editormoderecords.cpp @@ -371,21 +371,16 @@ std::optional editor_mode::slip_of(TTrack const &Track) c if (slip.switches.size() < 2) return std::nullopt; // where the straights of its first and its last switch meet: the joints of its tracks near it are inside the slip, its ends far off - auto const heading = [](segment_data const &Path) { - auto const &control{Path.points[segment_data::point::control1]}; - auto const direction{plan_of(control != glm::dvec3{} ? control : Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; - return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; - }; auto const &first{slip.switches.front()->m_paths.front()}; auto const &last{slip.switches.back()->m_paths.front()}; auto const a{plan_of(first.points[segment_data::point::start])}; auto const d{plan_of(last.points[segment_data::point::start])}; - auto const da{heading(first)}; - auto const dd{heading(last)}; - auto const determinant{geometry::cross(da, dd)}; - if (std::abs(determinant) < 1e-9) + auto const da{editor_track::end_direction(first, editor_track::point_kind::start)}; + auto const dd{editor_track::end_direction(last, editor_track::point_kind::start)}; + double alongfirst, alonglast; + if (false == geometry::lines_cross(a, da, d, dd, alongfirst, alonglast)) return std::nullopt; - auto const crossing{a + da * (geometry::cross(d - a, dd) / determinant)}; + auto const crossing{a + da * alongfirst}; auto const inside{0.5 * glm::distance(a, crossing)}; slip.parts = slip.switches; for (std::size_t i = 0; i < slip.parts.size(); ++i) @@ -442,7 +437,6 @@ std::optional editor_mode::slip_of(TTrack const &Track) c slip.ends[i] = ends[order[i]].first; slip.into[i] = ends[order[i]].second; } - slip.crossing = (plan_of(slip.ends[0]) + plan_of(slip.ends[1]) + plan_of(slip.ends[2]) + plan_of(slip.ends[3])) * 0.25; return slip; } diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index bd3fd6a4..651f06dc 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -161,6 +161,7 @@ bool editor_mode::route_from_curve(TTrack &Track) if (curve.reversals == 0) { design.vertices = {vertex}; + route_curved_ends(); } else { @@ -169,12 +170,70 @@ bool editor_mode::route_from_curve(TTrack &Track) vertex.compound = false; vertex.reverse_turn = false; design.vertices = {vertex, vertex}; + route_update(); } - route_update(); m_route.status = format(STR_P("Curve of %zu path, turning %d deg", "Curve of %zu paths, turning %d deg", m_route.chain.tracks.size()), m_route.chain.tracks.size(), static_cast(std::round(glm::degrees(curve.turn)))); return true; } +void editor_mode::route_curved_ends() +{ + auto &design{m_route.design}; + design.start_curved = false; + design.end_curved = false; + if (design.vertices.empty() || m_route.chain.tracks.empty()) + { + route_update(); + return; + } + // a curve beginning right at an end of the fragment at the curvature of the track adjoining there, turning its way, goes on from it, + // as a curve laid on from the diverging track of a switch does: laid so again, not from a straight there; so next to a switch laid + // into the curve too. A track of R 10 km or more is a straight there, as the lay tool takes it. The way the first curve and the last + // one turn by the legs of the polygon + auto const count{design.vertices.size()}; + auto const turn_of = [&](bool const Atend) { + if (count == 1) + return cross(design.start_direction, design.end_direction) > 0.0 ? 1 : -1; + glm::dvec2 const in{Atend ? design.vertices[count - 1].position - design.vertices[count - 2].position : design.start_direction}; + glm::dvec2 const out{Atend ? design.end_direction : design.vertices[1].position - design.vertices[0].position}; + return cross(in, out) > 0.0 ? 1 : -1; + }; + auto const goes_on = [&](bool const Atend) { + auto const radius{Atend ? design.end_radius : design.start_radius}; + if (radius <= 0.0 || radius >= 10000.0 || (Atend ? design.end_turn : design.start_turn) != turn_of(Atend)) + return false; + // within a tenth: the path of a switch bent into the curve approximates it at its ends, R 860 m at the start of one in R 800 m + auto const *track{Atend ? m_route.chain.tracks.back() : m_route.chain.tracks.front()}; + auto const bend{editor_track::bend_at(*track, Atend ? m_route.chain.end : m_route.chain.start)}; + return bend.has_value() && std::abs(std::abs(bend->curvature) - 1.0 / radius) <= 0.1 / radius; + }; + design.start_curved = goes_on(false); + design.end_curved = (count > 1 || false == design.start_curved) && goes_on(true); + route_update(); + // a curve read from its paths as going on from the curve adjoining it which the design can't lay along them so, as one whose arc + // going on is too short to be read as an arc: read from a straight there, whichever of the two keeps closer to its paths + if (design.start_curved || design.end_curved) + { + auto const departure = [&]() { + TTrack *worst{nullptr}; + return m_route.result.valid ? alignment_store::deviation(m_route.result, m_route.chain.tracks, worst) : std::numeric_limits::max(); + }; + auto const curved{departure()}; + if (curved > 0.25) + { + auto const kept{design}; + design.start_curved = false; + design.end_curved = false; + route_update(); + if (departure() >= curved) + { + design = kept; + route_update(); + } + } + } +} + alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const { if (Curve.designed.has_value()) @@ -591,7 +650,7 @@ bool editor_mode::route_from_line(TTrack &Track) design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); } - route_update(); + route_curved_ends(); } route.status = format(STR_P("Line of %zu path, %.0f m, %zu vertices", "Line of %zu paths, %.0f m, %zu vertices", route.chain.tracks.size()), route.chain.tracks.size(), route.chain.length, design.vertices.size()); return true; @@ -1099,18 +1158,7 @@ std::vector editor_mode::route_focused() const own.push_back(i); if (own.empty() || focus.what == part::whole) return own; - // the curvature at the start and at the end of an element, the arcs of a compound curve found by their radii - auto const curvatures = [](alignment::element const &Element) -> std::pair { - auto const own{Element.radius > 0.0 ? 1.0 / Element.radius : 0.0}; - switch (Element.kind) - { - case alignment::element_kind::transition_in: return {0.0, own}; - case alignment::element_kind::arc: return {own, own}; - case alignment::element_kind::transition_out: return {own, 0.0}; - case alignment::element_kind::spiral: return {Element.curvature_start, Element.curvature_end}; - default: return {0.0, 0.0}; - } - }; + // the arcs of a compound curve found by the curvature at the start and at the end of each element and their radii auto const same = [](double const A, double const B) { return std::abs(A - B) <= 1e-6 * std::max(std::abs(A), std::abs(B)) + 1e-12; }; std::vector radii; for (auto const &curve : result.curves) @@ -1119,7 +1167,7 @@ std::vector editor_mode::route_focused() const std::vector found; for (auto const i : own) { - auto const [start, end] {curvatures(result.elements[i])}; + auto const [start, end] {alignment::end_curvatures(result.elements[i])}; auto const arc{static_cast(focus.arc)}; switch (focus.what) { @@ -1396,13 +1444,9 @@ void editor_mode::draw_route_overlay() const void editor_mode::render_route_trace() { auto &trace{m_route.trace}; + // the curve is traced on the photo with the laying (Tab), the plan has no button of its own for it if (false == trace.active) - { - if (ImGui::Button(STR_C("Trace the curve on the photo"))) - route_trace_start(); - item_tooltip("Click points along the middle of the track on the orthophoto: after each click the curve takes the radius, or the radii of a compound curve and their shares, which follow the points best. The straights and the transition curves stay"); return; - } ImGui::SeparatorText(STR_C("Tracing the curve")); if (segmented("traceradii", trace.radii, {{0, STR_C("Auto"), "As few radii as follow the points, up to three"}, {1, STR_C("1 R"), "One radius"}, {2, STR_C("2 R"), "A compound curve of two radii"}, {3, STR_C("3 R"), "A compound curve of three radii"}})) @@ -1667,15 +1711,8 @@ bool editor_mode::render_route_band() auto const ends = [](alignment::element const &Element) -> std::pair { auto const sign{static_cast(Element.turn)}; - auto const k{Element.radius > 0.0 ? sign / Element.radius : 0.0}; - switch (Element.kind) - { - case alignment::element_kind::transition_in: return {0.0, k}; - case alignment::element_kind::arc: return {k, k}; - case alignment::element_kind::transition_out: return {k, 0.0}; - case alignment::element_kind::spiral: return {sign * Element.curvature_start, sign * Element.curvature_end}; - default: return {0.0, 0.0}; - } + auto const [start, end]{alignment::end_curvatures(Element)}; + return {sign * start, sign * end}; }; auto const colour = [](alignment::element const &Element) { return Element.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : Element.kind == alignment::element_kind::arc ? overlay_color::grip : IM_COL32(255, 140, 30, 255); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 431f25a7..e0092dd4 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -126,6 +126,10 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given // orthoarea ... | clear the download area through the points of the world, as drawn on the map; orthoareapause 0|1 // orthohires 0|1 the 4K tiles on or off; orthostats logs the tiles, the area and the requests made +// orthosettings [on|radius|opacity|lift|height|year|hires|drape|scene ]... the settings of the orthophoto changed as its +// panel and its checkbox change them, kept with the editor, and logged with the origin of the scenery +// nmtsettings [band|around|shape|follow|grow|offset|fitted ]... the settings of the terrain model changed as the terrain +// panel changes them, and logged with the offset of the model to the scenery // join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // layto from the free end at the first point towards the second, as a drag in the layout // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined @@ -174,6 +178,8 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // fittime ... the grade line of a straight over rolling ground of each length fitted, as at a click, and the time the // frame waits for it while the track is laid, the line fitted on the thread of the fitter // nmtauto [unfitted] the tracks put onto the terrain model, all or the ones whose grade line isn't fitted yet +// turntables the height of each turntable and of the model in its pit, and the heights of the tracks running up +// to it out along their lines from the rim, against the model there // fittedstate [reload] logs how many tracks have their grade line fitted, and which; reload: read again from the //$f lines // trackterrain ... the terrain drawn to the tracks, or their formations taken out of it, as the panel of the tracks // terrainhash a hash of the heights of the terrain chunks in memory, to compare the ground two builds made diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index c19e041d..7d8e205e 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -288,7 +288,8 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma for (std::size_t k = 0; k < plan.design.vertices.size(); ++k) { auto const &vertex{plan.design.vertices[k]}; - auto text{format("SELFTEST layplan vertex %zu: %s at %.3f %.3f", k, plan.corners[k] != 0 ? "bend" : "traced", k < plan.shape.vertices.size() ? plan.shape.vertices[k].x : 0.0, k < plan.shape.vertices.size() ? plan.shape.vertices[k].y : 0.0)}; + auto text{format("SELFTEST layplan vertex %zu: %s at %.3f %.3f, designed R %.1f L %.1f/%.1f", k, plan.corners[k] != 0 ? "bend" : "traced", k < plan.shape.vertices.size() ? plan.shape.vertices[k].x : 0.0, + k < plan.shape.vertices.size() ? plan.shape.vertices[k].y : 0.0, vertex.radius, vertex.transition_in, vertex.transition_out)}; for (auto const &curve : plan.shape.curves) if (curve.vertex == static_cast(k)) { @@ -476,7 +477,7 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma lay_back(); else lay_cancel(); - WriteLog(format("SELFTEST %s: %zu points, chain %zu, history %zu, %s", command.c_str(), m_lay.points.size(), m_lay.chain.size(), m_history.size(), m_lay.status.c_str())); + WriteLog(format("SELFTEST %s: %zu points, history %zu, %s", command.c_str(), m_lay.points.size(), m_history.size(), m_lay.status.c_str())); return selftest_result::handled; } // a line of known curves, turning by the deflections given (degrees, to the left positive), the points along its curves clicked as @@ -572,7 +573,7 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma lay_cancel(); auto const report = [&](std::string const &Step) { auto const *start{m_lay.start.track}; - WriteLog(format("SELFTEST laychain %s: chain %zu, points %zu, start %s%s at %.2f %.2f, history %zu, status %s", Step.c_str(), m_lay.chain.size(), m_lay.points.size(), start != nullptr ? start->name().c_str() : "-", start != nullptr && start->m_editorremoved ? " (removed)" : "", + WriteLog(format("SELFTEST laychain %s: points %zu, start %s%s at %.2f %.2f, history %zu, status %s", Step.c_str(), m_lay.points.size(), start != nullptr ? start->name().c_str() : "-", start != nullptr && start->m_editorremoved ? " (removed)" : "", m_lay.points.empty() ? 0.0 : m_lay.points.front().x, m_lay.points.empty() ? 0.0 : m_lay.points.front().z, m_history.size(), m_lay.status.c_str())); }; for (std::string word; words >> word;) diff --git a/application/editormodeselftestswitch.cpp b/application/editormodeselftestswitch.cpp index 5ba0f2b5..189b9c2e 100644 --- a/application/editormodeselftestswitch.cpp +++ b/application/editormodeselftestswitch.cpp @@ -391,6 +391,26 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com WriteLog(text); return selftest_result::handled; } + // the key H as pressed over the view: the selected switch thrown over + if (command == "keyh") + { + auto const handled{track_shortcut(GLFW_KEY_H)}; + auto const *track{selected_track()}; + auto const switched{track != nullptr && track->eType == tt_Switch && track->SwitchExtension != nullptr}; + WriteLog(format("SELFTEST keyh: %s, %s set to %d, blades at %.3f of %.3f", handled ? "thrown" : "not taken", track != nullptr ? track->name().c_str() : "-", switched ? track->SwitchExtension->CurrentIndex : -1, switched ? track->SwitchExtension->fOffset : 0.f, + switched ? track->SwitchExtension->fDesiredOffset : 0.f)); + return selftest_result::handled; + } + // where the blades of the switch are on their way over + if (command == "blades") + { + std::string name; + words >> name; + auto const *track{simulation::Paths.find(name)}; + if (track != nullptr && track->eType == tt_Switch && track->SwitchExtension != nullptr) + WriteLog(format("SELFTEST blades %s: %.3f of %.3f", name.c_str(), track->SwitchExtension->fOffset, track->SwitchExtension->fDesiredOffset)); + return selftest_result::handled; + } if (command == "throw") { std::string name; @@ -498,13 +518,21 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com if (command == "switchfree") { // a switch with no track: pressed at the point, the direction of its main track toward the second one (none at a free end), let - // go at the third one, whose side it takes + // go at the third one, whose side it takes; past its end from a free end it's bent, one: one-sided (Tab), shift: at a free end the + // diverging track leads into it std::string label; glm::dvec3 press{0.0}; glm::dvec3 heading{0.0}; glm::dvec3 release{0.0}; words >> label >> press.x >> press.z >> heading.x >> heading.z >> release.x >> release.z; std::replace(label.begin(), label.end(), '_', ' '); + auto oneside{false}; + auto shift{false}; + for (std::string flag; words >> flag;) + { + oneside |= flag == "one"; + shift |= flag == "shift"; + } auto &tool{m_switch}; show_track_tab(track_tab::turnout); tool.templates = editor_track::standard_switch_templates(); @@ -517,7 +545,10 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com tool.armed = i; break; } + // the free end taken where it was pressed, not by the mouse on the screen + m_cursor_override = press; start_free_switch(press); + m_cursor_override.reset(); if (false == tool.placing) { WriteLog("SELFTEST switchfree: not started, no template " + label); @@ -526,15 +557,27 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com if (tool.free_end.track == nullptr) tool.free_heading = glm::normalize(geometry::plan_of(heading - tool.point)); tool.mouse = glm::dvec3{release.x, tool.point.y, release.z}; + tool.one_sided = oneside; + // the free end the cursor is at found where it was let go, not by the mouse on the screen; Shift held as it's let go + m_cursor_override = tool.mouse; + auto &io{ImGui::GetIO()}; + auto const shifted{io.KeyShift}; + io.KeyShift = shift; tool.placing = false; finish_switch_placement(); + io.KeyShift = shifted; + m_cursor_override.reset(); WriteLog("SELFTEST switchfree" + (tool.free_end.track != nullptr ? " from " + tool.free_end.track->name() : std::string{}) + ": " + tool.status); if (auto const *track{selected_track()}; track != nullptr && track->eType == tt_Switch) for (std::size_t i = 0; i < track->m_paths.size(); ++i) { auto const &a{track->m_paths[i].points[segment_data::point::start]}; auto const &b{track->m_paths[i].points[segment_data::point::end]}; - WriteLog(format("SELFTEST switchfree path %s/%zu: %.3f %.3f %.3f -> %.3f %.3f %.3f", track->name().c_str(), i, a.x, a.y, a.z, b.x, b.y, b.z)); + // the radius the path turns with from end to end, signed the way it turns + geometry::bezier const curve{track->m_paths[i]}; + auto const turn{geometry::signed_angle(geometry::plan_of(curve.first(0.0)), geometry::plan_of(curve.first(1.0)))}; + WriteLog(format("SELFTEST switchfree path %s/%zu: %.3f %.3f %.3f -> %.3f %.3f %.3f, R %.1f, turning R %.1f", track->name().c_str(), i, a.x, a.y, a.z, b.x, b.y, b.z, static_cast(track->m_paths[i].radius), + std::abs(turn) > 1e-6 ? curve.plan_length() / turn : 0.0)); } return selftest_result::handled; } @@ -545,6 +588,9 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com int direction{1}; int side{1}; words >> label >> point.x >> point.z >> direction >> side; + // snap: the start of a switch into a curve drawn to the ends of the cant ramps and to the joints, as a drag lets it go + std::string snap; + words >> snap; std::replace(label.begin(), label.end(), '_', ' '); auto &tool{m_switch}; tool.templates = editor_track::standard_switch_templates(); @@ -590,6 +636,21 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com tool.curved = true; tool.along = nearest_station(tool.frame, point); tool.pressed = tool.along; + if (snap == "snap") + { + std::string what; + tool.along = curved_switch_start(direction, &what); + WriteLog(format("SELFTEST switchcurve start %.3f, pressed %.3f%s", tool.along, tool.pressed, what.empty() ? "" : (": " + what).c_str())); + // the tracks of the curve and their ends along it, where the frame has them and where they are + for (auto const *track : tool.frame_tracks) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const &at{track->m_paths.front().points[index]}; + auto const station{nearest_station(tool.frame, at)}; + auto const framed{sample_at(tool.frame, station).position}; + WriteLog(format("SELFTEST switchcurve frame %s.%s: station %.3f, at %.3f %.3f, frame %.3f %.3f", track->name().c_str(), index == segment_data::point::start ? "start" : "end", station, at.x, at.z, framed.x, framed.z)); + } + } // a slip bent into the curve with its crossing track, its paths and its record logged if (tool.templates[tool.armed].double_slip) { @@ -738,8 +799,17 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com } if (command == "switchslide") { + // back, ahead: one bent into a curve as far as the far end of the track next to it double distance{0.0}; - words >> distance; + std::string word; + words >> word; + std::vector frame; + std::array sides; + double station{0.0}; + if ((word == "back" || word == "ahead") && bent_switch_frame(*track, frame, sides, station)) + distance = word == "back" ? -station : frame.back().station - station - (nearest_station(frame, track->m_paths.front().points[segment_data::point::end]) - station); + else + distance = std::atof(word.c_str()); double low{0.0}; double high{0.0}; std::string reason; @@ -748,6 +818,20 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com auto const room{set.has_value() ? double_room(*set, low, high, length, reason) : switch_room(*track, low, high, reason)}; auto const done{slide_switch(*track, distance)}; WriteLog(format("SELFTEST switchslide %s %.3f: room %d %.3f .. %.3f, done %d, %s", name.c_str(), distance, room ? 1 : 0, low, high, done ? 1 : 0, m_switch.status.c_str())); + // how the switch meets the tracks at its ends: the turn between them, the gap, the radius of the one next to it + for (auto const &joint : editor_track::joints(*track)) + { + geometry::bezier const mine{track->m_paths[joint.own.path]}; + auto const atend{joint.own.kind == editor_track::point_kind::end}; + auto const out{geometry::plan_of(atend ? mine.first(1.0) : -mine.first(0.0))}; + // the direction of the joint points out of the other track at its end + auto const kink{glm::length(out) > 1e-9 && glm::length(joint.direction) > 1e-9 ? glm::degrees(std::acos(std::clamp(-glm::dot(glm::normalize(out), glm::normalize(joint.direction)), -1.0, 1.0))) : 0.0}; + auto const at{editor_track::point_position(*track, joint.own)}; + geometry::bezier const theirs{joint.other->m_paths[joint.theirs.path]}; + auto const turn{geometry::signed_angle(geometry::plan_of(theirs.first(0.0)), geometry::plan_of(theirs.first(1.0)))}; + WriteLog(format("SELFTEST switchslide joint %d.%s %.3f %.3f - %s: kink %.4f deg, gap %.4f m, its R %.1f, L %.2f m", joint.own.path, atend ? "end" : "start", at.x, at.z, joint.other->name().c_str(), kink, + glm::distance(at, editor_track::point_position(*joint.other, joint.theirs)), std::abs(turn) > 1e-6 ? theirs.plan_length() / turn : 0.0, theirs.plan_length())); + } return selftest_result::handled; } std::string label; diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index 55287a2a..4bb61b3d 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -56,7 +56,8 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co auto config{m_orthophoto.settings()}; if (words.str().find(" off") != std::string::npos) { - m_orthophoto.enabled(false); + // as the user turns it off: the terrain model leaves it so + choose_orthophoto(false); WriteLog("SELFTEST orthophoto off"); return selftest_result::handled; } @@ -104,6 +105,108 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog(format("SELFTEST orthohires %d", hires)); return selftest_result::handled; } + if (command == "orthosettings") + { + // as the panel of the orthophoto changes them, kept with the editor + auto config{m_orthophoto.settings()}; + std::string key; + double value{0.0}; + auto changed{false}; + while (words >> key >> value) + { + changed = true; + if (key == "radius") + config.radius = std::clamp(static_cast(value), 0, editor_orthophoto::max_radius); + else if (key == "opacity") + config.opacity = std::clamp(static_cast(value), 0.0f, 1.0f); + else if (key == "lift") + config.lift = static_cast(value); + else if (key == "height") + config.height = static_cast(value); + else if (key == "year") + config.year = static_cast(value); + else if (key == "hires") + config.hires = value != 0.0; + else if (key == "drape") + config.drape = value != 0.0; + else if (key == "scene") + config.in_scene = value != 0.0; + else if (key == "on") + choose_orthophoto(value != 0.0); + } + if (changed) + { + m_orthophoto.settings(config); + save_orthophoto_settings(); + } + config = m_orthophoto.settings(); + WriteLog(format("SELFTEST orthosettings on %d radius %d opacity %.2f lift %.2f height %.2f year %d hires %d drape %d scene %d origin %.2f %.2f", m_orthophoto.enabled() ? 1 : 0, config.radius, config.opacity, config.lift, + config.height, config.year, config.hires ? 1 : 0, config.drape ? 1 : 0, config.in_scene ? 1 : 0, config.north, config.east)); + return selftest_result::handled; + } + if (command == "nmtsettings") + { + // as the terrain panel changes them, kept with the editor the next frame (keep_nmt_settings); the terrain while laying at + // once, as its chip keeps it + auto &state{m_nmt_state}; + std::string key; + double value{0.0}; + while (words >> key >> value) + { + if (key == "band") + state.band = std::clamp(value, 50.0, 3000.0); + else if (key == "around") + m_terrain_add_count = std::clamp(static_cast(value), 1, 8); + else if (key == "shape") + state.shape = value != 0.0; + else if (key == "follow") + state.follow = value != 0.0; + else if (key == "offset") + state.offset = value; + else if (key == "fitted") + state.fitted = value != 0.0; + else if (key == "grow") + { + state.grow = value != 0.0; + EditorSettings.terrain_while_laying(state.grow); + EditorSettings.save(); + } + } + WriteLog(format("SELFTEST nmtsettings band %.0f around %d shape %d follow %d grow %d offset %+.2f fitted %d", state.band, m_terrain_add_count, state.shape ? 1 : 0, state.follow ? 1 : 0, state.grow ? 1 : 0, state.offset, + state.fitted ? 1 : 0)); + return selftest_result::handled; + } + if (command == "wmsdescribe") + { + // what a WMS tells of itself, as the layers window reads it before one is added + std::string url; + words >> url; + auto const info{editor_orthophoto::describe(url)}; + WriteLog(format("SELFTEST wmsdescribe: \"%s\", %zu layers, EPSG:2180 %d%s", info.title.c_str(), info.layers.size(), info.puwg ? 1 : 0, info.error.empty() ? "" : (", error " + info.error).c_str())); + WriteLog("SELFTEST wmsdescribe terms: " + info.terms); + for (std::size_t i = 0; i < std::min(info.layers.size(), 5); ++i) + WriteLog("SELFTEST wmsdescribe layer " + info.layers[i].first + " \"" + info.layers[i].second + "\""); + return selftest_result::handled; + } + if (command == "wms") + { + // a WMS layer of the user's on top of the others, as the layers window adds it: wms
+ editorSettings::wms_layer entry; + words >> entry.url >> entry.layers; + entry.title = entry.layers; + EditorSettings.wms_layers().insert(EditorSettings.wms_layers().begin(), entry); + WriteLog("SELFTEST wms " + entry.url + " " + entry.layers); + return selftest_result::handled; + } + if (command == "wmsstats") + { + for (std::size_t i = 0; i < m_wms.size(); ++i) + { + auto const stats{m_wms[i]->stats()}; + WriteLog(format("SELFTEST wmsstats %zu %s: enabled %d, shown %d, loading %d, failed %d", i, m_wms[i]->settings().layers.c_str(), m_wms[i]->enabled() ? 1 : 0, stats.resident, stats.loading, stats.failed)); + } + return selftest_result::handled; + } if (command == "orthostats") { auto const stats{m_orthophoto.stats()}; @@ -203,6 +306,13 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co } return selftest_result::handled; } + if (command == "nmtoffset") + { + // the offset of the terrain model set, 0 as in an editor opened anew + words >> m_nmt_state.offset; + WriteLog(format("SELFTEST nmtoffset %+.2f", m_nmt_state.offset)); + return selftest_result::handled; + } if (command == "nmtauto") { // the tracks put onto the terrain model, as the button of the panel of the tracks does, all or the ones not fitted yet; @@ -213,6 +323,61 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog("SELFTEST nmtauto " + which + ": " + m_nmt_state.status); return selftest_result::handled; } + if (command == "turntables") + { + // each turntable: the height of its rails and of the model in its pit; of each track running up to it the heights out along + // its line from the rim, the model there under the rails beside them, and how far from the rim the line keeps level + for (auto const &table : nmt_turntables()) + { + auto const &points{table.table->m_paths.front().points}; + double pit{std::numeric_limits::quiet_NaN()}; + if (double height; m_nmt.height(table.centre.x, table.centre.z, height)) + pit = height + m_nmt_state.offset; + std::string included; + for (auto const &item : standing_templates([](std::string const &) { return true; })) + if (false == item.values.empty() && ToLower(item.values.front()) == table.table->name()) + included += format(" %s at %.3f %.3f %.3f", item.file.c_str(), item.location.x, item.location.y, item.location.z); + WriteLog(format("SELFTEST turntable %s: rails %.3f .. %.3f m, rim %.2f m, the model in the pit %.2f m, %zu tracks run up to it; its include%s", table.table->name().c_str(), points[segment_data::point::start].y, + points[segment_data::point::end].y, table.rim, pit, table.ends.size(), included.empty() ? " none" : included.c_str())); + for (auto const &[track, path, atend] : table.ends) + { + auto route{editor_track::run_route(*track, 80.0, path)}; + auto const samples{editor_track::sample_route(route, 1.0)}; + if (samples.size() < 2) + continue; + auto const &rim{track->m_paths[path].points[atend ? segment_data::point::end : segment_data::point::start]}; + auto const reversed{geometry::plan_distance(samples.back().position, rim) < geometry::plan_distance(samples.front().position, rim)}; + auto const length{samples.back().chainage - samples.front().chainage}; + auto const chainage_at = [&](double const Off) { return reversed ? samples.back().chainage - Off : samples.front().chainage + Off; }; + auto const height_at = [&](double const Off) { return editor_track::sampled_elevation(samples, chainage_at(Off)); }; + auto const rails{static_cast(track->rail_height())}; + std::string text; + for (auto const off : {0.0, 2.0, 5.0, 10.0, 15.0, 20.0, 30.0, 45.0, 60.0}) + { + if (off > length) + break; + text += format(" %.0f m %.3f", off, height_at(off)); + for (auto const &sample : samples) + if (std::abs(sample.chainage - chainage_at(off)) < 0.51) + { + if (double model; m_nmt.height(sample.position.x, sample.position.z, model)) + text += format(" (model %.2f)", model + m_nmt_state.offset + rails); + break; + } + } + // level off the rim within a centimetre + double level{0.0}; + for (double off = 0.0; off <= length; off += 0.5) + if (std::abs(height_at(off) - points[segment_data::point::start].y) <= 0.01) + level = off; + else + break; + WriteLog(format("SELFTEST turntable %s track %s: at the rim %.3f m, %+.2f per mille over its first 5 m, level within 1 cm of the table for %.1f m; out along it%s", table.table->name().c_str(), track->name().c_str(), rim.y, + 1000.0 * (height_at(std::min(5.0, length)) - height_at(0.0)) / std::max(1e-6, std::min(5.0, length)), level, text.c_str())); + } + } + return selftest_result::handled; + } if (command == "fittedstate") { // the tracks whose grade line is fitted, as the panel of the tracks tells it; reload: the marks read again from the lines @@ -313,13 +478,21 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co } if (command == "nmtgrow") { - // the cursor of a track laid at the place: the terrain grows around it + // the cursor of a track laid at the place, its first point placed there unless "chosen" says the tool is only chosen: the terrain + // grows around it once the laying begins auto const active{m_lay.active}; - words >> m_lay.mouse.x >> m_lay.mouse.z; + auto const points{m_lay.points}; + std::string chosen; + words >> m_lay.mouse.x >> m_lay.mouse.z >> chosen; m_lay.active = true; + if (chosen != "chosen" && m_lay.points.empty()) + m_lay.points.push_back(m_lay.mouse); + else if (chosen == "chosen") + m_lay.points.clear(); m_nmt_state.grow_time = -10.0; nmt_grow(); m_lay.active = active; + m_lay.points = points; WriteLog(format("SELFTEST nmtgrow: %zu chunks waiting, grown %d", m_nmt_state.pending.size(), m_nmt_state.grown ? 1 : 0)); return selftest_result::handled; } @@ -495,6 +668,13 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog(format("SELFTEST terrainselect: %zu selected", m_terrain_selection.size())); return selftest_result::handled; } + if (command == "terrainremove") + { + // the chunks selected taken out, as the button of the chunk panel and Del do + remove_selected_chunks(); + WriteLog("SELFTEST terrainremove: " + m_terrain_status); + return selftest_result::handled; + } if (command == "terrainresolution") { float spacing{1.f}; diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 05cb7a40..02f4e448 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -400,6 +400,14 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm auto const &entry{records[r]}; WriteLog(format("SELFTEST alignment %zu in %s: km %.3f, %zu vertices, %zu elements, consistent %d, length %.3f, kept %.3f - %.3f, %zu clicks", r, scene::Layers.layer(entry.layer).name.c_str(), entry.origin / 1000.0, entry.design.vertices.size(), entry.elements.size(), entry.consistent ? 1 : 0, entry.result.length, entry.from, entry.kept_to(), entry.clicks.size())); + // the curves as the plan has them, against the elements laid + for (std::size_t k = 0; k < entry.design.vertices.size(); ++k) + { + auto const &vertex{entry.design.vertices[k]}; + WriteLog(format("SELFTEST vertex %zu designed R %.1f L %.1f / %.1f cant %.0f compound %d arcs %zu", k, vertex.radius, vertex.transition_in, vertex.transition_out, vertex.cant, vertex.compound ? 1 : 0, vertex.arcs.size())); + } + for (auto const &curve : entry.result.curves) + WriteLog(format("SELFTEST planned curve %d R %.1f L %.1f / %.1f", curve.vertex, curve.radius, curve.transition_in, curve.transition_out)); for (auto const &e : entry.elements) WriteLog(format("SELFTEST element %s vertex %d km %.3f L %.3f R %.3f -> %.3f cant %.1f", e.kind.c_str(), e.vertex, (entry.origin + e.chainage) / 1000.0, e.length, e.radius_start, e.radius_end, e.cant)); } diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index 408663a6..b55d27f0 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -47,7 +47,38 @@ using namespace track_mode; // the name of the template in the language of the editor, editormodeswitchtool.cpp std::string switch_name_translated(std::string Title); +namespace +{ + +segment_data straight_path(glm::dvec3 const &Start, glm::dvec3 const &End) +{ + segment_data path; + path.points[segment_data::point::start] = Start; + path.points[segment_data::point::end] = End; + return path; +} + +} // namespace + +std::vector editor_mode::switch_memo_key() const +{ + auto const &tool{m_switch}; + auto const &io{ImGui::GetIO()}; + auto const heading{tool.free_heading.value_or(glm::dvec2{9.0})}; + return {tool.mouse.x, tool.mouse.y, tool.mouse.z, tool.point.x, tool.point.z, io.MousePos.x, io.MousePos.y, heading.x, heading.y, tool.free_end.position.x, tool.free_end.position.z, + static_cast(reinterpret_cast(tool.free_end.track)), tool.along, tool.pressed, static_cast(tool.armed), static_cast(tool.connection), + static_cast(tool.placing) + 2.0 * tool.free + 4.0 * tool.curved + 8.0 * tool.one_sided + 16.0 * io.KeyShift + 32.0 * io.KeyCtrl + 64.0 * io.KeyAlt, static_cast(m_history.size())}; +} + std::vector editor_mode::switch_preview() const +{ + auto key{switch_memo_key()}; + if (false == m_switch_memo.fresh(key)) + m_switch_memo.keep(std::move(key), switch_preview_now()); + return m_switch_memo.result; +} + +std::vector editor_mode::switch_preview_now() const { auto const &tool{m_switch}; if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) @@ -73,17 +104,11 @@ std::vector editor_mode::switch_preview() const } if (tool.templates[tool.armed].double_slip) { - // a slip at the crossing where it was pressed: its two straights, laid there once dragged + // the slip dragged out on a straight: its two straights crossing, the one it brings along through the quarter of the cursor slip_plan plan; - if (false == plan_double_slip(tool.line, tool.point, tool.templates[tool.armed], plan) || plan.replaces != nullptr) + if (false == plan_double_slip(tool.line, tool.point, tool.templates[tool.armed], plan, lone_slip_side()) || plan.replaces != nullptr) return {}; - auto const straight = [](glm::dvec3 const &From, glm::dvec3 const &To) { - segment_data path; - path.points[segment_data::point::start] = From; - path.points[segment_data::point::end] = To; - return path; - }; - return {straight(plan.points[0], plan.points[1]), straight(plan.points[2], plan.points[3])}; + return {straight_path(plan.ends[0], plan.ends[1]), straight_path(plan.ends[2], plan.ends[3])}; } if (switch_link plan; plan_switch_link(plan)) { @@ -172,9 +197,12 @@ bool editor_mode::start_switch_placement() return false; if (tool.templates[tool.armed].double_slip) { - // at the crossing where it was pressed, laid only once dragged, as a switch is + // at a crossing, where it was pressed; with no track crossing here it's dragged out like a switch, the crossing track it brings + // along through the quarter the cursor is let go in. Laid only once dragged, as a switch is + slip_plan plan; + auto const lone{false == plan_double_slip(line, cursor, tool.templates[tool.armed], plan) && plan.lone}; tool.along = along; - tool.point = cursor; + tool.point = lone ? point : cursor; tool.mouse = cursor; tool.line = line; tool.placing = true; @@ -214,7 +242,7 @@ void editor_mode::finish_switch_placement() return; if (tool.templates[tool.armed].double_slip && false == tool.curved) { - insert_double_slip(tool.line, tool.point, tool.templates[tool.armed]); + insert_double_slip(tool.line, tool.point, tool.templates[tool.armed], lone_slip_side()); return; } if (linked) @@ -267,7 +295,23 @@ bool editor_mode::free_switch_offered() const editor_track::snap_target editor_mode::free_switch_end(glm::dvec3 const &Ground) const { - return editor_track::find_free_end(nullptr, kRailCategory, Ground, kLaySnapRadius, {}); + // the free end under the cursor on the screen, however high the tracks are against the ground the cursor is on: a scenery built + // below or above its terrain; else the nearest one within reach in the plan + if (false == m_cursor_override.has_value() && ImGui::IsMousePosValid()) + { + auto const &io{ImGui::GetIO()}; + if (auto const end{snap_free_end(Ground, {io.MousePos.x, io.MousePos.y}, nullptr, kRailCategory, {})}; end.track != nullptr) + return end; + } + editor_track::snap_target result; + auto nearest{kLaySnapRadius}; + for (auto const &end : editor_track::free_ends(nullptr, kRailCategory, Ground, kLaySnapRadius + 100.0, {})) + if (auto const distance{plan_distance(end.position, Ground)}; distance < nearest) + { + nearest = distance; + result = end; + } + return result; } void editor_mode::start_free_switch(glm::dvec3 const &Ground) @@ -281,6 +325,7 @@ void editor_mode::start_free_switch(glm::dvec3 const &Ground) tool.free = true; tool.free_end = free_switch_end(Ground); tool.free_heading.reset(); + tool.one_sided = false; if (tool.free_end.track != nullptr) { // going on from the free end in its direction @@ -323,12 +368,22 @@ std::vector editor_mode::free_switch_paths() const int side; double grade; free_switch_pose(start, heading, side, grade); + // dragged on past its end from a free end: bent, with the track into the free end the cursor is at + if (free_bend bend; free_switch_bend(bend)) + { + auto paths{bend.paths}; + if (bend.target.track != nullptr && bend.problem.empty()) + paths.insert(paths.end(), bend.link.pieces.begin(), bend.link.pieces.end()); + return paths; + } return editor_track::place_switch(tool.templates[tool.armed], start, heading, side, grade); } void editor_mode::insert_free_switch() { auto &tool{m_switch}; + free_bend bend; + auto const bent{free_switch_bend(bend)}; tool.free = false; // a click with no drag on the ground gives no direction; from a free end it's the end's if (tool.free_end.track == nullptr && false == tool.free_heading.has_value()) @@ -336,17 +391,35 @@ void editor_mode::insert_free_switch() tool.status = refused(STR_C("Drag along the main track of the switch, then sideways to the side of the diverging one")); return; } - auto const paths{free_switch_paths()}; + auto const paths{bent ? bend.paths : free_switch_paths()}; if (paths.size() < 2) return; auto const made{create_switch_set(tool.templates[tool.armed], paths, tool.free_end.track, switch_name_for_new())}; if (made.empty()) return; - push_track_snapshot({}, made, {}); + // the track into the free end the cursor was at, in the same step of the history + std::vector> states; + std::vector created{made}; + auto const linked{bent && bend.target.track != nullptr && bend.problem.empty()}; + if (linked) + link_tracks(bend.target, bend.link, bend.target.track->eType == tt_Normal ? *bend.target.track : *tool.free_end.track, states, created); + push_track_snapshot(std::move(states), std::move(created), {}); straight_refresh(); switch_placed(made); - auto const text{tool.free_end.track != nullptr ? format(STR_C("Switch %s laid on from %s, its other ends free: lay on from them"), made.front()->name().c_str(), tool.free_end.track->name().c_str()) - : format(STR_C("Switch %s laid with no track, its ends free: lay on from them"), made.front()->name().c_str())}; + auto text{tool.free_end.track != nullptr ? format(STR_C("Switch %s laid on from %s, its other ends free: lay on from them"), made.front()->name().c_str(), tool.free_end.track->name().c_str()) + : format(STR_C("Switch %s laid with no track, its ends free: lay on from them"), made.front()->name().c_str())}; + if (bent && bend.radius > 0.0) + text = format(STR_C("Switch %s laid on from %s bent into R %.0f m, %s"), made.front()->name().c_str(), tool.free_end.track->name().c_str(), bend.radius, + bend.side == bend.turn ? STR_C("one-sided") : STR_C("two-sided")); + if (linked) + text += "\n" + link_summary(bend.target, bend.link, editor_track::branch_radius(bend.paths[bend.diverging ? 1 : 0])); + else if (bent && false == bend.problem.empty()) + text += "\n" + bend.problem; + if (bent) + for (auto const &warning : bend.report.warnings) + text += "\n" + warning; + for (auto const &warning : free_switch_exits(paths)) + text += "\n" + warning; tool.status = text + (tool.status.empty() ? std::string{} : "\n" + tool.status); ui()->set_status(tool.status, false); } @@ -851,14 +924,6 @@ std::vector editor_mode::link_tracks(editor_track::snap_target const & namespace { -segment_data straight_path(glm::dvec3 const &Start, glm::dvec3 const &End) -{ - segment_data path; - path.points[segment_data::point::start] = Start; - path.points[segment_data::point::end] = End; - return path; -} - // the part of the slip curve from Start along Heading, turning by Turn to Finish, between the distances From and To along it; where it // changes the radius made of pieces, with one curve from end to end for the versions which know no pieces segment_data slip_arc(editor_track::slip_curve const &Curve, glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) @@ -930,7 +995,6 @@ segment_data slip_arc(editor_track::slip_curve const &Curve, glm::dvec3 const &S std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type, std::function const &Bend) { auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; }; - auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) { return straight_path(Start, End); }; // the ends of the two diagonals at the crossing are kept apart further than the simulation joins the ends, 2 cm auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))}; auto const gap{std::max(0.25, 0.06 / halfsine)}; @@ -955,18 +1019,14 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g std::string base; auto const prefix{Type == "Skrz" ? "editor_skrz" : Type == "Rkp" ? "editor_dkp" : "editor_dks"}; // a crossing keeps the names of its tracks, only its record holds the name - auto const recorded = [](std::string const &Name) { - auto const &records{switch_store::records()}; - return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; }); - }; - for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || recorded(base); ++i) + for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || switch_store::has(base); ++i) base = prefix + std::to_string(i); std::vector parts; if (Type == "Skrz") { // the two tracks just cross, their ends far from the crossing - parts.push_back(editor_track::create_path(Style, laid(straight(PointA, PointB)))); - parts.push_back(editor_track::create_path(Style, laid(straight(PointC, PointD)))); + parts.push_back(editor_track::create_path(Style, laid(straight_path(PointA, PointB)))); + parts.push_back(editor_track::create_path(Style, laid(straight_path(PointC, PointD)))); parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); record_slip(parts, "crossing", Type, 0.0, Angle, base); return parts; @@ -974,18 +1034,18 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g editor_track::switch_template shape; shape.length = glm::distance(plan_of(PointA), Crossing); bool const single{Type == "Rkp"}; - parts.push_back(editor_track::create_switch(shape, {laid(straight(PointA, centreA)), laid(arc(PointA, First, side, PointD, 0.0, middle - split))}, Style, base + "_a")); + parts.push_back(editor_track::create_switch(shape, {laid(straight_path(PointA, centreA)), laid(arc(PointA, First, side, PointD, 0.0, middle - split))}, Style, base + "_a")); if (single) - parts.push_back(editor_track::create_path(Style, laid(straight(PointB, centreB)))); + parts.push_back(editor_track::create_path(Style, laid(straight_path(PointB, centreB)))); else - parts.push_back(editor_track::create_switch(shape, {laid(straight(PointB, centreB)), laid(back(arc(PointC, Second, -side, PointB, middle + split, whole)))}, Style, base + "_b")); + parts.push_back(editor_track::create_switch(shape, {laid(straight_path(PointB, centreB)), laid(back(arc(PointC, Second, -side, PointB, middle + split, whole)))}, Style, base + "_b")); if (single) - parts.push_back(editor_track::create_path(Style, laid(straight(PointC, centreC)))); + parts.push_back(editor_track::create_path(Style, laid(straight_path(PointC, centreC)))); else - parts.push_back(editor_track::create_switch(shape, {laid(straight(PointC, centreC)), laid(arc(PointC, Second, -side, PointB, 0.0, middle - split))}, Style, base + "_c")); - parts.push_back(editor_track::create_switch(shape, {laid(straight(PointD, centreD)), laid(back(arc(PointA, First, side, PointD, middle + split, whole)))}, Style, base + "_d")); - parts.push_back(editor_track::create_path(Style, laid(straight(centreA, centreB)))); - parts.push_back(editor_track::create_path(Style, laid(straight(centreC, centreD)))); + parts.push_back(editor_track::create_switch(shape, {laid(straight_path(PointC, centreC)), laid(arc(PointC, Second, -side, PointB, 0.0, middle - split))}, Style, base + "_c")); + parts.push_back(editor_track::create_switch(shape, {laid(straight_path(PointD, centreD)), laid(back(arc(PointA, First, side, PointD, middle + split, whole)))}, Style, base + "_d")); + parts.push_back(editor_track::create_path(Style, laid(straight_path(centreA, centreB)))); + parts.push_back(editor_track::create_path(Style, laid(straight_path(centreC, centreD)))); parts.push_back(editor_track::create_path(Style, laid(arc(PointA, First, side, PointD, middle - split, middle + split)))); if (false == single) parts.push_back(editor_track::create_path(Style, laid(arc(PointC, Second, -side, PointB, middle - split, middle + split)))); @@ -1004,7 +1064,7 @@ glm::dvec3 editor_mode::cursor_on_straight(editor_track::straight const &Line) c return cursor; } -bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan) const +bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan, int const Side) const { Plan = slip_plan{}; // the crossing of the straight with the rail track nearest the cursor, whatever that track is: the slip goes there or @@ -1071,10 +1131,69 @@ bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec return true; } } + // the curves of the slip for the angle of the tracks crossing, as long as the straights have room for them + bool const plain{Shape.type == "Skrz"}; + auto const shaped = [&](double const Angle, double const Available) { + auto const *gauged{Line.tracks.front()}; + auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; + auto curve{editor_track::slip_of(Shape.radius, Shape.middle, Angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)}; + curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); + auto half{plain ? Shape.a : curve.tangent()}; + if (plain && half > Available) + { + half = Available; + if (half < 2.0) + { + Plan.problem = STR_C("Not enough straight track around the crossing"); + return false; + } + } + else if (half > Available) + { + curve = curve.scaled(Available / half); + half = Available; + if (curve.radius < 50.0) + { + Plan.problem = STR_C("Not enough straight track around the crossing"); + return false; + } + Plan.note = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast(curve.radius), static_cast(Available)); + } + Plan.curve = curve; + Plan.half = half; + return true; + }; if (nearest == nullptr) { + Plan.lone = true; Plan.problem = STR_C("No track crosses the selected straight under the cursor"); - return false; + if (Side == 0 || Line.tracks.empty()) + return false; + // no track crossing here: the slip brings the crossing one along, at its own angle, ahead to the side given, its ends free + auto const angle{std::atan(1.0 / (Shape.ratio > 0.0 ? Shape.ratio : 9.0))}; + auto const first{Line.direction}; + auto const direction{turned(first, static_cast(Side) * angle)}; + auto const alongfirst{std::clamp(glm::dot(plan_of(Point) - plan_of(Line.start), first), 0.0, Line.length)}; + auto const crossing{plan_of(Line.start) + first * alongfirst}; + if (false == shaped(angle, std::min(alongfirst, Line.length - alongfirst) - 0.5)) + return false; + Plan.problem.clear(); + auto const half{Plan.half}; + auto const height = [&](double const Along) { return Line.start.y + Line.grade * Along; }; + auto const at = [&](glm::dvec2 const &Planar, double const Height) { return glm::dvec3{Planar.x, Height, Planar.y}; }; + Plan.crossing = crossing; + Plan.first = first; + Plan.direction = direction; + Plan.angle = angle; + Plan.alongfirst = alongfirst; + Plan.points = {at(crossing - first * half, height(alongfirst - half)), at(crossing + first * half, height(alongfirst + half)), at(crossing - direction * half, height(alongfirst - half * std::cos(angle))), + at(crossing + direction * half, height(alongfirst + half * std::cos(angle)))}; + // the straights of both tracks out to the joints of the template, past the frogs of the diamond, as far as the line has room + auto const reach{std::max(half, std::min(Shape.a, std::min(alongfirst, Line.length - alongfirst) - 0.5))}; + Plan.reach = reach; + Plan.ends = {at(crossing - first * reach, height(alongfirst - reach)), at(crossing + first * reach, height(alongfirst + reach)), at(crossing - direction * reach, height(alongfirst - reach * std::cos(angle))), + at(crossing + direction * reach, height(alongfirst + reach * std::cos(angle)))}; + return true; } if (nearest->eType != tt_Normal) { @@ -1097,7 +1216,6 @@ bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec if (glm::dot(first, direction) < 0.0) direction = -direction; auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; - bool const plain{Shape.type == "Skrz"}; if (angle < glm::radians(0.5) || angle > glm::radians(plain ? 89.0 : 30.0)) { Plan.problem = STR_C("The straights cross at an angle unsuitable for a double slip"); @@ -1111,31 +1229,9 @@ bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)}; auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)}; auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5}; - auto const *gauged{Line.tracks.front()}; - auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; - auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)}; - curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); - auto half{plain ? Shape.a : curve.tangent()}; - if (plain && half > available) - { - half = available; - if (half < 2.0) - { - Plan.problem = STR_C("Not enough straight track around the crossing"); - return false; - } - } - else if (half > available) - { - curve = curve.scaled(available / half); - half = available; - if (curve.radius < 50.0) - { - Plan.problem = STR_C("Not enough straight track around the crossing"); - return false; - } - Plan.note = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast(curve.radius), static_cast(available)); - } + if (false == shaped(angle, available)) + return false; + auto const half{Plan.half}; auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; auto const heightsecond = [&](double const Along) { return second.start.y + second.grade * Along; }; auto const sign{glm::dot(second.direction, direction) > 0.0 ? 1.0 : -1.0}; @@ -1146,10 +1242,11 @@ bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec Plan.direction = direction; Plan.angle = angle; Plan.alongfirst = alongfirst; - Plan.half = half; - Plan.curve = curve; Plan.points = {at(crossing - first * half, heightfirst(alongfirst - half)), at(crossing + first * half, heightfirst(alongfirst + half)), at(crossing - direction * half, heightsecond(alongsecond - sign * half)), at(crossing + direction * half, heightsecond(alongsecond + sign * half))}; + // the tracks crossing are there already, nothing laid out past the slip + Plan.reach = half; + Plan.ends = Plan.points; return true; } @@ -1176,16 +1273,16 @@ std::string editor_mode::slip_skew(editor_track::switch_template const &Shape, d } auto const ratio{1.0 / std::tan(Angle)}; if (fitting == nullptr) - return format(STR_C("The straights cross at 1:%.2f, %s is made for 1:%.2f and no slip template is made for that"), ratio, switch_name_translated(Shape.title()).c_str(), Shape.ratio); + return format(STR_C("The straights cross at 1:%.2f, %s is made for 1:%.2f and no slip template for that: drag the slip out on one straight, its crossing track comes along at its skos"), ratio, switch_name_translated(Shape.title()).c_str(), Shape.ratio); return format(STR_C("The straights cross at 1:%.2f, %s is made for 1:%.2f: %s fits that crossing"), ratio, switch_name_translated(Shape.title()).c_str(), Shape.ratio, switch_name_translated(fitting->title()).c_str()); } -bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape) +bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, int const Side) { auto &state{m_straights}; state.status.clear(); slip_plan plan; - if (false == plan_double_slip(Line, Point, Shape, plan)) + if (false == plan_double_slip(Line, Point, Shape, plan, Side)) { state.status = refused(plan.problem); return false; @@ -1211,11 +1308,13 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv std::vector created; std::vector removed; TTrack *style{nullptr}; - if (false == cut_straight(Line, alongfirst - half, alongfirst + half, states, created, removed, &style)) + // a slip bringing its crossing track along takes the line up to the joints of its template + auto const reach{other == nullptr ? std::max(half, plan.reach) : half}; + if (false == cut_straight(Line, alongfirst - reach, alongfirst + reach, states, created, removed, &style)) return false; - auto const secondline{editor_track::find_straight(*other, m_straights.tolerance)}; - auto const secondalong{glm::dot(crossing - plan_of(secondline.start), secondline.direction)}; - if (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr)) + auto const secondline{other != nullptr ? editor_track::find_straight(*other, m_straights.tolerance) : editor_track::straight{}}; + auto const secondalong{other != nullptr ? glm::dot(crossing - plan_of(secondline.start), secondline.direction) : 0.0}; + if (other != nullptr && (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr))) { for (auto *track : created) editor_track::retire(*track); @@ -1236,14 +1335,36 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv auto const nearer{glm::distance((plan_of(pointA) + plan_of(pointD)) * 0.5, plan_of(Point)) <= glm::distance((plan_of(pointC) + plan_of(pointB)) * 0.5, plan_of(Point))}; auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, curve, styled, Shape.type) : build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, curve, styled, Shape.type)}; - editor_track::commit(parts); - created.insert(created.end(), parts.begin(), parts.end()); + std::vector made{parts}; + // the straights of both tracks from the ends of the slip out to the joints of the template + if (reach > half + 0.01) + for (auto const &[from, to] : {std::pair{plan.ends[0], pointA}, {pointB, plan.ends[1]}, {plan.ends[2], pointC}, {pointD, plan.ends[3]}}) + if (auto *straight{editor_track::create_path(styled, straight_path(from, to))}) + made.push_back(straight); + editor_track::commit(made); + created.insert(created.end(), made.begin(), made.end()); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); switch_placed(parts); + if (other == nullptr) + { + state.status = format(STR_C("The slip laid with its crossing track at 1:%.1f, the ends of that track free: lay on from them"), 1.0 / std::tan(angle)) + (plan.note.empty() ? std::string{} : "\n" + plan.note); + ui()->set_status(state.status, false); + } return true; } +int editor_mode::lone_slip_side() const +{ + auto const &tool{m_switch}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const &first{tool.line.direction}; + // the crossing track through the quarter the cursor is in: ahead to the left or behind to the right turns it to the left + auto const left{cross(first, offset) >= 0.0}; + auto const ahead{glm::dot(first, offset) >= 0.0}; + return left == ahead ? 1 : -1; +} + bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape) { auto &state{m_straights}; @@ -1413,7 +1534,10 @@ std::vector editor_mode::sample_chain(editor_track::c bezier const curve{path}; auto const length{glm::distance(curve.p0, curve.p3)}; int const steps{std::max(8, static_cast(std::ceil(length / 0.5)))}; - for (int k = (result.empty() ? 0 : 1); k <= steps; ++k) + // the first point of a track going on right where the one before ended is that one's last; one past a switch the chain leaves + // out starts apart from it, and its first point is taken too, the joint there on the frame where it is + auto const continuous{false == result.empty() && glm::distance(curve.point(forward ? 0.0 : 1.0), result.back().position) < 1e-3}; + for (int k = (result.empty() || false == continuous ? 0 : 1); k <= steps; ++k) { auto const t{forward ? static_cast(k) / steps : 1.0 - static_cast(k) / steps}; auto const position{curve.point(t)}; @@ -1511,7 +1635,33 @@ double editor_mode::curved_switch_start(int const Direction, std::string *What) return tool.pressed; auto const length{tool.templates[tool.armed].length}; std::string what; - auto const start{switch_curve::snapped_start(switch_curve::ramp_ends(frame_points(tool.frame)), tool.pressed, length, Direction, 3.0, &what)}; + auto start{switch_curve::snapped_start(switch_curve::ramp_ends(frame_points(tool.frame)), tool.pressed, length, Direction, 3.0, &what)}; + // as on a straight, an end of the switch within half a metre of a joint of the tracks goes right onto it: no path of a few centimetres + // left beside it, no gap at it + constexpr double kJointPull{0.5}; + auto nearest{kJointPull}; + auto shift{0.0}; + auto const first{Direction > 0 ? start : start - length}; + for (auto const *track : tool.frame_tracks) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const joint{nearest_station(tool.frame, track->m_paths.front().points[index])}; + for (auto const end : {first, first + length}) + if (auto const off{std::abs(joint - end)}; off > 1e-4 && off < nearest) + { + nearest = off; + shift = joint - end; + } + } + } + if (shift != 0.0) + { + start += shift; + what = STR("The switch drawn to the joint of the tracks"); + } // a start which would put the switch off the curve isn't one auto const total{tool.frame.back().station}; if (Direction > 0 ? start < 0.0 || start > total - length : start < length || start > total) @@ -1524,6 +1674,61 @@ double editor_mode::curved_switch_start(int const Direction, std::string *What) namespace { +// where a switch with a straight frog stops being bent, as the DB bends such switches into a curve: from the start of the switch up to the +// frog, both tracks straight over the frog; along the main track from the start of the switch, where the straight the diverging track +// ends with begins. None for a switch with its curve running through the frog, the ones of table 11.9, nor a double one +std::optional frog_zone(editor_track::switch_template const &Shape) +{ + if (Shape.frog_straight < 0.5 || Shape.twofold() || Shape.double_slip || Shape.type != "Rz") + return std::nullopt; + auto const &diverging{Shape.local[1]}; + auto const &last{diverging.pieces.empty() ? diverging : diverging.pieces.back()}; + auto const &end{last.points[segment_data::point::end]}; + auto const &control{last.points[segment_data::point::control2]}; + auto direction{control != glm::dvec3{} ? -plan_of(control) : plan_of(end - last.points[segment_data::point::start])}; + if (glm::length(direction) < 1e-9) + return std::nullopt; + direction = glm::normalize(direction); + // in the plan of a template x runs across the main track, z along it + auto const along{end.z - Shape.frog_straight * direction.y}; + if (along < 1.0 || along > Shape.length - 0.5) + return std::nullopt; + return along; +} + +// the pieces of a path of a switch template, one crossing the place along the main track split there +std::vector split_along(segment_data const &Path, double const Along) +{ + std::vector const pieces{Path.pieces.empty() ? std::vector{Path} : Path.pieces}; + std::vector result; + for (auto const &piece : pieces) + { + auto const &start{piece.points[segment_data::point::start]}; + auto const &end{piece.points[segment_data::point::end]}; + if (start.z > Along - 1e-3 || end.z < Along + 1e-3) + { + result.push_back(piece); + continue; + } + bezier const curve{piece}; + double low{0.0}; + double high{1.0}; + for (int i = 0; i < 60; ++i) + { + auto const middle{0.5 * (low + high)}; + (curve.point(middle).z < Along ? low : high) = middle; + } + auto const t{0.5 * (low + high)}; + for (auto [from, to] : {std::pair{0.0, t}, std::pair{t, 1.0}}) + { + auto part{geometry::part_of(piece, from, to)}; + part.pieces.clear(); + result.push_back(part); + } + } + return result; +} + // a path from its start, in the plan, a point every quarter of a metre std::vector plan_points(segment_data const &Path) { @@ -1555,26 +1760,29 @@ std::vector plan_points(segment_data const &Path) } // namespace -switch_curve::switch_report editor_mode::curved_switch_report(std::vector const &Paths, double const Station, int const Direction) const +switch_curve::switch_report editor_mode::curved_switch_report(std::vector const &Paths, std::vector const &Frame, double const Station, int const Direction) const { auto const &tool{m_switch}; if (Paths.size() < 2 || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) return {}; auto const &shape{tool.templates[tool.armed]}; - return switch_curve::check(plan_points(Paths[0]), plan_points(Paths[1]), frame_points(tool.frame), Station, Station + Direction * shape.length, shape.radius, shape.ratio); + auto const frog{frog_zone(shape)}; + return switch_curve::check(plan_points(Paths[0]), plan_points(Paths[1]), frame_points(Frame), Station, Station + Direction * shape.length, shape.radius, shape.ratio, frog.has_value() ? shape.length - *frog : 0.0); } -segment_data editor_mode::frame_path(segment_data const &Local, std::vector const &Frame, double const Station, int const Direction, int const Side) +segment_data editor_mode::frame_path(segment_data const &Local, std::vector const &Frame, double const Station, int const Direction, int const Side, std::optional const &Bent) { auto const map = [&](glm::dvec3 const &Point, glm::dvec2 &Tangent, glm::dvec2 const &Localtangent) { - auto const sample{sample_at(Frame, Station + Direction * Point.z)}; + auto const bent{Bent.has_value() ? std::min(Point.z, *Bent) : Point.z}; + auto const sample{sample_at(Frame, Station + Direction * bent)}; auto const along{sample.tangent * static_cast(Direction)}; glm::dvec2 const across{glm::dvec2{-along.y, along.x} * static_cast(Side)}; Tangent = glm::normalize(along * Localtangent.y + across * Localtangent.x); - auto const planar{plan_of(sample.position) + across * Point.x}; - auto const cant{glm::radians(static_cast(sample.roll) * Direction)}; + auto const planar{plan_of(sample.position) + along * (Point.z - bent) + across * Point.x}; + auto const level{Point.z > bent ? sample_at(Frame, Station + Direction * Point.z) : sample}; + auto const cant{glm::radians(static_cast(level.roll) * Direction)}; auto const cone{-Point.x * Side * std::tan(cant)}; - return glm::dvec3{planar.x, sample.position.y + Point.y + cone, planar.y}; + return glm::dvec3{planar.x, level.position.y + Point.y + cone, planar.y}; }; auto const hermite = [](glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection) { auto const handle{plan_distance(Start, End) / 3.0}; @@ -1628,6 +1836,9 @@ segment_data editor_mode::frame_path(segment_data const &Local, std::vector editor_mode::curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side) { + // a switch with a straight frog bent up to the frog, beyond it going on along the curve as it heads there; the heights and the cant + // those of the line where the point is along it + auto const frog{frog_zone(Shape)}; std::vector locals{Shape.local.begin(), Shape.local.end()}; if (Shape.twofold()) { @@ -1638,9 +1849,12 @@ std::vector editor_mode::curved_switch_paths(editor_track::switch_ for (std::size_t i = 0; i < locals.size(); ++i) { auto const &local{locals[i]}; - auto path{frame_path(local, Frame, Station, Direction, Side)}; - for (auto const &piece : local.pieces) - path.pieces.push_back(frame_path(piece, Frame, Station, Direction, Side)); + auto path{frame_path(local, Frame, Station, Direction, Side, frog)}; + // the tracks of a switch bent up to the frog in pieces: the bent one and the straight one over the frog + auto const pieces{frog.has_value() && i < 2 ? split_along(local, *frog) : local.pieces}; + if (pieces.size() >= 2 || false == local.pieces.empty()) + for (auto const &piece : pieces) + path.pieces.push_back(frame_path(piece, Frame, Station, Direction, Side, frog)); // the main tracks follow the curve they are laid into if (i % 2 == 0) path.radius = sample_at(Frame, Station + Direction * 0.5 * (local.points[segment_data::point::start].z + local.points[segment_data::point::end].z)).radius; @@ -1662,10 +1876,16 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; auto const from{Direction > 0 ? station : station - shape.length}; auto const to{from + shape.length}; - std::vector> states; - std::vector created; - std::vector removed; + editor_track::edit edit; + // a track of the scenery changed: its state before kept once for Ctrl+Z, none for one made here + auto const keep = [&](TTrack &Track) { + if (std::find(edit.created.begin(), edit.created.end(), &Track) == edit.created.end()) + edit.capture(Track); + }; + auto const frogged{frog_zone(shape).has_value()}; std::vector candidates{tool.frame_tracks}; + // a cut this near a joint isn't made: no path of a few centimetres, the end of the track there goes onto the end of the switch instead + constexpr double kJointSnap{0.05}; for (auto const cut : {from, to}) { auto const point{sample_at(tool.frame, cut).position}; @@ -1677,13 +1897,14 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) auto const found{editor_track::point_at(*candidate, t)}; if (plan_distance(found, point) > 0.05) continue; - if (t > 0.002 && t < 0.998) + auto const &own{candidate->m_paths.front()}; + auto const atjoint{std::min(plan_distance(own.points[segment_data::point::start], point), plan_distance(own.points[segment_data::point::end], point)) < kJointSnap}; + if (false == atjoint && t > 0.002 && t < 0.998) { - if (std::find(created.begin(), created.end(), candidate) == created.end() && std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == candidate; })) - states.emplace_back(candidate, editor_track::capture(*candidate)); + keep(*candidate); if (auto *second{editor_track::split_path(*candidate, t)}) { - created.push_back(second); + edit.created.push_back(second); candidates.push_back(second); } } @@ -1699,22 +1920,236 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) if (where > from + 0.01 && where < to - 0.01) { editor_track::retire(*candidate); - removed.push_back(candidate); + edit.removed.push_back(candidate); } } auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; - auto const report{curved_switch_report(paths, station, Direction)}; + // a joint a few centimetres off an end of the switch, not cut there: the end of the track left beside it goes right onto the end of + // the switch, so the two join and no gap is left; the end of one bent up to the frog is fitted to it further on + std::vector snapped; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + if (index == segment_data::point::end && frogged) + continue; + auto const target{paths.front().points[index]}; + for (auto *candidate : candidates) + { + if (candidate->m_editorremoved || candidate->eType != tt_Normal) + continue; + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + auto const off{plan_distance(candidate->m_paths.front().points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end], target)}; + if (off < 1e-4 || off > kJointSnap) + continue; + keep(*candidate); + editor_track::move_point(*candidate, {0, kind}, target); + if (std::find(snapped.begin(), snapped.end(), candidate) == snapped.end()) + snapped.push_back(candidate); + } + } + } + editor_track::commit(snapped); + auto const report{curved_switch_report(paths, tool.frame, station, Direction)}; + // a switch bent up to the frog leaves the curve straight, a little off it at its end: the track going on from there along the line + // starts where the switch ends, heading as it does, and meets the line again at its other end + std::string fitted; + if (frogged) + { + auto const endsample{sample_at(tool.frame, station + Direction * shape.length)}; + auto const ending{endsample.position}; + auto const &main{paths.front()}; + auto const &last{main.pieces.empty() ? main : main.pieces.back()}; + auto const switchend{main.points[segment_data::point::end]}; + auto const outward{-editor_track::end_direction(last, editor_track::point_kind::end)}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || + std::find(edit.removed.begin(), edit.removed.end(), track) != edit.removed.end()) + continue; + auto const &path{track->m_paths.front()}; + auto const atend{plan_distance(path.points[segment_data::point::end], ending) < 0.05}; + if (false == atend && plan_distance(path.points[segment_data::point::start], ending) >= 0.05) + continue; + keep(*track); + auto const reshape = [&](segment_data &Path) { + auto &point{Path.points[atend ? segment_data::point::end : segment_data::point::start]}; + auto &control{Path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const other{Path.points[atend ? segment_data::point::start : segment_data::point::end]}; + auto const handle{glm::length(plan_of(control)) > 1e-6 ? glm::length(plan_of(control)) : plan_distance(point, other) / 3.0}; + // into the track from the end touching the switch + auto const into{outward * (atend ? -1.0 : 1.0) * handle}; + point = {switchend.x, point.y, switchend.z}; + control = {into.x, control.y, into.y}; + }; + auto &own{track->m_paths.front()}; + reshape(own); + if (false == own.pieces.empty()) + reshape(atend ? own.pieces.back() : own.pieces.front()); + editor_track::commit({track}); + auto const turn{glm::degrees(std::acos(std::clamp(glm::dot(outward, glm::normalize(endsample.tangent * static_cast(Direction))), -1.0, 1.0)))}; + fitted = "\n" + format(STR_C("Track %s going on from the switch fitted to its end, which leaves the curve straight over the frog: %.1f cm off it, turned by %.2f deg"), track->name().c_str(), + 100.0 * plan_distance(switchend, ending), turn); + break; + } + } auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; - created.insert(created.end(), made.begin(), made.end()); + edit.created.insert(edit.created.end(), made.begin(), made.end()); if (made.size() == 1) tool.status = format(STR_C("Switch %s inserted into the curve, Ctrl+Z takes it back"), made.front()->name().c_str()); - // what the norms say of it stays in the status + // what the norms say of it stays in the status, after what the drive placed with it tells + std::string norms; for (auto const &warning : report.warnings) - tool.status += "\n" + warning; - auto const placed{created}; - push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + norms += "\n" + warning; + norms += fitted; + tool.status += norms; + auto const placed{edit.created}; + push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed)); straight_refresh(); switch_placed(placed); + if (false == norms.empty() && tool.status.find(norms) == std::string::npos) + tool.status += norms; +} + +std::vector editor_mode::free_switch_exits(std::vector const &Paths) const +{ + auto const &tool{m_switch}; + std::vector result; + if (tool.free_end.track == nullptr || tool.free_end.track->eType != tt_Switch) + return result; + // the main and the diverging track of this one, each going on from that switch + for (std::size_t i = 0; i < std::min(2, Paths.size()); ++i) + for (auto const &warning : switch_exit_warnings(tool.free_end.track, tool.free_end.position, nullptr, {}, {Paths[i]})) + if (std::find(result.begin(), result.end(), warning) == result.end()) + result.push_back(warning); + return result; +} + +bool editor_mode::free_switch_bend(free_bend &Bend) const +{ + auto key{switch_memo_key()}; + if (false == m_bend_memo.fresh(key)) + { + free_bend bend; + auto const bent{free_switch_bend_now(bend)}; + m_bend_memo.keep(std::move(key), {bent, bend}); + } + Bend = m_bend_memo.result.second; + return m_bend_memo.result.first; +} + +bool editor_mode::free_switch_bend_now(free_bend &Bend) const +{ + auto const &tool{m_switch}; + Bend = {}; + if (false == tool.free || tool.free_end.track == nullptr || false == tool.free_heading.has_value() || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return false; + auto const &shape{tool.templates[tool.armed]}; + if (shape.twofold() || shape.double_slip || shape.radius < 1.0) + return false; + glm::dvec3 start; + glm::dvec2 heading; + int side; + double grade; + free_switch_pose(start, heading, side, grade); + // a free end the cursor is at, other than the one the switch goes on from + std::vector const exclude{tool.free_end.track}; + auto const &io{ImGui::GetIO()}; + if (false == m_cursor_override.has_value()) + Bend.target = snap_free_end(tool.mouse, {io.MousePos.x, io.MousePos.y}, nullptr, kRailCategory, exclude); + if (Bend.target.track == nullptr) + Bend.target = editor_track::find_free_end(nullptr, kRailCategory, tool.mouse, kLaySnapRadius, exclude); + auto const toward{plan_of((Bend.target.track != nullptr ? Bend.target.position : tool.mouse) - start)}; + auto const along{glm::dot(toward, heading)}; + // near the switch the drag gives the side of its diverging track, as of a switch laid straight; past its end it bends it + if (along < shape.length) + return false; + Bend.diverging = Bend.target.track != nullptr && io.KeyShift; + // the curvature, positive to the left: of the circle going on along the free end through the cursor, or into the straight of the + // free end it is at, one curve from the start of the switch + auto const lateral{cross(heading, toward)}; + auto curvature{2.0 * lateral / (along * along + lateral * lateral)}; + if (Bend.target.track != nullptr && glm::length(plan_of(Bend.target.direction)) > 1e-9) + { + auto const into{glm::normalize(plan_of(Bend.target.direction))}; + auto const turn{geometry::signed_angle(heading, into)}; + auto const determinant{cross(heading, into)}; + if (std::abs(turn) <= glm::radians(0.3)) + curvature = 0.0; + else if (std::abs(determinant) > 1e-9) + { + // how far ahead the two straights meet + auto const ahead{cross(plan_of(Bend.target.position - start), into) / determinant}; + if (ahead > 0.0) + curvature = std::copysign(std::tan(0.5 * std::abs(turn)) / ahead, turn); + } + // the diverging track into it: the switch bent so that its diverging track turns as much, the main track the other way where the + // curve is gentler than the one of the switch, the same way where it's sharper + if (Bend.diverging) + { + auto const way{curvature >= 0.0 ? 1 : -1}; + curvature = way * (std::abs(curvature) - 1.0 / shape.radius); + Bend.side = way; + } + } + // gentler than this it isn't bent, the drag giving the side as along the straight + constexpr double kStraighter{12000.0}; + if (std::abs(curvature) < 1.0 / kStraighter) + { + if (Bend.target.track == nullptr) + return false; + curvature = 0.0; + } + else if (Bend.target.track == nullptr) + { + // to the radii a designer takes, the nearest one within 5 %, else to 10 m + constexpr double kRadii[]{10000.0, 8000.0, 6000.0, 5000.0, 4000.0, 3500.0, 3000.0, 2500.0, 2000.0, 1800.0, 1500.0, 1400.0, 1200.0, 1100.0, 1000.0, 900.0, 800.0, 700.0, 600.0, 500.0, 400.0, 300.0}; + auto const radius{1.0 / std::abs(curvature)}; + auto chosen{std::round(radius / 10.0) * 10.0}; + auto closest{0.05}; + for (auto const typical : kRadii) + if (auto const off{std::abs(radius / typical - 1.0)}; off < closest) + { + closest = off; + chosen = typical; + } + curvature = std::copysign(1.0 / std::max(chosen, std::max(100.0, 0.5 * shape.radius)), curvature); + } + Bend.radius = curvature != 0.0 ? 1.0 / std::abs(curvature) : 0.0; + Bend.turn = curvature >= 0.0 ? 1 : -1; + if (false == Bend.diverging) + Bend.side = curvature == 0.0 ? side : tool.one_sided ? Bend.turn : -Bend.turn; + // the curve it's bent into, a little past its end + glm::dvec2 const normal{-heading.y, heading.x}; + auto const reach{shape.length + 2.0}; + int const steps{std::max(8, static_cast(std::ceil(reach / 0.5)))}; + for (int i = 0; i <= steps; ++i) + { + auto const station{reach * i / steps}; + auto const angle{curvature * station}; + auto const planar{curvature == 0.0 ? plan_of(start) + heading * station : plan_of(start) + (heading * std::sin(angle) + normal * (1.0 - std::cos(angle))) / curvature}; + curve_sample sample; + sample.station = station; + sample.position = {planar.x, start.y + grade * station, planar.y}; + sample.tangent = glm::normalize(heading * std::cos(angle) + normal * std::sin(angle)); + sample.radius = static_cast(Bend.radius); + Bend.frame.push_back(sample); + } + Bend.paths = curved_switch_paths(shape, Bend.frame, 0.0, 1, Bend.side); + if (Bend.paths.size() < 2) + return false; + Bend.report = curved_switch_report(Bend.paths, Bend.frame, 0.0, 1); + if (Bend.target.track != nullptr) + { + // the track into it goes on with the curve of the one it leaves, then straight, or with reverse curves + auto const &branch{Bend.paths[Bend.diverging ? 1 : 0]}; + auto const radius{editor_track::branch_radius(branch)}; + auto speed{design_speed(tool.free_end.track, Bend.target.track)}; + if (Bend.diverging) + speed = std::min(editor_track::diverging_speed(radius), speed); + Bend.link = editor_track::link_branch(branch_start(branch), radius > 0.5 ? radius : kStraighter, branch_target(Bend.target), glm::radians(m_joints.kink_angle() * 0.5), editor_track::reverse_insert(speed), stretchable(Bend.target)); + Bend.problem = link_problem(Bend.target, Bend.link); + } + return true; } bool editor_mode::plan_curved_slip(editor_track::switch_template const &Shape, int const Direction, int const Side, curved_slip &Plan) const @@ -1767,8 +2202,7 @@ bool editor_mode::plan_curved_slip(editor_track::switch_template const &Shape, i Plan.curve = curve; Plan.crossing = plan_of(sample.position); Plan.first = sample.tangent * static_cast(Direction); - glm::dvec2 const left{-Plan.first.y, Plan.first.x}; - Plan.second = Plan.first * std::cos(angle) + left * (static_cast(Side) * std::sin(angle)); + Plan.second = turned(Plan.first, static_cast(Side) * angle); auto const at = [](glm::dvec2 const &Planar) { return glm::dvec3{Planar.x, 0.0, Planar.y}; }; Plan.points = {at(Plan.crossing - Plan.first * half), at(Plan.crossing + Plan.first * half), at(Plan.crossing - Plan.second * half), at(Plan.crossing + Plan.second * half)}; return true; diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp index 3000006b..21624279 100644 --- a/application/editormodeswitchslide.cpp +++ b/application/editormodeswitchslide.cpp @@ -768,8 +768,7 @@ bool editor_mode::slip_room(slip_set const &Slip, slip_slide &Slide, std::string if (std::find(Slide.movers.begin(), Slide.movers.end(), other) != Slide.movers.end() || Slide.ends[end].first != nullptr) continue; Slide.ends[end] = {other, point}; - straight[end] = other->eType == tt_Normal && other->m_paths.size() == 1 && false == has_controls(other->m_paths.front()) && - std::abs(geometry::cross(glm::normalize(plan_of(other->m_paths.front().points[segment_data::point::end] - other->m_paths.front().points[segment_data::point::start])), plan_of(Slide.shift))) < 1e-3; + straight[end] = straight_along(*other); room[end] = straight[end] ? geometry::chord_length(other->m_paths.front()) - 1.0 : 0.0; } // back, it shortens the track before it and lengthens the one past it; on, the other way @@ -818,6 +817,166 @@ bool editor_mode::slide_slip(slip_set const &Slip, double const Distance) return true; } +bool editor_mode::bent_switch_frame(TTrack const &Track, std::vector &Frame, std::array &Sides, double &Station) const +{ + Frame.clear(); + Sides = {nullptr, nullptr}; + if (Track.eType != tt_Switch || Track.m_paths.size() != 2 || Track.m_editorremoved || false == has_controls(Track.m_paths.front()) || slip_of(Track).has_value() || double_of(Track).has_value() || + crossover_of(Track).has_value()) + return false; + // the plain tracks the main track goes on into, each run towards the end of the switch it joins: the one before into its start, the + // one past it out of its end + std::array forward{true, true}; + for (auto const &joint : editor_track::joints(Track)) + { + if (joint.own.path != 0 || joint.other == nullptr || joint.other->eType != tt_Normal || joint.other->m_editorremoved || joint.other->m_paths.size() != 1) + continue; + auto const past{joint.own.kind == editor_track::point_kind::end ? 1 : 0}; + Sides[past] = joint.other; + forward[past] = (joint.theirs.kind == editor_track::point_kind::end) == (past == 0); + } + if (Sides[0] == nullptr || Sides[1] == nullptr || Sides[0] == Sides[1]) + return false; + editor_track::chain chain; + chain.tracks = {Sides[0], const_cast(&Track), Sides[1]}; + chain.forward = {forward[0], true, forward[1]}; + Frame = sample_chain(chain); + Station = nearest_station(Frame, Track.m_paths.front().points[segment_data::point::start]); + return Frame.size() >= 2; +} + +std::vector editor_mode::bent_switch_paths(TTrack const &Track, std::vector const &Frame, double const Station, double const Distance) const +{ + // a path as seen from the curve where the switch starts: across it, over it, along it, as frame_path takes a path of a template + auto const local_end = [&](glm::dvec3 const &World, glm::dvec3 const &Control, glm::dvec3 &Point, glm::dvec3 &Localcontrol) { + auto const at{nearest_station(Frame, World)}; + auto const sample{sample_at(Frame, at)}; + glm::dvec2 const across{-sample.tangent.y, sample.tangent.x}; + auto const lateral{glm::dot(plan_of(World) - plan_of(sample.position), across)}; + Point = {lateral, World.y - sample.position.y + lateral * std::tan(glm::radians(static_cast(sample.roll))), at - Station}; + auto const planar{plan_of(Control)}; + Localcontrol = glm::length(planar) > 1e-9 ? glm::dvec3{glm::dot(planar, across), Control.y, glm::dot(planar, sample.tangent)} : glm::dvec3{0.0}; + }; + auto const localized = [&](segment_data const &Path) { + auto local{Path}; + local.pieces.clear(); + local_end(Path.points[segment_data::point::start], Path.points[segment_data::point::control1], local.points[segment_data::point::start], local.points[segment_data::point::control1]); + local_end(Path.points[segment_data::point::end], Path.points[segment_data::point::control2], local.points[segment_data::point::end], local.points[segment_data::point::control2]); + return local; + }; + std::vector result; + for (std::size_t i = 0; i < Track.m_paths.size(); ++i) + { + auto const &own{Track.m_paths[i]}; + auto path{frame_path(localized(own), Frame, Station + Distance, 1, 1)}; + for (auto const &piece : own.pieces) + path.pieces.push_back(frame_path(localized(piece), Frame, Station + Distance, 1, 1)); + // the main track follows the curve it lies in + if (i == 0) + path.radius = own.radius; + result.push_back(path); + } + return result; +} + +bool editor_mode::slide_bent_switch(TTrack &Track, double const Distance) +{ + std::vector frame; + std::array sides; + double station; + if (false == bent_switch_frame(Track, frame, sides, station)) + return false; + auto const &main{Track.m_paths.front()}; + auto const length{nearest_station(frame, main.points[segment_data::point::end]) - station}; + // as far as the far end of a track next to it, which goes then, the switch joining what it joined there: P4 of another switch, say + auto const back{station}; + auto const ahead{frame.back().station - station - length}; + if (Distance < -back - 1e-3 || Distance > ahead + 1e-3) + { + m_switch.status = format(STR_C("The switch goes %.2f m back and %.2f m on along the curve at most"), std::max(0.0, back), std::max(0.0, ahead)); + ui()->set_status(m_switch.status, true); + return false; + } + // a track shorter than this left beside it goes too + constexpr double kRemnant{0.5}; + auto distance{std::clamp(Distance, -back, ahead)}; + if (back + distance < kRemnant) + distance = -back; + else if (ahead - distance < kRemnant) + distance = ahead; + auto const paths{bent_switch_paths(Track, frame, station, distance)}; + auto const joints{editor_track::joints(Track)}; + std::vector handled(joints.size(), false); + // the line leaving the end of the diverging track: the curve past it laid again up to the straight beyond it + std::vector branches; + for (std::size_t i = 0; i < joints.size(); ++i) + if (joints[i].own.path == 1) + if (auto branch{take_switch_branch(Track, joints[i], i)}) + branches.push_back(std::move(*branch)); + editor_track::edit edit; + edit.capture(Track); + // the tracks of the curve on both sides laid along it up to the new ends of the switch + std::vector gone; + for (int k = 0; k < 2; ++k) + { + auto &side{*sides[k]}; + for (std::size_t i = 0; i < joints.size(); ++i) + if (joints[i].own.path == 0 && joints[i].other == &side) + handled[i] = true; + // taken up whole: the switch takes its place up to its far end + if ((k == 0 ? back + distance : ahead - distance) < 1e-3) + { + gone.push_back(side.name()); + editor_track::retire(side); + edit.removed.push_back(&side); + continue; + } + auto const joint{main.points[k == 0 ? segment_data::point::start : segment_data::point::end]}; + auto const &path{side.m_paths.front()}; + auto const atend{glm::distance(path.points[segment_data::point::end], joint) < glm::distance(path.points[segment_data::point::start], joint)}; + auto from{nearest_station(frame, path.points[segment_data::point::start])}; + auto to{nearest_station(frame, path.points[segment_data::point::end])}; + (atend ? to : from) = station + distance + (k == 0 ? 0.0 : length); + auto const a{sample_at(frame, from)}; + auto const b{sample_at(frame, to)}; + auto const way{to >= from ? 1.0 : -1.0}; + auto const span{std::abs(to - from)}; + auto const turn{std::abs(geometry::signed_angle(a.tangent, b.tangent))}; + auto const handle{turn > 1e-6 ? 4.0 / 3.0 * std::tan(turn / 4.0) * span / turn : span / 3.0}; + segment_data laid{path}; + laid.pieces.clear(); + laid.points[segment_data::point::start] = a.position; + laid.points[segment_data::point::end] = b.position; + // right at the new end of the switch + laid.points[atend ? segment_data::point::end : segment_data::point::start] = paths.front().points[k == 0 ? segment_data::point::start : segment_data::point::end]; + auto const rise{laid.points[segment_data::point::end].y - laid.points[segment_data::point::start].y}; + laid.points[segment_data::point::control1] = {a.tangent.x * way * handle, rise / 3.0, a.tangent.y * way * handle}; + laid.points[segment_data::point::control2] = {-b.tangent.x * way * handle, -rise / 3.0, -b.tangent.y * way * handle}; + laid.rolls = {static_cast(a.roll * way), static_cast(b.roll * way)}; + edit.capture(side); + side.m_paths.front() = laid; + } + Track.m_paths.assign(paths.begin(), paths.begin() + 2); + auto const notes{plan_switch_branches(Track, joints, branches, handled)}; + auto const loose{switch_rejoin(Track, joints, handled, edit)}; + editor_track::commit(edit.changed); + for (auto const &branch : branches) + lay_switch_branch(branch, edit); + push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed)); + switch_store::follow(); + records_to_history(); + straight_refresh(); + m_switch.status = format(STR_C("Switch %s moved %.2f m along the curve"), Track.name().c_str(), distance); + for (auto const &name : gone) + m_switch.status += "\n" + format(STR_C("%s taken up whole, the switch joins what it joined"), name.c_str()); + if (false == loose.empty()) + m_switch.status += "\n" + std::string{STR_C("Too far from the new ends of the switch, left unjoined: ")} + loose; + if (false == notes.empty()) + m_switch.status += "\n" + notes; + ui()->set_status(m_switch.status, false == loose.empty()); + return true; +} + bool editor_mode::slide_switch(TTrack &Track, double const Distance) { if (std::abs(Distance) < 0.005) @@ -828,6 +987,9 @@ bool editor_mode::slide_switch(TTrack &Track, double const Distance) // a switch of a crossover along a curve takes the crossover with it if (crossover_of(Track).has_value()) return slide_curved_connection(Track, Distance); + // one bent into a curve slides along the curve + if (Track.m_paths.size() == 2 && has_controls(Track.m_paths.front())) + return slide_bent_switch(Track, Distance); auto const set{double_of(Track)}; auto &first{set.has_value() ? *set->first : Track}; if (first.m_paths.size() < 2) @@ -874,7 +1036,15 @@ bool editor_mode::switch_slides(TTrack const &Track) const auto const set{double_of(Track)}; return set.has_value() && switch_slides(*set->first) && switch_slides(*set->second); } - return Track.eType == tt_Switch && Track.m_paths.size() == 2 && false == Track.m_editorremoved && false == has_controls(Track.m_paths.front()); + if (Track.eType != tt_Switch || Track.m_paths.size() != 2 || Track.m_editorremoved) + return false; + if (false == has_controls(Track.m_paths.front())) + return true; + // bent into a curve: along the curve, where it goes on into plain tracks on both sides + std::vector frame; + std::array sides; + double station; + return bent_switch_frame(Track, frame, sides, station); } void editor_mode::switch_slide_start(TTrack &Track) @@ -903,6 +1073,25 @@ void editor_mode::switch_slide_start(TTrack &Track) slide.high = room.high; return; } + // bent into a curve: along it, the tracks next to it keeping a metre, the curve past its diverging track drawn as it'll be laid + if (std::array sides; bent_switch_frame(Track, slide.frame, sides, slide.station)) + { + auto const &main{Track.m_paths.front()}; + auto const &mouse{ImGui::GetIO().MousePos}; + slide.active = true; + slide.track = &Track; + slide.pressed = {mouse.x, mouse.y}; + slide.grip = nearest_station(slide.frame, cursor_level(main.points[segment_data::point::start].y)); + slide.length = nearest_station(slide.frame, main.points[segment_data::point::end]) - slide.station; + slide.low = -slide.station; + slide.high = std::max(0.0, slide.frame.back().station - slide.station - slide.length); + auto const joints{editor_track::joints(Track)}; + for (std::size_t i = 0; i < joints.size(); ++i) + if (joints[i].own.path == 1) + if (auto branch{take_switch_branch(Track, joints[i], i)}) + slide.branches.push_back({joints[i], 1, std::move(*branch)}); + return; + } auto const set{double_of(Track)}; auto &first{set.has_value() ? *set->first : Track}; auto const axis{editor_track::main_axis(first)}; @@ -957,6 +1146,23 @@ void editor_mode::switch_slide_update() auto &slide{m_switch.slide}; if (false == slide.active) return; + // along the curve it's bent into + if (false == slide.frame.empty()) + { + if (slide.track == nullptr || slide.track->m_editorremoved) + { + slide = {}; + return; + } + auto const height{sample_at(slide.frame, slide.station).position.y}; + auto distance{std::clamp(nearest_station(slide.frame, cursor_level(height)) - slide.grip, slide.low, slide.high)}; + // drawn to the far ends of the tracks next to it, where it takes one up whole and joins what it joined + for (auto const end : {slide.low, slide.high}) + if (std::abs(distance - end) < 1.0) + distance = end; + slide.distance = distance; + return; + } auto const axis{slide.track != nullptr && false == slide.track->m_editorremoved && slide.track->m_paths.size() >= 2 ? editor_track::main_axis(*slide.track) : std::nullopt}; if (false == axis.has_value()) { @@ -999,7 +1205,7 @@ void editor_mode::draw_switch_slide() const auto const &mouse{ImGui::GetIO().MousePos}; if (glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) <= kDragPixels) return; - auto paths{editor_track::slid(*slide.track, slide.distance)}; + auto paths{slide.frame.empty() ? editor_track::slid(*slide.track, slide.distance) : bent_switch_paths(*slide.track, slide.frame, slide.station, slide.distance)}; if (paths.empty()) return; // a slip with all it takes along diff --git a/application/editormodeswitchtool.cpp b/application/editormodeswitchtool.cpp index 47ac9fbc..b20eaec1 100644 --- a/application/editormodeswitchtool.cpp +++ b/application/editormodeswitchtool.cpp @@ -142,7 +142,8 @@ std::string template_tooltip(editor_track::switch_template const &Template) if (Template.type == "Skrz") text += STR_C("Drag at a crossing of two straights"); else - text += Template.type == "Rkp" ? STR_C("Drag at a crossing of two straights at its skos, on the side of the tracks to join") : STR_C("Drag at a crossing of two straights at its skos"); + text += std::string{STR_C("Drag it out on one straight: its crossing track comes along at its skos, through the quarter of the cursor")} + "\n" + + (Template.type == "Rkp" ? STR_C("Or drag at a crossing of two straights at its skos, on the side of the tracks to join") : STR_C("Or drag at a crossing of two straights at its skos")); return text; } text += "\n" + format(STR_C("a %.3f m to the centre of the switch, b %.3f m on to its end"), Template.a, Template.b); @@ -231,7 +232,7 @@ void editor_mode::render_switch_ui() char const *tooltip; }; choice const choices[] = {{switch_kind::single, "Switch", "A switch in a straight or a curve"}, - {switch_kind::slip, "Slip switch", "Rkp or Rkpd at a crossing of two straights at its skos, or bent into a curve of a side track"}, + {switch_kind::slip, "Slip switch", "Rkp or Rkpd dragged out on one straight with its crossing track, at a crossing of two straights at its skos, or bent into a curve of a side track"}, {switch_kind::crossing, "Crossing", "A crossing of two straights"}, {switch_kind::connection, "Connection", "Crossover, double, scissors, ladder"}}; { @@ -277,7 +278,9 @@ void editor_mode::render_switch_ui() std::vector> how; switch (kind) { - case switch_kind::slip: how = {{"Drag at a crossing", "of two straights at the skos of the slip: it goes there"}, {"Drag on a curve", "bent into it, in a side track only (11.4 pkt 4b)"}}; break; + case switch_kind::slip: + how = {{"Drag on a straight", "the slip with its crossing track, through the quarter of the cursor"}, {"Drag at a crossing", "of two straights at the skos of the slip: it goes there"}, {"Drag on a curve", "bent into it, in a side track only (11.4 pkt 4b)"}}; + break; case switch_kind::crossing: how = {{"Drag at a crossing", "of two straights: it goes there"}}; break; case switch_kind::connection: how = {{"Drag", "from a straight onto the other track"}, {"Shift+wheel", "the kind"}, {"Ctrl+wheel", "R of the aligning curve"}}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 074c9e55..4090afe5 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -22,6 +22,7 @@ http: #include "editor/editorIncludeInfo.hpp" #include "utilities/Globals.h" +#include "utilities/Timer.h" #include "rendering/renderer.h" #include "world/Track.h" #include "world/TrackFeatures.h" @@ -479,7 +480,7 @@ std::array const &editor_mode::track_modes() {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters", editor_icons::icon::select, 0}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track", editor_icons::icon::lay_track, 0}, {"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too", editor_icons::icon::curve, 0}, - {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch", editor_icons::icon::turnout, 0}, + {"Switch", "T", track_tab::turnout, "Puts switches into a straight or a curve: press on it and drag along the way the switch goes. Edits the geometry of the selected switch", editor_icons::icon::turnout, 0}, {"Parallel", "N", track_tab::parallel, "A second track beside a track: LMB on it, then the side and the spacing", editor_icons::icon::parallel, 0}, {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it", editor_icons::icon::turntable, 1}, {"Signalling", "I", track_tab::srk, "The signals and the signalling of the stations: LMB on a switch lays its station out as a schematic, the signals placed by hand from the templates in the panel", editor_icons::icon::signal, 1}, @@ -508,7 +509,6 @@ void editor_mode::show_track_tab(track_tab const Wanted) m_lay.modes.clear(); m_lay.plan = {}; m_lay.start = {}; - m_lay.chain.clear(); m_lay.preview.clear(); m_lay.status.clear(); m_lay.active = Tab == track_tab::lay; @@ -574,6 +574,14 @@ bool editor_mode::track_shortcut(int const Key) m_tape = {false == m_tape.active, {}, m_tape.mouse}; return true; } + if (Key == GLFW_KEY_H) + { + auto *track{selected_track()}; + if (track == nullptr || track->eType != tt_Switch) + return false; + throw_switch(*track); + return true; + } auto const key{Key == GLFW_KEY_C ? GLFW_KEY_G : Key}; for (auto const &mode : track_modes()) { @@ -1332,6 +1340,28 @@ void editor_mode::render_turnout_ui() ImGui::SetTooltip("%s\n%s", STR_C("Moves the switch along its straight by the distance typed, + towards the frog; the straight paths on both sides get shorter or longer. Dragging the switch with the mouse does the same"), format(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - reach).c_str()); } + else if (false == set.has_value()) + { + // one bent into a curve slides along it, the tracks next to it keeping a metre; its curve found only while the settings are open + std::vector bentframe; + std::array bentsides{}; + double bentstation{0.0}; + if (bent_switch_frame(*track, bentframe, bentsides, bentstation)) + { + auto const along{nearest_station(bentframe, track->m_paths.front().points[segment_data::point::end]) - bentstation}; + auto const back{std::max(0.0, bentstation)}; + auto const ahead{std::max(0.0, bentframe.back().station - bentstation - along)}; + ImGui::PushItemWidth(100.0f); + ImGui::InputDouble("##switchslide", &tool.slide_typed, 0.0, 0.0, "%.2f m"); + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Slide along the curve"))) + slide_switch(*track, std::clamp(tool.slide_typed, -back, ahead)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s\n%s", STR_C("Moves the switch along the curve it's bent into by the distance typed, + towards the frog; the tracks of the curve on both sides get shorter or longer, one taken up whole goes and the switch joins what it joined, P4 of another switch say. Dragging the switch with the mouse does the same"), + format(STR_C("Room on the curve: %.2f m back, %.2f m ahead"), back, ahead).c_str()); + } + } ImGui::TreePop(); } ImGui::PopID(); @@ -1467,6 +1497,14 @@ void editor_mode::render_path_ui() show_track_tab(track_tab::turnout); item_tooltip("A switch: the turnout tool sets its geometry, side and drive"); } + if (track->eType == tt_Switch && track->SwitchExtension != nullptr) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Throw (H)"))) + throw_switch(*track); + item_tooltip(track->SwitchExtension->CurrentIndex ? "Set to the diverging track: throws it over to the main one. In the editor only, to see the blades and the route; the simulation starts with the switches set to the main track" + : "Set to the main track: throws it over to the diverging one. In the editor only, to see the blades and the route; the simulation starts with the switches set to the main track"); + } std::vector const tracks{track}; render_track_select_tools(tracks); render_track_terrain(tracks); @@ -1604,6 +1642,41 @@ std::string editor_mode::designs_trimmed(std::vector const &Removed) return cut > 0 ? std::string{STR_P("The design kept for the line is cut down to the tracks left along it", "The designs kept for the lines are cut down to the tracks left along them", static_cast(cut))} : std::string{}; } +void editor_mode::throw_switch(TTrack &Switch) +{ + if (Switch.eType != tt_Switch || Switch.SwitchExtension == nullptr) + return; + // as the simulation throws it, the blades going over at the speed of the switch, moved on frame by frame by move_thrown_switches. To see + // the blades and the route the paths make; the simulation starts with the switches set to the main track + auto const state{1 - (Switch.SwitchExtension->CurrentIndex & 1)}; + Switch.Switch(state); + if (std::find(m_thrown_switches.begin(), m_thrown_switches.end(), &Switch) == m_thrown_switches.end()) + m_thrown_switches.push_back(&Switch); + ui()->set_status(format(state ? STR_C("Switch %s set to the diverging track, H throws it back") : STR_C("Switch %s set to the main track, H throws it over"), Switch.name().c_str()), false); +} + +void editor_mode::move_thrown_switches() +{ + auto const elapsed{static_cast(Timer::GetDeltaRenderTime())}; + for (auto thrown{m_thrown_switches.begin()}; thrown != m_thrown_switches.end();) + { + auto *track{*thrown}; + if (track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr) + { + thrown = m_thrown_switches.erase(thrown); + continue; + } + auto &extension{*track->SwitchExtension}; + auto const left{extension.fDesiredOffset - extension.fOffset}; + auto const step{elapsed * std::max(extension.fOffsetSpeed, 0.01f)}; + auto const done{std::abs(left) <= step}; + extension.fOffset = done ? extension.fDesiredOffset : extension.fOffset + std::copysign(step, left); + // the blades laid again where they are; the animation of the cell, if it has the switch, finds it over and lets it go + track->RaAnimate(); + thrown = done ? m_thrown_switches.erase(thrown) : std::next(thrown); + } +} + void editor_mode::delete_selected_track() { auto *track{selected_track()}; @@ -1675,12 +1748,8 @@ void editor_mode::delete_selected_track() } editor_track::commit(created); // kept in the scenery as a crossing - auto const recorded = [](std::string const &Name) { - auto const &records{switch_store::records()}; - return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; }); - }; std::string base; - for (int i = 1; base.empty() || recorded(base); ++i) + for (int i = 1; base.empty() || switch_store::has(base); ++i) base = "editor_skrz" + std::to_string(i); record_slip(created, "crossing", "Skrz", 0.0, slip->record.ratio > 0.0 ? std::atan(1.0 / slip->record.ratio) : 0.0, base); ui()->set_status(STR_C("The slip is replaced by its two tracks crossing, Shift+Del removes all of it"), false); @@ -1744,19 +1813,19 @@ std::vector editor_mode::track_key_hints(bool const All) else if (m_lay.points.size() > 1) { // the points clicked are no track until Enter: it comes right after the click, where the bar at the bottom shows it - hints = {{"LMB", curve ? "a point along the middle of the curve, the last two on the straight after it" : "a bend of the straights, eased with a large curve"}, {"Enter", "lay the points clicked, then go on"}, + hints = {{"LMB", curve ? "a point along the middle of the curve, which ends at the last one" : "a bend of the straights, eased with a large curve"}, {"Enter", "lay the points clicked"}, {"Backspace", "take back the point"}, {"LMB on a free end", "join and lay"}, {"Tab", tab}, {"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"Esc", "twice: drop the points not laid"}}; } else { - hints = {{"LMB", curve ? "a point along the middle of the curve, the last two on the straight after it" : "a bend of the straights, eased with a large curve"}, {"LMB on a free end", "join and lay"}, {"Tab", tab}}; + hints = {{"LMB", curve ? "a point along the middle of the curve, which ends at the last one" : "a bend of the straights, eased with a large curve"}, {"LMB on a free end", "join and lay"}, {"Tab", tab}}; if (m_lay.mouse_snap.track != nullptr && m_lay.start.track != nullptr) hints.insert(hints.end(), {{"Ctrl+wheel", "radius"}, {"Shift+wheel", "another shape"}}); else if (m_lay.mouse_snap.track != nullptr && curve) hints.push_back({"Ctrl+wheel", "radius"}); hints.insert(hints.end(), {{"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"Enter", "stop laying"}, - {"Backspace", m_lay.chain.empty() ? "take back the point" : "take back the last piece"}, {"Esc", "stop laying"}}); + {"Backspace", "take back the point"}, {"Esc", "stop laying"}}); } } else if (false == m_straights.detour.empty()) @@ -1775,7 +1844,11 @@ std::vector editor_mode::track_key_hints(bool const All) hints.insert(hints.end(), {{"Release", "build"}, {"Esc", "cancel"}}); } else if (m_switch.placing && m_switch.free) - hints = m_switch.free_end.track != nullptr ? std::vector{{"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}} + hints = m_switch.free_end.track != nullptr ? std::vector{{"Drag sideways", "side of the diverging track"}, + {"Drag on past its end", "bends the main track toward the cursor"}, + {"Tab", "the diverging track the other way or the same way"}, + {"At a free end", "the main track leads into it, with Shift the diverging one"}, + {"Esc", "cancel"}} : std::vector{{"Drag along", "the direction of the main track, held from 4 m on"}, {"Drag sideways", "side of the diverging track"}, {"Back to the start", "the direction free again"}, {"Esc", "cancel"}}; else if (m_switch.placing && m_switch.curved && current_switch_kind() == switch_kind::slip) hints = {{"Drag", "the crossing track ahead through the quarter of the cursor, the slip curve of a single slip on its side"}, {"Esc", "cancel"}}; @@ -1790,9 +1863,15 @@ std::vector editor_mode::track_key_hints(bool const All) switch (current_switch_kind()) { case switch_kind::connection: hints = {{"Press on a straight", "and drag onto the other track"}, {"Shift+wheel", "another kind"}, {"Ctrl+wheel", "R of the aligning curve"}}; break; - case switch_kind::slip: hints = {{"Press+drag at a crossing", "of two straights"}, {"Press+drag on a curve", "the slip bent into it, in a side track only (11.4 pkt 4b)"}}; break; - case switch_kind::crossing: hints = {{"Press+drag at a crossing", "of two straights"}}; break; - default: hints = {{"LMB on a track", "the straight or the curve for the switch"}, {"Press+drag on the ground", "a switch with no track, on from a free end near it"}}; break; + case switch_kind::slip: + case switch_kind::crossing: + hints = {{"Press+drag at a crossing", "of two straights"}, {"Press+drag on a straight", "with nothing crossing it: the slip with its crossing track, which goes through the quarter of the cursor"}}; + if (current_switch_kind() == switch_kind::slip) + hints.push_back({"Press+drag on a curve", "the slip bent into it, in a side track only (11.4 pkt 4b)"}); + break; + default: + hints = {{"Press+drag on a track", "a switch on it: along its direction, sideways its side"}, {"Press+drag on the ground", "a switch with no track, on from a free end near it"}, {"LMB on a track", "selects it"}}; + break; } if (selected != nullptr) { @@ -1804,9 +1883,9 @@ std::vector editor_mode::track_key_hints(bool const All) else if (m_track_tab == track_tab::turnout && m_switch.armed < 0) hints = {{"List in the panel", "the type of the switch"}, {"LMB on a track", "another track"}}; else if (m_track_tab == track_tab::turnout && current_switch_kind() == switch_kind::slip) - hints = {{"Press+drag at a crossing", "of two straights at the skos of the slip"}, {"Press+drag on a curve", "the slip bent into it, in a side track only (11.4 pkt 4b)"}}; + hints = {{"Press+drag on a straight", "the slip with its crossing track, through the quarter of the cursor"}, {"Press+drag at a crossing", "of two straights at the skos of the slip"}, {"Press+drag on a curve", "the slip bent into it, in a side track only (11.4 pkt 4b)"}}; else if (m_track_tab == track_tab::turnout) - hints = {{"Press+drag on the track", "along: direction, sideways: side"}, {"List in the panel", "the type of the switch"}, {"LMB on another track", "choose it"}}; + hints = {{"Press+drag on the track", "along: direction, sideways: side"}, {"List in the panel", "the type of the switch"}, {"Press+drag on another track", "a switch on it"}}; else if (m_track_tab == track_tab::profile && m_profile.route.spans.empty()) hints = {{"LMB on a track", "the grade line of its whole line"}, {"Shift+LMB on more tracks", "the route between the outermost ones"}, {"Shift+drag a box", "the route over the tracks in it"}}; else if (m_track_tab == track_tab::profile) @@ -2390,6 +2469,53 @@ void editor_mode::draw_build_overlay() const } } }; + // the track being laid as its two rails: their axes 1.5 m apart, the gauge and the head of a rail, at the height of the path + auto const kRailAxes{1.5}; + auto const drawrails = [&](segment_data const &Path, ImU32 const Color) { + for (auto const &[colour, width] : {std::pair{IM_COL32(0, 0, 0, 170), 4.0f}, std::pair{Color, 2.0f}}) + for (auto const &piece : Path.pieces.empty() ? std::vector{Path} : Path.pieces) + { + bezier const curve{piece}; + auto const rail_at = [&](double const T, double const Side) { + auto const tangent{plan_of(curve.first(T))}; + if (glm::length(tangent) < 1e-9) + return curve.point(T); + auto const across{glm::normalize(glm::dvec2{-tangent.y, tangent.x}) * (Side * 0.5 * kRailAxes)}; + return curve.point(T) + glm::dvec3{across.x, 0.0, across.y}; + }; + for (auto const side : {-1.0, 1.0}) + { + auto previous{rail_at(0.0, side)}; + for (int i = 1; i <= 24; ++i) + { + auto const next{rail_at(i / 24.0, side)}; + projection.line(drawlist, previous, next, colour, width); + previous = next; + } + } + } + }; + // a circle on the ground as wide as the rails are apart, around the point; a few pixels across at least, seen from far + auto const drawwidth = [&](glm::dvec3 const &Point, ImU32 const Color) { + ImVec2 centre; + if (false == projection.project(Point, centre)) + return; + std::array outline; + auto seen{true}; + for (std::size_t i = 0; i < outline.size() && seen; ++i) + { + auto const angle{2.0 * glm::pi() * static_cast(i) / outline.size()}; + seen = projection.project(Point + glm::dvec3{std::cos(angle), 0.0, std::sin(angle)} * (0.5 * kRailAxes), outline[i]); + } + auto const dot{false == seen || std::hypot(outline[0].x - centre.x, outline[0].y - centre.y) < 4.0f}; + for (auto const &[colour, width] : {std::pair{IM_COL32(0, 0, 0, 170), 4.0f}, std::pair{Color, 2.0f}}) + { + if (dot) + drawlist->AddCircle(centre, 4.0f, colour, 16, width); + else + drawlist->AddPolyline(outline.data(), static_cast(outline.size()), colour, ImDrawFlags_Closed, width); + } + }; // a slip is selected whole: all its tracks with the one picked if (auto const *selected{selected_track()}; selected != nullptr && false == m_switch.slide.active) if (auto const slip{slip_of(*selected)}) @@ -2532,7 +2658,13 @@ void editor_mode::draw_build_overlay() const std::string text{plan.problem}; glm::dvec3 marker{cursor}; ImU32 colour{overlay_color::invalid}; - if (valid && plan.replaces != nullptr) + // with nothing crossing here the slip is dragged out with its crossing track + if (false == valid && plan.lone) + { + colour = overlay_color::highlight; + text = STR_C("Press and drag: the slip with its crossing track, through the quarter of the cursor"); + } + else if (valid && plan.replaces != nullptr) { marker = plan.replaces->location(); colour = overlay_color::highlight; @@ -2564,6 +2696,56 @@ void editor_mode::draw_build_overlay() const auto const preview{switch_preview()}; for (auto const &piece : preview) drawpath(piece, overlay_color::marked); + // the lines told of a switch bent, in a box beside the point + auto const readout_at = [&](glm::dvec3 const &Point, std::vector> const &Lines) { + ImVec2 screen; + if (false == projection.project(Point, screen)) + return; + auto const wrap{460.0f}; + auto height{0.0f}; + auto width{0.0f}; + for (auto const &[text, colour] : Lines) + { + auto const size{ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap)}; + height += size.y + 2.0f; + width = std::max(width, size.x); + } + ImVec2 corner{screen.x + 24.0f, screen.y + 24.0f}; + drawlist->AddRectFilled({corner.x - 6.0f, corner.y - 4.0f}, {corner.x + width + 6.0f, corner.y + height + 4.0f}, IM_COL32(10, 20, 30, 215), 4.0f); + for (auto const &[text, colour] : Lines) + { + drawlist->AddText(ImGui::GetFont(), ImGui::GetFontSize(), corner, colour, text.c_str(), nullptr, wrap); + corner.y += ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap).y + 2.0f; + } + }; + // a radius counted positive the way the line turns, the other way with a minus + auto const radius = [](double const Curvature) { return std::abs(Curvature) < 2e-5 ? std::string{STR_C("straight")} : format("%.0f m", 1.0 / Curvature); }; + // laid on from a free end: bent, the radius of the main track and the way of the diverging one, the track into the free end the + // cursor is at, the norms; from an end of a switch, the change of the curvature from it + if (m_switch.free && preview.size() >= 2) + { + // few lines: the radii, the track into the free end, the warnings; what the keys do the bar at the bottom tells + std::vector> lines; + if (free_bend bend; free_switch_bend(bend)) + { + if (bend.radius > 0.0) + lines.emplace_back(format(bend.side == bend.turn ? STR_C("one-sided: main R %.0f m, diverging R %.0f m") : STR_C("two-sided: main R %.0f m, diverging R %.0f m"), bend.radius, editor_track::branch_radius(bend.paths[1])), + IM_COL32(255, 170, 40, 255)); + if (bend.target.track != nullptr) + { + lines.emplace_back(bend.problem.empty() ? link_summary(bend.target, bend.link, editor_track::branch_radius(bend.paths[bend.diverging ? 1 : 0])) : bend.problem, bend.problem.empty() ? IM_COL32(255, 60, 255, 255) : overlay_color::invalid); + ImVec2 screen; + if (projection.project(bend.target.position, screen)) + drawlist->AddCircle(screen, 14.0f, bend.problem.empty() ? IM_COL32(255, 60, 255, 255) : overlay_color::invalid, 20, 3.0f); + } + for (auto const &warning : bend.report.warnings) + lines.emplace_back(warning, overlay_color::invalid); + } + for (auto const &warning : free_switch_exits(preview)) + lines.emplace_back(warning, overlay_color::invalid); + if (false == lines.empty()) + readout_at(m_switch.point, lines); + } // into a curve: the ends of the cant ramps, what the start was drawn to, the radii at the ends of the switch and the norms if (auto const &tool{m_switch}; tool.curved && tool.connection < 0 && preview.size() >= 2) { @@ -2601,9 +2783,7 @@ void editor_mode::draw_build_overlay() const else { station = curved_switch_start(forward, &snapped); - auto const report{curved_switch_report(preview, station, forward)}; - // a radius counted positive the way the line turns, the other way with a minus - auto const radius = [](double const Curvature) { return std::abs(Curvature) < 2e-5 ? std::string{STR_C("straight")} : format("%.0f m", 1.0 / Curvature); }; + auto const report{curved_switch_report(preview, m_switch.frame, station, forward)}; if (report.curved) { lines.emplace_back(format(STR_C("main track R %s at the start, %s at the end"), radius(report.main_start).c_str(), radius(report.main_end).c_str()), IM_COL32(255, 255, 255, 235)); @@ -2619,26 +2799,7 @@ void editor_mode::draw_build_overlay() const lines.emplace_back(note, IM_COL32(170, 200, 230, 235)); for (auto const &warning : warnings) lines.emplace_back(warning, overlay_color::invalid); - ImVec2 screen; - if (projection.project(sample_at(tool.frame, station).position, screen)) - { - auto const wrap{460.0f}; - auto height{0.0f}; - auto width{0.0f}; - for (auto const &[text, colour] : lines) - { - auto const size{ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap)}; - height += size.y + 2.0f; - width = std::max(width, size.x); - } - ImVec2 corner{screen.x + 24.0f, screen.y + 24.0f}; - drawlist->AddRectFilled({corner.x - 6.0f, corner.y - 4.0f}, {corner.x + width + 6.0f, corner.y + height + 4.0f}, IM_COL32(10, 20, 30, 215), 4.0f); - for (auto const &[text, colour] : lines) - { - drawlist->AddText(ImGui::GetFont(), ImGui::GetFontSize(), corner, colour, text.c_str(), nullptr, wrap); - corner.y += ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap).y + 2.0f; - } - } + readout_at(sample_at(tool.frame, station).position, lines); } switch_link plan; auto const linked{plan_switch_link(plan)}; @@ -2660,7 +2821,7 @@ void editor_mode::draw_build_overlay() const return piece_plan_radius(Path) < 0.5 ? IM_COL32(255, 255, 255, 230) : overlay_color::marked; }; for (auto const &piece : lay.preview) - drawpath(piece, piece_colour(piece)); + drawrails(piece, piece_colour(piece)); auto const label_at = [&](glm::dvec3 const &Point, std::string const &Text, ImU32 const Colour) { ImVec2 screen; if (false == projection.project(Point, screen)) @@ -2743,7 +2904,13 @@ void editor_mode::draw_build_overlay() const ImVec2 screen; if (false == projection.project(lay.points[i], screen)) continue; - if (i > 0 && i < lay.modes.size() && lay.modes[i] == lay_mode::curve) + // the start of the line as wide as the track + if (i == 0) + { + drawwidth(lay.points[i], overlay_color::grip); + continue; + } + if (i < lay.modes.size() && lay.modes[i] == lay_mode::curve) { auto const off{departure(lay.points[i])}; auto const colour{off < 0.0 ? overlay_color::grip : off <= 0.1 ? IM_COL32(90, 220, 110, 255) : off <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255)}; diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index 9a239391..5d03a701 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -102,6 +102,14 @@ inline std::string curve_label(alignment::curve_report const &Curve, alignment:: return text + speed_of(curve_sharpest(Curve), Limits); } +inline std::string lines_of(std::vector const &Texts) +{ + std::string text; + for (auto const &line : Texts) + text += (text.empty() ? "" : "\n") + line; + return text; +} + // how a curve laid goes on from the curve of the track it starts at, or into the one of the track it ends at; empty from a straight inline std::string curve_goes_on(alignment::curve_report const &Curve) { @@ -112,10 +120,7 @@ inline std::string curve_goes_on(alignment::curve_report const &Curve) if (Curve.curvature_out > 0.0) lines.push_back(Curve.transition_out < 1.0 ? format(STR_C("goes into R %.0f m of the track it ends at"), 1.0 / Curve.curvature_out) : format(STR_C("goes into R %.0f m of the track it ends at, a transition of %.0f m between them"), 1.0 / Curve.curvature_out, Curve.transition_out)); - std::string text; - for (auto const &line : lines) - text += (text.empty() ? "" : "\n") + line; - return text; + return lines_of(lines); } // the transition curves and the cant the norms put in a layout, a compound curve where it goes on from an arc turning the same way @@ -161,13 +166,6 @@ inline std::string vertical_of(std::vector const &Pieces) return format(STR_C("height %+.2f m grade %+.1f to %+.1f per mille"), rise, low, high); } -inline std::string lines_of(std::vector const &Texts) -{ - std::string text; - for (auto const &line : Texts) - text += (text.empty() ? "" : "\n") + line; - return text; -} inline std::string narrowed(editor_track::layout const Layout, double const Radius, double const Arc) { @@ -324,13 +322,15 @@ inline void compound_from(editor_track::curve const &Curve, alignment::vertex &V { if (false == Curve.compound || Curve.arcs.size() < 2) return; + // the radii and the transitions read off the paths rounded, as they'd be designed; read from the designs they were laid by, as they are + auto const value = [&](double const Value) { return Curve.laid ? Value : std::round(Value); }; Vertex.compound = true; - Vertex.radius = std::round(Curve.arcs.front().radius); + Vertex.radius = value(Curve.arcs.front().radius); Vertex.share = Curve.arcs.front().turn; Vertex.first_cant = -1.0; Vertex.arcs.clear(); for (std::size_t i = 1; i < Curve.arcs.size(); ++i) - Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); + Vertex.arcs.push_back({value(Curve.arcs[i].radius), value(Curve.arcs[i].transition), Curve.arcs[i].turn}); } inline char const *type_name(TTrack const &Track) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 1beebf86..afa7a02a 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -204,7 +204,7 @@ void editor_mode::draw_track_lines() const auto const metres{ortho_metres_per_pixel()}; auto const colouring{ui()->mode() == nodebank_panel::TRACK ? m_track_colouring : track_colouring::plain}; // the top view of the rails alone draws them in a colour of their own, as on a plan: both rails where they're apart on the screen - auto const rails{metres > 0.0 && (Global.EditorOrthoTracks > 1 || Global.EditorOrthoMap) && colouring == track_colouring::plain}; + auto const rails{metres > 0.0 && Global.EditorOrthoTracks > 1 && colouring == track_colouring::plain}; if (metres < kLinesFrom && colouring == track_colouring::plain && false == rails) return; auto const &io{ImGui::GetIO()}; @@ -812,11 +812,23 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) return make(kind::straight_tool, m_straights.tool == 1 ? "Press: break the straight here, drag to turn the rest" : "Click: start of the shift"); } } + // a switch armed, at a free end of a switch, P4 of its diverging track or the end of its main one: the switch goes on from there at + // once, as from the free end of a plain track + if (m_track_tab == track_tab::turnout && m_hover.track != nullptr && m_hover.track->eType == tt_Switch && free_switch_offered()) + if (auto const end{free_switch_end(ground)}; end.track == m_hover.track && geometry::plan_distance(end.position, ground) <= 3.0) + { + intent.position = end.position; + return make(kind::switch_free, "Press and drag: a switch going on from this end of the switch, sideways its side"); + } if ((m_track_tab == track_tab::turnout || m_track_tab == track_tab::path) && m_hover.track != nullptr && switch_slides(*m_hover.track)) { intent.track = m_hover.track; intent.path = m_hover.path; intent.position = m_hover.point; + // one bent into a curve slides along the curve + auto const bent{m_hover.track->eType == tt_Switch && m_hover.track->m_paths.size() == 2 && geometry::has_controls(m_hover.track->m_paths.front())}; + if (bent) + return make(kind::switch_slide, m_track_tab == track_tab::path ? "Click: select this switch, drag: slide it along the curve" : "Click: edit this switch, drag: slide it along the curve"); return make(kind::switch_slide, m_track_tab == track_tab::path ? "Click: select this switch, drag: slide it along the straight" : "Click: edit this switch, drag: slide it along the straight"); } if (selected != nullptr && false == selected->m_paths.empty()) @@ -829,6 +841,15 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) return make(kind::switch_at, "Press and drag: a switch here, along sets its direction, sideways its side"); } } + // a press on another plain track takes it for the switch at once, with no click to select it first; a click only selects it + if (m_track_tab == track_tab::turnout && m_switch.armed >= 0 && m_hover.track != nullptr && m_hover.track != selected && m_hover.track->eType == tt_Normal && + false == m_hover.track->m_paths.empty()) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + return make(kind::switch_at, "Press and drag: a switch on this track, along sets its direction, sideways its side. Click: select the track"); + } // a press on the ground lays a switch with no track there, going on from a free end within reach if (m_hover.track == nullptr && free_switch_offered()) { @@ -1032,6 +1053,9 @@ void editor_mode::track_press(track_intent const &Intent) return; case kind::switch_at: { + // the press took a track not selected for the switch: let go with no drag it only selects the track + if (Intent.track != nullptr && Intent.track != selected_track()) + select_track(Intent.track); auto const &io{ImGui::GetIO()}; m_switch.pressed_at = {io.MousePos.x, io.MousePos.y}; start_switch_placement(); @@ -1298,6 +1322,8 @@ void editor_mode::render_track_context() ImGui::Separator(); if (ImGui::MenuItem(STR_C("Switch"), "T", false, normal || track->eType == tt_Switch)) show_track_tab(track_tab::turnout); + if (track->eType == tt_Switch && track->SwitchExtension != nullptr && ImGui::MenuItem(STR_C("Throw it over"), "H")) + throw_switch(*track); if (ImGui::MenuItem(STR_C("Plan"), "G", false, normal)) show_track_tab(track_tab::straights); if (ImGui::MenuItem(STR_C("Parallel track"), "N", false, normal)) diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index d06ace70..dbd34273 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -236,8 +236,8 @@ void editor_mode::work_area_tools(std::vector &Buttons) choose(terrain_tool::fill)}); auto const orthophoto{m_orthophoto.enabled()}; Buttons.push_back({icon::orthophoto, STR_C("Orthophoto"), nullptr, STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery"), orthophoto, - [this, orthophoto]() { m_orthophoto.enabled(false == orthophoto); }, true}); - auto const advanced{m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::restore || m_orthophoto_window}; + [this, orthophoto]() { choose_orthophoto(false == orthophoto); }, true}); + auto const advanced{m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::restore || orthophoto_settings_open()}; Buttons.push_back({icon::more, terrain_tools_more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Point spacing, restoring the terrain, orthophoto settings"), terrain_tools_more, []() { terrain_tools_more = false == terrain_tools_more; }, true}); if (false == terrain_tools_more && false == advanced) @@ -246,7 +246,11 @@ void editor_mode::work_area_tools(std::vector &Buttons) chosen(terrain_tool::spacing), choose(terrain_tool::spacing), true}); Buttons.push_back({icon::joints, STR_C("Restore terrain"), nullptr, STR_C("LMB takes back the touch-ups made over the tracks: the terrain returns to the shape the tracks give it"), chosen(terrain_tool::restore), choose(terrain_tool::restore)}); - Buttons.push_back({icon::count, STR_C("Orthophoto settings..."), nullptr, nullptr, m_orthophoto_window, [this]() { m_orthophoto_window = !m_orthophoto_window; }}); + Buttons.push_back({icon::count, STR_C("Orthophoto settings..."), nullptr, nullptr, orthophoto_settings_open(), [this]() { + // among the base layers in the layers window + ui()->show_layers(); + m_orthophoto_window = true; + }}); break; } } diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f058e629..e4b07743 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -112,6 +112,13 @@ class editor_ui : public ui_layer void set_file_actions(std::function Save, std::function Export); void set_new_scenery(std::function New) { m_newscenery = std::move(New); } void set_open_scenery(std::function Open) { m_openscenery = std::move(Open); } + // draws the base layers under the scenery at the bottom of the layers window (they live in the editor mode) + void set_base_layers(std::function Renderer) { m_layerspanel.base_layers = std::move(Renderer); } + void show_layers() + { + m_layerspanel.is_open = true; + m_layerspanel.focus = true; + } // part of the main window between the docked windows, where the 3d view is seen; for the overlays placed in it static glm::vec2 view_min() { diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index ed41bbf7..da3b5674 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -1828,6 +1828,11 @@ void layers_panel::render() ImGui::SetNextWindowPos(ImVec2S(520, 60), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2S(440, 340), ImGuiCond_FirstUseEver); auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name}; + if (focus) + { + ImGui::SetNextWindowFocus(); + focus = false; + } if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) { if (scene::Layers.empty()) @@ -2017,6 +2022,14 @@ void layers_panel::render_list() } ImGui::PopID(); } + // under the layers of the scenery what lies under it + if (base_layers) + { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::TextDisabled("%s", STR_C("Base layers, under the scenery")); + base_layers(); + } ImGui::EndChild(); } diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 4b00b130..1b39e9a2 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -226,6 +226,11 @@ class layers_panel : public ui_panel // saves the scenery; provided by the editor mode std::function save; + // the base layers under the scenery, the orthophoto and the WMS layers of the user's, with their settings; provided by the + // editor mode + std::function base_layers; + // the window brought to the front the next time it's drawn + bool focus{false}; // outcome of the most recent operation, shown at the bottom of the window std::string status; bool status_error{false}; diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 3844b618..525ab015 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -720,6 +720,10 @@ result fit_between(design const &Design) fit.compound = true; fit.curved = true; fit.deflection = deflection; + // going on from the diverging track of a switch: at its radius, no transition between them + auto const held{from ? Design.start_held : Design.end_held}; + if (held) + (from ? fit.transition_in : fit.transition_out) = 0.0; fit.arcs = {{fit.radius, 0.0, 1.0, -1.0}}; if (vertex.compound && false == vertex.arcs.empty()) { @@ -727,6 +731,8 @@ result fit_between(design const &Design) for (auto const &arc : vertex.arcs) fit.arcs.push_back({std::max(1.0, arc.radius), std::max(0.0, arc.transition), arc.share, arc.cant}); } + if (held) + (from ? fit.arcs.front() : fit.arcs.back()).radius = from ? Design.start_radius : Design.end_radius; if (from) { fit.ends.from = 1.0 / Design.start_radius; @@ -743,11 +749,24 @@ result fit_between(design const &Design) fit.arcs.back().cant = fit.ends.to_cant; } auto const normalin{perpendicular(directionin) * static_cast(side)}; - auto const tangents_at = [&](double const Scale, double &Tangentin, double &Tangentout) { + // the size of the curve which begins it right at the start, or ends it at the end: the scale of its radii, the arc held at the + // radius of a switch kept; of a single arc held so the length of the transition on its other side + auto const lengthened{held && fit.arcs.size() == 1}; + auto &beyond{from ? fit.transition_out : fit.transition_in}; + auto const sized = [&](double const Value, std::vector &Arcs) { + for (std::size_t i = 0; i < Arcs.size(); ++i) + if (false == held || i != (from ? 0 : Arcs.size() - 1)) + Arcs[i].radius *= Value; + }; + auto const tangents_at = [&](double const Value, double &Tangentin, double &Tangentout) { auto arcs{fit.arcs}; - for (auto &arc : arcs) - arc.radius *= Scale; - auto const shape{build_compound(deflection, arcs, fit.transition_in, fit.transition_out, {}, fit.ends)}; + auto in{fit.transition_in}; + auto out{fit.transition_out}; + if (lengthened) + (from ? out : in) = Value; + else + sized(Value, arcs); + auto const shape{build_compound(deflection, arcs, in, out, {}, fit.ends)}; auto const end{directionin * shape.end.x + normalin * shape.end.y}; Tangentin = cross(directionout, end) / cross(directionout, directionin); Tangentout = glm::dot(end - directionin * Tangentin, directionout); @@ -761,14 +780,23 @@ result fit_between(design const &Design) return (from ? in : out) - wanted; }; // the radii grow with the tangent: from those given the way the miss says, doubled or halved until it changes its sign, then - // the root between, by the regula falsi of Illinois - auto low{1.0}; + // the root between, by the regula falsi of Illinois. The transition out of an arc held moves it in, the tangent growing with it: + // from none, doubled up to one turning by nearly all the deflection + auto low{lengthened ? 0.0 : 1.0}; auto lowmiss{miss(low)}; auto high{low}; auto highmiss{lowmiss}; auto const smallest{5.0 / fit.radius}; auto const largest{1e6 / fit.radius}; - while ((lowmiss < 0.0) == (highmiss < 0.0) && high < largest && low > smallest) + auto const longest{1.96 * fit.arcs.front().radius * deflection}; + while (lengthened && (lowmiss < 0.0) == (highmiss < 0.0) && lowmiss < -1e-5 && high < longest) + { + low = high; + lowmiss = highmiss; + high = std::min(longest, std::max(1.0, 2.0 * high)); + highmiss = miss(high); + } + while (false == lengthened && (lowmiss < 0.0) == (highmiss < 0.0) && high < largest && low > smallest) { if (lowmiss < 0.0) { @@ -785,14 +813,16 @@ result fit_between(design const &Design) lowmiss = miss(low); } } - if ((lowmiss < 0.0) == (highmiss < 0.0)) + // an arc held which begins right there with no transition out of it + auto const met{lengthened && std::abs(lowmiss) <= 1e-5}; + if (false == met && (lowmiss < 0.0) == (highmiss < 0.0)) { r.errors.emplace_back(from ? format(STR_C("Vertex %zu: no curve goes on from the curve of the adjoining track (R %.0f m) to the straight after it"), k + 1, Design.start_radius) : format(STR_C("Vertex %zu: no curve goes on from the straight before it into the curve of the adjoining track (R %.0f m)"), k + 1, Design.end_radius)); return r; } auto scale{low}; - for (int i = 0, kept = 0; i < 60; ++i) + for (int i = 0, kept = 0; i < 60 && false == met; ++i) { scale = (low * highmiss - high * lowmiss) / (highmiss - lowmiss); auto const value{miss(scale)}; @@ -815,10 +845,12 @@ result fit_between(design const &Design) kept = 1; } } - for (auto &arc : fit.arcs) - arc.radius *= scale; + if (lengthened) + beyond = scale; + else + sized(scale, fit.arcs); fit.radius = fit.arcs.front().radius; - tangents_at(1.0, fit.tangent_in, fit.tangent_out); + tangents_at(lengthened ? beyond : 1.0, fit.tangent_in, fit.tangent_out); continue; } if (vertex.compound && false == vertex.arcs.empty()) @@ -1294,15 +1326,8 @@ result fit_between(design const &Design) auto const &e{r.elements[End ? r.elements.size() - 1 - n : n]}; if (e.length < 0.01) continue; - auto const arc{e.radius > 0.0 ? 1.0 / e.radius : 0.0}; - switch (e.kind) - { - case element_kind::arc: return arc * e.turn; - case element_kind::transition_in: return End ? arc * e.turn : 0.0; - case element_kind::transition_out: return End ? 0.0 : arc * e.turn; - case element_kind::spiral: return (End ? e.curvature_end : e.curvature_start) * e.turn; - default: return 0.0; - } + auto const [start, end]{end_curvatures(e)}; + return (End ? end : start) * e.turn; } return 0.0; }; @@ -1333,6 +1358,19 @@ glm::dvec3 along_end(glm::dvec3 const &Point, glm::dvec2 const &Direction, doubl } // namespace +std::pair end_curvatures(element const &Element) +{ + auto const own{Element.radius > 0.0 ? 1.0 / Element.radius : 0.0}; + switch (Element.kind) + { + case element_kind::transition_in: return {0.0, own}; + case element_kind::arc: return {own, own}; + case element_kind::transition_out: return {own, 0.0}; + case element_kind::spiral: return {Element.curvature_start, Element.curvature_end}; + default: return {0.0, 0.0}; + } +} + result compute(design const &Design) { auto first{fit_between(Design)}; @@ -1372,7 +1410,7 @@ double speed_limit(double const Radius, limits const &Limits) return std::sqrt(Radius * (Limits.cant_max + allowance) / factor); } -result compute_recommended(design &Design, bool const Transitions) +result compute_recommended(design &Design) { for (auto &vertex : Design.vertices) { @@ -1381,7 +1419,7 @@ result compute_recommended(design &Design, bool const Transitions) vertex.transition_out = 0.0; } auto r{compute(Design)}; - if (false == Transitions || false == r.valid) + if (false == r.valid) return r; auto const speed{Design.speed}; for (int i = 0; i < 3; ++i) @@ -1540,7 +1578,7 @@ design join_design(join_frame const &Frame, join_kind const Kind) std::optional fitted_join(join_frame const &Frame, join_kind const Kind) { auto shape{join_design(Frame, Kind)}; - auto const r{compute_recommended(shape, Frame.request.transitions)}; + auto const r{compute_recommended(shape)}; auto const sharp{std::any_of(r.curves.begin(), r.curves.end(), [](curve_report const &Report) { return Report.radius < 10.0; })}; if (false == r.valid || sharp || turning(r) > Frame.heading + kPi + 1e-6 || r.length > 3.0 * Frame.distance + 1.0) return std::nullopt; @@ -2433,6 +2471,8 @@ constexpr double kTraceRobust{0.25}; constexpr double kPointsWithin{0.3}; // m, how far they may be from it, clicked by hand far out: a row of bends making a curve no closer is cut in parts constexpr double kPointsLoose{1.0}; +// m, the shortest arc of a compound curve fitted through points: a shorter one is a kink the points made, no arc +constexpr double kShortestArc{10.0}; // the parameters: the logarithms of the radii, then the weights of the shares of all but the first radius, as logarithms too. // Between: the transitions between the arcs those of the norms, else the ones of the base @@ -2567,8 +2607,14 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< bestcost = cost; bestlog = low + spacing * i; } - if (bestcost == std::numeric_limits::max()) + // two points for each radius; a curve going on from the one of the adjoining track has its start, its curvature there given too, and + // where one radius doesn't reach the end from there two are tried: a sharper one after the one it goes on from + auto const going{Design.start_curved || Design.end_curved}; + auto const alone{bestcost < std::numeric_limits::max()}; + auto const most{std::clamp(Radii > 0 ? Radii : 3, 1, std::max(going && false == alone ? 2 : 1, static_cast(Points.size()) / 2))}; + if (false == alone && most < 2) return best; + if (alone) { constexpr double kGolden{0.6180339887498949}; auto a{bestlog - spacing}; @@ -2607,7 +2653,6 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< }; std::array fits; fits[1] = {{bestlog}, cost_of(one, {bestlog})}; - auto const most{std::clamp(Radii > 0 ? Radii : 3, 1, std::max(1, static_cast(Points.size()) / 2))}; auto const shift{std::log(1.3)}; auto const share = [](double const Second, double const First) { return std::log(Second / First); }; auto const rms = [&](int const Count) { return std::sqrt(fits[Count].cost / static_cast(Points.size())); }; @@ -2624,13 +2669,18 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< if (wanted(2)) { auto const two{residuals(2)}; - for (auto const fraction : {0.3, 0.5, 0.7}) - for (auto const sign : {-1.0, 1.0}) - { - std::vector parameters{bestlog + sign * shift, bestlog - sign * shift, share(1.0 - fraction, fraction)}; - if (auto const cost{least_squares(parameters, two)}; cost < fits[2].cost) - fits[2] = {parameters, cost}; - } + // radii far apart too where the curve goes on from another one + std::vector shifts{shift}; + if (going) + shifts.insert(shifts.end(), {std::log(3.0), std::log(8.0)}); + for (auto const apart : shifts) + for (auto const fraction : {0.3, 0.5, 0.7}) + for (auto const sign : {-1.0, 1.0}) + { + std::vector parameters{bestlog + sign * apart, bestlog - sign * apart, share(1.0 - fraction, fraction)}; + if (auto const cost{least_squares(parameters, two)}; cost < fits[2].cost) + fits[2] = {parameters, cost}; + } take(2); } if (wanted(3)) @@ -3361,6 +3411,33 @@ double shortest_between(design const &Design) } // namespace +void settle_laid(std::vector &Vertices, result const &Laid) +{ + for (auto const &curve : Laid.curves) + { + // a curve going on from an adjoining one is begun by that one's radius, not made smaller + if (curve.vertex < 0 || curve.vertex >= static_cast(Vertices.size()) || curve.curvature_in > 0.0 || curve.curvature_out > 0.0) + continue; + auto &vertex{Vertices[curve.vertex]}; + // two transitions meeting have their lengths from the radius they meet at + auto const transitions{vertex.spirals || (curve.transition_in >= vertex.transition_in - 1e-6 && curve.transition_out >= vertex.transition_out - 1e-6)}; + if (curve.radius >= vertex.radius * (1.0 - 1e-6) && transitions) + continue; + auto const scale{curve.radius / std::max(1.0, vertex.radius)}; + vertex.radius = curve.radius; + for (auto &arc : vertex.arcs) + { + arc.radius *= scale; + arc.transition *= scale; + } + if (false == vertex.spirals) + { + vertex.transition_in = curve.transition_in; + vertex.transition_out = curve.transition_out; + } + } +} + result ease_corners(design &Design, std::vector &Corners, corner_radii const &Radii, stretch_fit const &Fit) { // a point following a slight bend clicked closer than the curves leave a straight of the shortest length was clicked on a curve @@ -3483,6 +3560,8 @@ result ease_corners(design &Design, std::vector &Corners, corner_radii con break; line = ease_bends(Design, Corners, Radii); } + if (line.valid) + settle_laid(Design.vertices, line); return line; } @@ -3513,6 +3592,7 @@ std::optional stretch_of(design const &Design, std::vector= needed - 0.5) + break; + auto const between{parameter_of(needed)}; + residuals_of const rest{[&](std::vector const &Parameters, std::vector &Residuals) { return residuals({between, Parameters[0]}, Residuals); }}; + std::vector out{best[1]}; + if (least_squares(out, rest) == std::numeric_limits::max()) + break; + best = {between, out[0]}; + } if (best.empty()) return; design fitted; @@ -3747,38 +3847,70 @@ void fit_curve(design &Design, std::size_t const Vertex, std::vector auto const speed{std::max(1.0, Design.speed)}; // the transitions together turn no more than the curve: longer ones would only be cut to it auto const deflection{vertex_deflection(Design, Vertex)}; + // the transition into the curve and the one out of it each as long as the points show it, where there are points enough for both: a + // curve begun short after a free end and running out long into the straight after it is one curve, not a sharp arc with a flat + // one after it + auto const apart{Points.size() >= 4}; auto const shaped = [&](std::vector const &Parameters, design &Trial) { Trial = Design; auto const radius{std::exp(std::clamp(Parameters[0], std::log(kTraceRadiusMin), std::log(kTraceRadiusMax)))}; - plain_curve(Trial.vertices[Vertex], radius, std::min({Parameters[1] * Parameters[1], radius * deflection, longest_transition(speed, radius, Trial.norms)}), speed, Trial.norms); + auto const longest{longest_transition(speed, radius, Trial.norms)}; + auto in{std::min(Parameters[1] * Parameters[1], longest)}; + auto out{std::min(Parameters.back() * Parameters.back(), longest)}; + if (auto const room{2.0 * radius * deflection}; in + out > room) + { + auto const scale{room / (in + out)}; + in *= scale; + out *= scale; + } + auto &curve{Trial.vertices[Vertex]}; + plain_curve(curve, radius, std::min(in, out), speed, Trial.norms); + curve.transition_in = in; + curve.transition_out = out; }; residuals_of const residuals{[&](std::vector const &Parameters, std::vector &Residuals) { design trial; shaped(Parameters, trial); return stretch_residuals(trial, Points, Residuals); }}; - // from the transitions it has and from those of the norms; the square root of the length, as a length of nought would hold the - // step at nought + // from the transitions it has and from those of the norms; the square root of the length, as a length of nought would hold the step + // at nought auto const radius{std::max(kTraceRadiusMin, Design.vertices[Vertex].radius)}; auto const allowed{std::max(1.0, std::min(speed, speed_limit(radius, Design.norms)))}; + // started below the longest transition: above it the length is held, the fit no longer moving it + auto const started = [&](double const Length) { return std::sqrt(std::max(std::min(Length, 0.9 * std::min(radius * deflection, longest_transition(speed, radius, Design.norms))), 1.0)); }; + auto const norm{std::max(recommend(allowed, radius, Design.norms).transition, 10.0)}; + auto const fitted_from = [&](std::vector> const &Starts, std::vector &Best) { + auto lowest{std::numeric_limits::max()}; + for (auto parameters : Starts) + if (auto const cost{least_squares(parameters, residuals)}; cost < lowest) + { + lowest = cost; + Best = parameters; + } + return lowest; + }; std::vector best; - auto lowest{std::numeric_limits::max()}; - for (auto const length : {std::max(Design.vertices[Vertex].transition_in, 10.0), std::max(recommend(allowed, radius, Design.norms).transition, 10.0)}) + auto const alike{fitted_from({{std::log(radius), started(std::max(Design.vertices[Vertex].transition_in, 10.0))}, {std::log(radius), started(norm)}}, best)}; + // the two apart, from the ones alike and from one short and the other long: taken where they keep to the points clearly closer, not + // only to points clicked a little off + constexpr double kApart{0.8}; + if (apart && false == best.empty()) { - // started below the longest transition: above it the length is held, the fit no longer moving it - std::vector parameters{std::log(radius), std::sqrt(std::min(length, 0.9 * std::min(radius * deflection, longest_transition(speed, radius, Design.norms))))}; - if (auto const cost{least_squares(parameters, residuals)}; cost < lowest) - { - lowest = cost; - best = parameters; - } + std::vector two; + if (fitted_from({{best[0], best[1], best[1]}, {std::log(radius), started(10.0), started(norm)}, {std::log(radius), started(norm), started(10.0)}}, two) < kApart * alike) + best = two; } if (best.empty()) return; design fitted; shaped(best, fitted); auto &curve{fitted.vertices[Vertex]}; - plain_curve(curve, std::max(kTraceRadiusMin, std::round(curve.radius)), std::round(curve.transition_in), speed, fitted.norms); + auto const in{std::round(curve.transition_in)}; + auto const out{std::round(curve.transition_out)}; + plain_curve(curve, std::max(kTraceRadiusMin, std::round(curve.radius)), std::min(in, out), speed, fitted.norms); + curve.transition_in = in; + curve.transition_out = out; Design = fitted; } @@ -3931,7 +4063,7 @@ std::optional fit_arcs(design const &Single, std::vector 0.0 && Element.length < 10.0; + return Element.vertex == 0 && Element.curvature_start == Element.curvature_end && Element.curvature_start > 0.0 && Element.length < kShortestArc; })) return std::nullopt; auto const stretch{stretch_of(Shaped, Points)}; @@ -4108,30 +4240,22 @@ std::optional fit_chain(design const &Base, std::vector vertices; double first; double along; - if (false == crossing(start, startdirection, origins.front(), directions.front(), first, along) || first <= 0.0) + if (false == geometry::lines_cross(start, startdirection, origins.front(), directions.front(), first, along) || first <= 0.0) return false; vertices.push_back(start + startdirection * first); for (std::size_t j = 0; j + 1 < lines; ++j) { double a; double b; - if (false == crossing(origins[j], directions[j], origins[j + 1], directions[j + 1], a, b)) + if (false == geometry::lines_cross(origins[j], directions[j], origins[j + 1], directions[j + 1], a, b)) return false; vertices.push_back(origins[j] + directions[j] * a); } double last; - if (false == crossing(end, -enddirection, origins.back(), directions.back(), last, along) || last <= 0.0) + if (false == geometry::lines_cross(end, -enddirection, origins.back(), directions.back(), last, along) || last <= 0.0) return false; vertices.push_back(end - enddirection * last); // each vertex on from the one before along the line between them @@ -4210,6 +4334,364 @@ std::optional fit_chain(design const &Base, std::vector const &Points) +{ + trace_stretch fit; + if (Points.empty() || false == Base.start_curved || Base.start_radius <= 0.0 || glm::length(Base.start_direction) < 1e-9) + return fit; + auto const speed{std::max(1.0, Base.speed)}; + auto const start{plan_of(Base.start)}; + auto const heading{glm::normalize(Base.start_direction)}; + auto const side{Base.start_turn > 0 ? 1.0 : -1.0}; + auto const from{1.0 / Base.start_radius}; + auto const fromcant{std::max(0.0, Base.start_cant)}; + // going on from the diverging track of a switch: its first arc of the radius of the switch, with no transition before it + auto const held{Base.start_held}; + auto const target{Points.back()}; + // none where the last point lies on the other side of the heading + auto const offset{target - start}; + auto const lateral{glm::dot(offset, perpendicular(heading) * side)}; + auto const chord{glm::length(offset)}; + if (lateral < 1e-3 || chord < 1.0) + return fit; + // the shape: the transition from the curvature of the adjoining track to the first arc, the arcs with the transitions between them, + // the transition from the last one to the straight, which goes on to the last point where the points clicked after the curve show it + struct shape + { + double in{0.0}; + std::vector curvatures; + std::vector arcs; + std::vector between; + double out{0.0}; + double straight{0.0}; + }; + auto const curvature_of = [](double const Logarithm) { return 1.0 / std::exp(std::clamp(Logarithm, std::log(kTraceRadiusMin), std::log(kTraceRadiusMax))); }; + auto const segments_of = [&](shape const &Shape) { + std::vector> segments{{from, Shape.curvatures.front(), Shape.in}}; + for (std::size_t i = 0; i < Shape.curvatures.size(); ++i) + { + if (i > 0) + segments.push_back({Shape.curvatures[i - 1], Shape.curvatures[i], Shape.between[i - 1]}); + segments.push_back({Shape.curvatures[i], Shape.curvatures[i], Shape.arcs[i]}); + } + segments.push_back({Shape.curvatures.back(), 0.0, Shape.out}); + segments.push_back({0.0, 0.0, Shape.straight}); + return segments; + }; + auto const turn_of = [&](shape const &Shape) { + double turn{0.0}; + for (auto const &[first, last, length] : segments_of(Shape)) + turn += 0.5 * (first + last) * length; + return turn; + }; + // where the shape ends, heading which way + auto const end_of = [&](shape const &Shape, glm::dvec2 &End, glm::dvec2 &Leaving) { + glm::dvec2 origin{start}; + glm::dvec2 direction{heading}; + for (auto const &[first, last, length] : segments_of(Shape)) + { + if (length < 1e-9) + continue; + auto const local{spiral_point(first, last, length, length)}; + origin += direction * local.x + perpendicular(direction) * side * local.y; + direction = turned(direction, side * local.angle); + } + End = origin; + Leaving = direction; + }; + // the line of the shape in the plan, a point every half a metre about + auto const line_of = [&](shape const &Shape) { + plan_line line; + glm::dvec2 origin{start}; + glm::dvec2 direction{heading}; + double chainage{0.0}; + line.points.push_back(origin); + line.chainages.push_back(0.0); + for (auto const &[first, last, length] : segments_of(Shape)) + { + if (length < 1e-6) + continue; + glm::dvec2 const across{perpendicular(direction) * side}; + int const count{std::clamp(static_cast(std::ceil(length / 0.5)), 1, 400)}; + for (int i = 1; i <= count; ++i) + { + auto const local{spiral_point(first, last, length, length * i / count)}; + line.points.push_back(origin + direction * local.x + across * local.y); + line.chainages.push_back(chainage + length * i / count); + } + auto const local{spiral_point(first, last, length, length)}; + origin += direction * local.x + across * local.y; + direction = turned(direction, side * local.angle); + chainage += length; + } + return line; + }; + // the shape of the parameters: the square roots of the lengths of the transitions and of the straight, then for each arc but the + // last the logarithm of its radius, the square root of its length and of the transition after it; the last arc, its radius and + // length, Last. Held, the transition in is none and the first arc of the radius of the switch; a single arc so is given the square + // root of the straight after it in Last in place of its radius + auto const single_held = [&](std::vector const &Parameters) { return held && Parameters.size() == 3; }; + auto const shaped = [&](std::vector const &Parameters, std::array const &Last) { + shape result; + result.in = held ? 0.0 : Parameters[0] * Parameters[0]; + result.out = Parameters[1] * Parameters[1]; + result.straight = Parameters[2] * Parameters[2]; + for (std::size_t i = 3; i + 2 < Parameters.size(); i += 3) + { + result.curvatures.push_back(held && i == 3 ? from : curvature_of(Parameters[i])); + result.arcs.push_back(Parameters[i + 1] * Parameters[i + 1]); + result.between.push_back(Parameters[i + 2] * Parameters[i + 2]); + } + if (single_held(Parameters)) + { + result.curvatures.push_back(from); + result.straight = Last[0] * Last[0]; + } + else + result.curvatures.push_back(curvature_of(Last[0])); + result.arcs.push_back(Last[1] * Last[1]); + return result; + }; + // the radius and the length of the last arc bringing the end to the last point, the rest given: by Newton from the ones found last + auto const reach = [&](std::vector const &Parameters, std::array &Solution) { + auto const miss = [&](std::array const &At, glm::dvec2 &Miss) { + auto const trial{shaped(Parameters, At)}; + if (turn_of(trial) > kPi - 1e-2) + return false; + glm::dvec2 end; + glm::dvec2 leaving; + end_of(trial, end, leaving); + Miss = end - target; + return true; + }; + constexpr double kStep{1e-6}; + auto at{Solution}; + for (int i = 0; i < 40; ++i) + { + glm::dvec2 here; + if (false == miss(at, here)) + return false; + if (glm::length(here) < 1e-5) + { + Solution = at; + return true; + } + glm::dvec2 radial; + glm::dvec2 along; + auto byradius{at}; + byradius[0] += kStep; + auto byarc{at}; + byarc[1] += kStep; + if (false == miss(byradius, radial) || false == miss(byarc, along)) + return false; + radial = (radial - here) / kStep; + along = (along - here) / kStep; + auto const determinant{radial.x * along.y - radial.y * along.x}; + if (std::abs(determinant) < 1e-12) + return false; + auto const stepradius{(along.x * here.y - here.x * along.y) / determinant}; + auto const steparc{(here.x * radial.y - radial.x * here.y) / determinant}; + // half a step of the logarithm of the radius at most, the curve not swinging round; of the root of the straight after an arc + // held two + auto const damped{std::min(1.0, (single_held(Parameters) ? 2.0 : 0.5) / std::max(std::abs(stepradius), 1e-12))}; + at[0] += stepradius * damped; + if (single_held(Parameters)) + at[0] = std::abs(at[0]); + at[1] = std::abs(at[1] + steparc * damped); + } + return false; + }; + // the transitions as long as the norms ask for where the points don't show them shorter: a metre short of it weighed as a millimetre + // off a point, which only chooses among the shapes too few points leave open + constexpr double kShort{0.001}; + std::array solution{}; + residuals_of const residuals{[&](std::vector const &Parameters, std::vector &Residuals) { + auto at{solution}; + if (false == reach(Parameters, at)) + return false; + solution = at; + auto const trial{shaped(Parameters, at)}; + auto const line{line_of(trial)}; + Residuals.clear(); + for (std::size_t i = 0; i + 1 < Points.size(); ++i) + Residuals.push_back(project_on(line, Points[i]).offset); + auto const need_between = [&](double const First, double const Second) { + auto const sharper{1.0 / std::max({First, Second, 1e-9})}; + auto const allowed{std::max(1.0, std::min(speed, speed_limit(sharper, Base.norms)))}; + return std::min(between_curves(allowed, First, 0.0, Second, 0.0, Base.norms), 0.9 * longest_transition(speed, sharper, Base.norms)); + }; + auto const last{1.0 / trial.curvatures.back()}; + auto const allowed{std::max(1.0, std::min(speed, speed_limit(last, Base.norms)))}; + Residuals.push_back(kShort * std::max(0.0, need_between(from, trial.curvatures.front()) - trial.in)); + for (std::size_t i = 1; i < trial.curvatures.size(); ++i) + Residuals.push_back(kShort * std::max(0.0, need_between(trial.curvatures[i - 1], trial.curvatures[i]) - trial.between[i - 1])); + Residuals.push_back(kShort * std::max(0.0, std::min(transition_length(allowed, last, 0.0, Base.norms), 0.9 * longest_transition(speed, last, Base.norms)) - trial.out)); + // the curve ending at the last point where the points don't show a straight after it + Residuals.push_back(kShort * trial.straight); + return true; + }}; + std::vector best; + std::array bestsolution{}; + auto lowest{std::numeric_limits::max()}; + // the square root of the straight to start from: off nought, where the fit couldn't move it + constexpr double kSeed{0.5}; + // an arc of a compound curve shorter than the shortest one, a sharp one squeezed between two long transitions to bring the curve to + // the points (R 21 m for a millimetre after R 229 m): no curve to lay, as the fit of the arcs through the points has it. The first arc + // going on from a switch at its radius may be as short as it is + auto const kinked = [&](shape const &Shape) { + if (Shape.curvatures.size() < 2) + return false; + for (std::size_t i = held ? 1 : 0; i < Shape.arcs.size(); ++i) + if (Shape.arcs[i] < kShortestArc) + return true; + return false; + }; + // the parameters fitted: all but those of a curve going on from a switch which the shape doesn't take, the transition in and the + // radius of the first arc, or the straight after a single arc, which the reach finds then + auto const attempt = [&](std::vector Parameters, std::array const &Start) { + solution = Start; + std::vector loose; + for (std::size_t i = 0; i < Parameters.size(); ++i) + if (false == held || (i != 0 && i != (single_held(Parameters) ? 2u : 3u))) + loose.push_back(i); + auto const whole = [&](std::vector const &Loose) { + auto result{Parameters}; + for (std::size_t i = 0; i < loose.size(); ++i) + result[loose[i]] = Loose[i]; + return result; + }; + std::vector fitted; + for (auto const i : loose) + fitted.push_back(Parameters[i]); + residuals_of const some{[&](std::vector const &Loose, std::vector &Residuals) { return residuals(whole(Loose), Residuals); }}; + if (auto const cost{least_squares(fitted, some)}; cost < lowest && false == kinked(shaped(whole(fitted), solution))) + { + lowest = cost; + best = whole(fitted); + bestsolution = solution; + } + }; + // one arc, from two guesses of how far it turns: as the points head at the end, Base ending that way, and as the circle going on + // from the start through the last point does; held at the radius of a switch, as far round as that, the straight after it the rest + // of the way + std::vector turns; + if (glm::length(Base.end_direction) > 1e-9) + { + auto const ending{glm::normalize(Base.end_direction)}; + auto turn{std::atan2(side * cross(heading, ending), glm::dot(heading, ending))}; + if (turn < 0.0) + turn += 2.0 * kPi; + if (turn > 1e-3 && turn < kPi - 2e-2) + turns.push_back(turn); + } + turns.push_back(2.0 * std::asin(std::clamp(lateral / chord, 0.0, 1.0))); + for (auto const turn : turns) + for (auto const length : {10.0, 25.0}) + { + if (held) + { + auto const arc{std::max(1.0, Base.start_radius * turn - 0.5 * length)}; + attempt({0.0, std::sqrt(length), 0.0}, {std::sqrt(std::max(1.0, chord - arc)), std::sqrt(arc)}); + continue; + } + auto const radius{std::clamp(chord / std::max(1e-3, 2.0 * std::sin(0.5 * turn)), kTraceRadiusMin, kTraceRadiusMax)}; + attempt({std::sqrt(length), std::sqrt(length), kSeed}, {std::log(radius), std::sqrt(std::max(1.0, radius * turn - length))}); + } + // Abreast: the shape ends abreast of the last point, not on it + auto const settle = [&](shape const &Found, bool const Abreast) -> std::optional { + glm::dvec2 end; + glm::dvec2 leaving; + end_of(Found, end, leaving); + if (false == Abreast && glm::distance(end, target) > 0.01) + return std::nullopt; + // laid as the design lays a curve going on from an adjoining one: its vertex where the straights cross, its radii scaled to make + // the tangent before it reach the start, which they do + design laid{Base}; + laid.end = {end.x, Base.end.y, end.y}; + laid.end_direction = leaving; + laid.end_curved = false; + laid.end_reserve = 0.0; + laid.vertices.assign(1, vertex{}); + glm::dvec2 meeting; + if (false == tangent_intersection(laid, meeting)) + return std::nullopt; + auto &curve{laid.vertices.front()}; + curve.position = meeting; + curve.radius = 1.0 / Found.curvatures.front(); + curve.transition_in = Found.in; + curve.transition_out = Found.out; + if (Found.curvatures.size() > 1) + { + // the shares of the arcs by how far they turn + curve.compound = true; + curve.share = std::max(1e-6, Found.arcs.front() * Found.curvatures.front()); + for (std::size_t i = 1; i < Found.curvatures.size(); ++i) + { + compound_arc arc; + arc.radius = 1.0 / Found.curvatures[i]; + arc.transition = Found.between[i - 1]; + arc.share = std::max(1e-6, Found.arcs[i] * Found.curvatures[i]); + curve.arcs.push_back(arc); + } + } + curve.cant = Found.curvatures.size() > 1 ? 0.0 : curved_cant(curve.radius, Found.in, Found.out, fromcant, speed, Base.norms); + auto stretch{stretch_of(laid, Points)}; + if (stretch.has_value()) + { + stretch->leaving = leaving; + stretch->ending = end; + } + return stretch; + }; + std::optional one; + if (false == best.empty()) + one = settle(shaped(best, bestsolution), false); + // held, the arc of the switch alone, as far round as abreast of the last point seen from its centre and ending there: as the points + // clicked along it show it, no transition out of it bringing it to a point on it + if (held) + { + auto const centre{start + perpendicular(heading) * side * Base.start_radius}; + auto const outward{target - centre}; + auto sweep{std::fmod(side * (std::atan2(outward.y, outward.x) - std::atan2(start.y - centre.y, start.x - centre.x)), 2.0 * kPi)}; + if (sweep < 0.0) + sweep += 2.0 * kPi; + if (glm::length(outward) > 0.5 && sweep > 1e-3 && sweep < kPi - 2e-2) + { + shape arc; + arc.curvatures = {from}; + arc.arcs = {Base.start_radius * sweep}; + if (auto const alone{settle(arc, true)}; alone.has_value() && (false == one.has_value() || alone->rms < one->rms)) + one = alone; + } + } + // two arcs where one doesn't keep to the points and there are enough of them to tell: a flatter one going on from the end, even a + // straight about, then a sharper one, as where the line turns hard at the last points. Going on from a switch, the first one of its + // radius, then a flatter or a sharper one + if (Points.size() >= 3 && (false == one.has_value() || one->worst > kPointsWithin)) + { + auto const single{best.empty() || held ? std::array{std::log(std::clamp(chord, kTraceRadiusMin, kTraceRadiusMax)), std::sqrt(chord)} : bestsolution}; + auto const radius{std::exp(single[0])}; + lowest = std::numeric_limits::max(); + best.clear(); + std::vector const firsts{held ? std::vector{Base.start_radius} : std::vector{Base.start_radius, 2.0 * Base.start_radius, 3000.0}}; + std::vector const seconds{held ? std::vector{0.5 * Base.start_radius, 2.0 * Base.start_radius} : std::vector{0.5 * radius}}; + auto const shares{held ? std::array{0.15, 0.5} : std::array{0.4, 0.7}}; + for (auto const first : firsts) + for (auto const second : seconds) + for (auto const share : shares) + { + auto const along{std::max(1.0, share * chord)}; + attempt({std::sqrt(10.0), std::sqrt(10.0), kSeed, std::log(first), std::sqrt(along), std::sqrt(10.0)}, {std::log(std::max(kTraceRadiusMin, second)), std::sqrt(std::max(1.0, (1.0 - share) * chord))}); + } + if (false == best.empty()) + if (auto const two{settle(shaped(best, bestsolution), false)}; two.has_value() && (false == one.has_value() || two->rms < 0.6 * one->rms)) + one = two; + } + if (one.has_value()) + fit = *one; + return fit; +} + trace_stretch fit_trace_stretch(design const &Base, std::vector const &Points, int const Radii, bool const Straights) { trace_stretch chosen; @@ -4258,19 +4740,38 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector cons int const side{cross(startdirection, enddirection) > 0.0 ? 1 : -1}; auto const fromstart{Base.start_curved && Base.start_radius > 0.0 && Base.start_turn == side}; auto const curved{fromstart || (Base.end_curved && Base.end_radius > 0.0 && Base.end_turn == side)}; - if (curved) + // going on from the diverging track of a switch: at its radius, the transition on the other side of the arc making it begin there + auto const held{fromstart ? Base.start_held : Base.end_held}; + if (curved && held) + { + curve.radius = fromstart ? Base.start_radius : Base.end_radius; + one = stretch_of(single, Points); + } + else if (curved) { curve.radius = fromstart ? Base.start_radius : Base.end_radius; fit_curved(single, 0, Points, fromstart); one = stretch_of(single, Points); + // a compound curve where one radius doesn't keep to points enough for two; where it doesn't reach the end at all, as where a + // curve sharper than the one it goes on from ends the stretch, of two radii from fewer points too. Its transition from the curve + // of the adjoining track as long as the norms ask for at least, the curvature not jumping at the joint auto const loose{one.has_value() && (one->rms > kStretchClose || one->worst > kPointsWithin)}; - if (one.has_value() && Radii != 1 && (Radii > 1 || loose)) + auto const missing{false == one.has_value()}; + if (Radii != 1 && (missing || ((Radii > 1 || loose) && Points.size() >= 4))) if (auto const fit{fit_trace(single, 0, Points, Radii > 1 ? Radii : 2)}; fit.valid && fit.fitted.compound) { auto several{single}; several.vertices.front() = fit.fitted; fit_compound(several, 0, Points); - if (auto const compound{stretch_of(several, Points)}; compound.has_value() && (Radii > 1 || compound->rms < 0.75 * one->rms)) + auto &shape{several.vertices.front()}; + auto const joining{fromstart ? shape.radius : shape.arcs.empty() ? shape.radius : shape.arcs.back().radius}; + auto const adjoining{fromstart ? Base.start_radius : Base.end_radius}; + auto const allowed{std::max(1.0, std::min(speed, speed_limit(std::min(joining, adjoining), Base.norms)))}; + auto const needed{std::min(between_curves(allowed, 1.0 / adjoining, std::max(0.0, fromstart ? Base.start_cant : Base.end_cant), 1.0 / std::max(1.0, joining), 0.0, Base.norms), + 0.9 * longest_transition(speed, adjoining, Base.norms))}; + auto &between{fromstart ? shape.transition_in : shape.transition_out}; + between = std::max(between, std::round(needed)); + if (auto const compound{stretch_of(several, Points)}; compound.has_value() && (Radii > 1 || false == one.has_value() || compound->rms < 0.75 * one->rms)) one = compound; } } @@ -4310,9 +4811,12 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector cons fit_compound(several, 0, Points); compound = stretch_of(several, Points); } - if (auto const more{fit_arcs(single, Points, Radii > 1 ? Radii : 8)}; more.has_value() && (false == compound.has_value() || more->worst < compound->worst)) + // no more radii than half the points: a few points clicked a little off don't show a curve of many radii + if (auto const more{fit_arcs(single, Points, Radii > 1 ? Radii : std::clamp(static_cast(Points.size()) / 2, 2, 8))}; more.has_value() && (false == compound.has_value() || more->worst < compound->worst)) compound = more; - if (compound.has_value() && (Radii > 1 || compound->rms < 0.75 * one->rms || (one->worst > kPointsWithin && compound->worst < one->worst))) + // one with an arc as flat as a straight between its curves only where it keeps to all the points much closer: not to come + // nearer to a point clicked a little off, sharp curves either side of a straight + if (compound.has_value() && (Radii > 1 || compound->rms < 0.75 * one->rms || (one->worst > kPointsWithin && compound->worst < one->worst && (compound->vertices.empty() || false == flattened(compound->vertices.front()))))) one = compound; } } @@ -4490,7 +4994,7 @@ void stretch_fitter::work() m_asked.pop_back(); auto const asked{*m_fitting}; lock.unlock(); - auto fit{fit_trace_stretch(asked.base, asked.points, asked.radii, asked.straights)}; + auto fit{asked.going ? fit_going_on(asked.base, asked.points) : fit_trace_stretch(asked.base, asked.points, asked.radii, asked.straights)}; lock.lock(); constexpr std::size_t kKept{16}; m_results.insert(m_results.begin(), {asked.key, std::move(fit)}); diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index da32cd16..2dd1ddc5 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -105,6 +105,11 @@ struct design // of the only vertex goes on from the start alone bool start_curved{false}; bool end_curved{false}; + // the curve going on from the adjoining track at the start, or into the one at the end, goes on from the diverging track of a switch: + // at its radius, with no transition between them. The arcs after the first one, or the transition out of a single one, are what + // makes it begin right there + bool start_held{false}; + bool end_held{false}; // mm, the cant of the adjoining track at the start and at the end, leaning into its turn double start_cant{0.0}; double end_cant{0.0}; @@ -165,6 +170,9 @@ struct element glm::dvec2 normal{0.0}; }; +// the curvature of the element at its start and at its end, not signed by the side it turns to: an arc of a compound curve its own +std::pair end_curvatures(element const &Element); + struct curve_report { int vertex{-1}; @@ -236,7 +244,7 @@ struct result result compute(design const &Design); double speed_limit(double const Radius, limits const &Limits); -result compute_recommended(design &Design, bool const Transitions); +result compute_recommended(design &Design); bool tangent_intersection(design const &Design, glm::dvec2 &Point); bool collinear(design const &Design); bool spiral_tangents(transition_shape const Shape, double const TurnIn, double const TurnOut, double &TangentIn, double &TangentOut); @@ -312,7 +320,6 @@ struct join_request join_end end; double radius{0.0}; int variant{0}; - bool transitions{false}; double speed{100.0}; double kink{0.1}; transition_shape shape{transition_shape::cubic_parabola}; @@ -405,8 +412,14 @@ struct trace_stretch std::vector offsets; // of the points from the line laid, positive to the left double rms{0.0}; double worst{0.0}; + glm::dvec2 leaving{0.0}; // the heading at the end where the fit chose it, fit_going_on + glm::dvec2 ending{0.0}; // where it ends there: the last point, or abreast of it on the arc of a switch going on }; trace_stretch fit_trace_stretch(design const &Base, std::vector const &Points, int const Radii, bool const Straights); +// a curve going on from the curve of the free end Base starts at (start_curved), along the points to the last of them, where it ends on a +// straight heading as it comes out: from the curvature of that end through a transition to an arc and through another to the straight. +// The transitions as long as the norms ask for where the points don't show them shorter. Not valid where no such curve follows them +trace_stretch fit_going_on(design const &Base, std::vector const &Points); // the fit of a stretch traced, with up to three radii and no straight in it; none while it's being fitted elsewhere using stretch_fit = std::function(design const &, std::vector const &)>; @@ -417,6 +430,9 @@ using stretch_fit = std::function(design const &, s // each part of the row where the whole doesn't keep to them; such a vertex is marked 0 then. Fit fits it, by fit_trace_stretch // where none is given result ease_corners(design &Design, std::vector &Corners, corner_radii const &Radii, stretch_fit const &Fit = {}); +// the curves laid smaller than their vertices ask for, to fit between their straights or into their deflection, given the radii and the +// transitions they were laid with: the design tells what's laid, and the plan lays the same curve again +void settle_laid(std::vector &Vertices, result const &Laid); // the stretches traced fitted on a thread of their own while the cursor moves: fitting one takes tens of milliseconds. The one asked // for last is fitted first, the oldest of too many waiting left out, the fits of the last ones kept @@ -430,6 +446,7 @@ class stretch_fitter std::vector points; int radii{0}; bool straights{true}; + bool going{false}; // fit_going_on }; ~stretch_fitter(); void ask(request Request); diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp index 41199ab0..16bb0205 100644 --- a/editor/editorAlignmentStore.cpp +++ b/editor/editorAlignmentStore.cpp @@ -709,6 +709,14 @@ std::optional vertex_of(std::vector> auto const end{project(r, entering(Run.back(), true), tolerance)}; if (false == start.has_value() || false == end.has_value()) continue; + // the tracks of a part of the curve only, as on either side of a switch laid into it, aren't laid by its design: it's theirs only + // where they reach over the whole curve, else they're recognized as they lie + auto const low{std::min(start->chainage, end->chainage) - 0.5}; + auto const high{std::max(start->chainage, end->chainage) + 0.5}; + if (std::any_of(entry.result.elements.begin(), entry.result.elements.end(), [&](alignment::element const &E) { + return E.vertex == *vertices.begin() && E.kind != alignment::element_kind::straight && (E.chainage < low || E.chainage + E.length > high); + })) + continue; designed_vertex result; result.record = static_cast(r); result.vertex = *vertices.begin(); diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp index fedb3ff8..25c92805 100644 --- a/editor/editorGeometry.hpp +++ b/editor/editorGeometry.hpp @@ -50,6 +50,18 @@ inline double cross(glm::dvec2 const &Direction, glm::dvec2 const &Point) return Direction.x * Point.y - Direction.y * Point.x; } +// where the line from A along AlongA crosses the one from B along AlongB, as the distances along each in the lengths of their +// directions; none where they run alike +inline bool lines_cross(glm::dvec2 const &A, glm::dvec2 const &AlongA, glm::dvec2 const &B, glm::dvec2 const &AlongB, double &First, double &Second) +{ + auto const determinant{cross(AlongA, AlongB)}; + if (std::abs(determinant) < 1e-9) + return false; + First = cross(B - A, AlongB) / determinant; + Second = cross(B - A, AlongA) / determinant; + return true; +} + inline glm::dvec2 turned(glm::dvec2 const &Vector, double const Angle) { auto const c{std::cos(Angle)}; @@ -137,6 +149,14 @@ struct bezier { return 6.0 * (1.0 - T) * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1); } + // the curvature in the plan at T, 1/m, positive to the left as the curve runs; none where it barely moves + double plan_curvature(double const T) const + { + auto const along{first(T)}; + auto const turning{second(T)}; + auto const speed{std::hypot(along.x, along.z)}; + return speed < 1e-9 ? 0.0 : (along.x * turning.z - along.z * turning.x) / (speed * speed * speed); + } double plan_length(int const Steps = 32) const { double length{0.0}; diff --git a/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp index cbaad9d1..5d285d9c 100644 --- a/editor/editorOrthophoto.cpp +++ b/editor/editorOrthophoto.cpp @@ -23,6 +23,7 @@ http://mozilla.org/MPL/2.0/. #include +#include #include #include #include @@ -31,6 +32,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -42,12 +44,9 @@ namespace fs = std::filesystem; constexpr int standard_pixels{1024}; // ~25 cm/px for a 256 m tile, matches the standard orthophoto constexpr int hires_pixels{4096}; // ~6 cm/px, the WMS MaxWidth/MaxHeight of the geoportal services constexpr int probe_pixels{64}; // coverage test of the high-resolution product -constexpr int map_pixels{1024}; // a side of a tile of the map, whatever its size in metres -constexpr int map_stamp{32}; // rows under a tile of the map the service writes its credit into, cut off -constexpr char const *map_service{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"}; constexpr int worker_count{8}; // concurrent requests; the service is slow but shouldn't be hammered -// of the workers at most that many on the tiles around the camera, each holding an image decoded (a 4K one is 64 MB); the rest only -// download the tiles of the area to the disk cache +// of the workers at most that many on the 4K tiles around the camera, each holding an image of 64 MB decoded; the standard tiles +// are taken by all of them, ahead of the download of the area to the disk cache constexpr std::size_t camera_workers{3}; constexpr std::size_t upload_budget{16u << 20}; // bytes of texture data uploaded per frame (always at least one tile) constexpr std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets @@ -130,12 +129,42 @@ std::string wms_url(tile_request const &Request, bool Hires, int Pixels, bool Pn return url; } +// among the files kept to be fetched again faster, which don't roam with the profile. The orthophoto an older editor kept with the +// settings, the standard and the 4K imagery of each year, is moved there once, as the first tile is asked for; whatever else it +// kept there, the base maps of old among it, is deleted, and so is the orthophoto where it can't be moved or the new place has its own fs::path cache_root() { - fs::path root = user_config_path(".cache"); + fs::path root = user_cache_path("geoportal"); if (root.empty()) - root = ".cache"; - return root / "geoportal"; + root = fs::path{".cache"} / "geoportal"; + static std::once_flag moved; + std::call_once(moved, [&]() { + auto const old{user_config_path(".cache")}; + std::error_code ec; + if (old.empty() || false == fs::exists(old, ec)) + return; + std::vector products; + for (auto const &entry : fs::directory_iterator(old / "geoportal", ec)) + { + auto const name{entry.path().filename().string()}; + if (entry.is_directory(ec) && (name.rfind("sr_", 0) == 0 || name.rfind("hr_", 0) == 0)) + products.push_back(entry.path()); + } + if (false == products.empty() && false == fs::exists(root, ec)) + { + fs::create_directories(root, ec); + std::size_t count{0}; + for (auto const &product : products) + { + fs::rename(product, root / product.filename(), ec); + count += ec ? 0 : 1; + } + auto const path{root.u8string()}; + WriteLog("Editor orthophoto: " + std::to_string(count) + " products of the orthophoto moved to " + std::string(path.begin(), path.end())); + } + fs::remove_all(old, ec); + }); + return root; } fs::path cache_file(tile_request const &Request, bool Hires, std::string const &Suffix) @@ -154,11 +183,17 @@ bool is_image(std::vector const &Data) bool read_file(fs::path const &Path, std::vector &Out) { - std::ifstream file(Path, std::ios::binary); + // read whole, not a character at a time: a tile of the cache is megabytes + std::ifstream file(Path, std::ios::binary | std::ios::ate); if (!file.is_open()) return false; - Out.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); - return !Out.empty(); + auto const size{static_cast(file.tellg())}; + if (size <= 0) + return false; + Out.resize(static_cast(size)); + file.seekg(0); + file.read(reinterpret_cast(Out.data()), size); + return file.gcount() == size; } void write_file(fs::path const &Path, std::vector const &Data) @@ -190,10 +225,11 @@ void write_file(fs::path const &Path, std::vector const &Data) // the requests made of the services, the disk cache left out std::atomic requests_made{0}; -// cached file if present, otherwise downloads (with a few retries, the service drops connections and answers 404 now and then) +// cached file if present, otherwise downloads (with a few retries, the service drops connections and answers 404 now and then); +// with no path nothing is read from the disk nor written to it bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, std::vector &Out, std::string &Error, stop_check const &Stop) { - if (read_file(Path, Out) && is_image(Out)) + if (false == Path.empty() && read_file(Path, Out) && is_image(Out)) return true; for (int attempt = 0; attempt < 4; ++attempt) @@ -218,7 +254,8 @@ bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, st Error = "unexpected response: " + std::string(Out.begin(), Out.begin() + std::min(Out.size(), 200)); continue; } - write_file(Path, Out); + if (false == Path.empty()) + write_file(Path, Out); return true; } return false; @@ -394,28 +431,49 @@ bool load_tile(http_client &Client, tile_request const &Request, image &Out, boo return true; } -// the base map of OpenStreetMap from geoportal.gov.pl, in the metres of PUWG 1992 as the imagery. the service writes its credit into -// the corner of every image: the image reaches a little further south and the rows with it are cut off -bool load_map(http_client &Client, int const Level, int const Ix, int const Iy, image &Out, std::string &Error, stop_check const &Stop) +// a parameter of a request as an address carries it: the names of the layers keep their letters, digits, separators and colons +std::string url_encoded(std::string const &Text) { - auto const size{static_cast(editor_orthophoto::tile_size) << Level}; - auto const below{size * map_stamp / map_pixels}; - auto const url{std::string{map_service} + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=osm&STYLES=&SRS=EPSG:2180&FORMAT=image/png&WIDTH=" + std::to_string(map_pixels) + - "&HEIGHT=" + std::to_string(map_pixels + map_stamp) + "&BBOX=" + std::to_string(Ix * size) + "," + std::to_string(Iy * size - below) + "," + std::to_string((Ix + 1) * size) + "," + - std::to_string((Iy + 1) * size)}; - std::vector data; - auto const path{cache_root() / "osm" / (std::to_string(size) + "_" + std::to_string(Iy) + "_" + std::to_string(Ix) + ".png")}; - if (!fetch(Client, url, path, data, Error, Stop) || !decode(data, Out, Error)) - return false; - if (Out.width != map_pixels || Out.height != map_pixels + map_stamp) + std::string out; + for (unsigned char const c : Text) { - Error = "unexpected size of the map " + std::to_string(Out.width) + "x" + std::to_string(Out.height); - std::error_code ec; - fs::remove(path, ec); - return false; + if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || c == ':' || c == ',') + out += static_cast(c); + else + { + char hex[4]; + std::snprintf(hex, sizeof(hex), "%%%02X", c); + out += hex; + } } - Out.height = map_pixels; - Out.rgba.resize(static_cast(map_pixels) * map_pixels * 4); + return out; +} + +// a tile of a WMS of the user's, in EPSG:2180 as the imagery: kept in memory only, never on the disk. Empty where it draws nothing +bool load_service(http_client &Client, editor_orthophoto::config const &Source, tile_request const &Request, image &Out, bool &Empty, std::string &Error, stop_check const &Stop) +{ + Empty = false; + auto const size{static_cast(editor_orthophoto::tile_size)}; + auto const east{Request.ix * size}; + auto const north{Request.iy * size}; + auto url{Source.service}; + if (url.find('?') == std::string::npos) + url += '?'; + else if (url.back() != '?' && url.back() != '&') + url += '&'; + // a style for each layer, the default one + auto const count{static_cast(std::count(Source.layers.begin(), Source.layers.end(), ',')) + 1}; + url += "SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:2180&FORMAT=image/png&TRANSPARENT=TRUE&LAYERS=" + url_encoded(Source.layers) + "&STYLES=" + std::string(count - 1, ',') + + "&WIDTH=" + std::to_string(Request.level) + "&HEIGHT=" + std::to_string(Request.level) + "&BBOX=" + std::to_string(east) + "," + std::to_string(north) + "," + std::to_string(east + size) + "," + + std::to_string(north + size); + std::vector data; + if (!fetch(Client, url, {}, data, Error, Stop) || !decode(data, Out, Error)) + return false; + for (std::size_t i = 3; i < Out.rgba.size(); i += 4) + if (Out.rgba[i] != 0) + return true; + Empty = true; + Out = image{}; return true; } @@ -465,6 +523,8 @@ constexpr float no_height{-std::numeric_limits::max()}; // a bank of its own for every geometry chunk: adding a chunk to a bank makes the renderer upload the whole bank again, // and the tiles fitted onto a heightmap terrain carry its full grid (hundreds of thousands of vertices each) std::vector scene_banks; +// the textures made by all the layers, named apart: main thread only +int texture_slots{0}; std::string slot_name(int Index) { @@ -548,6 +608,136 @@ editor_orthophoto::~editor_orthophoto() worker.join(); } +namespace +{ + +// the text of an element of the capabilities, its entities read +std::string xml_text(std::string const &Text) +{ + std::string out; + for (std::size_t i = 0; i < Text.size(); ++i) + { + if (Text[i] != '&') + { + out += Text[i]; + continue; + } + auto const end{Text.find(';', i)}; + if (end == std::string::npos) + { + out += Text[i]; + continue; + } + auto const entity{Text.substr(i + 1, end - i - 1)}; + if (entity == "amp") + out += '&'; + else if (entity == "lt") + out += '<'; + else if (entity == "gt") + out += '>'; + else if (entity == "quot") + out += '"'; + else if (entity == "apos") + out += '\''; + else if (false == entity.empty() && entity[0] == '#') + { + auto const code{entity.size() > 1 && (entity[1] == 'x' || entity[1] == 'X') ? std::strtoul(entity.c_str() + 2, nullptr, 16) : std::strtoul(entity.c_str() + 1, nullptr, 10)}; + // carriage returns left out, the rest as UTF-8 + if (code == 13) + ; + else if (code < 0x80) + out += static_cast(code); + else if (code < 0x800) + { + out += static_cast(0xC0 | (code >> 6)); + out += static_cast(0x80 | (code & 0x3F)); + } + else + { + out += static_cast(0xE0 | (code >> 12)); + out += static_cast(0x80 | ((code >> 6) & 0x3F)); + out += static_cast(0x80 | (code & 0x3F)); + } + } + else + out += Text.substr(i, end - i + 1); + i = end; + } + // the blanks at the ends of the text dropped + auto const first{out.find_first_not_of(" \t\r\n")}; + if (first == std::string::npos) + return {}; + return out.substr(first, out.find_last_not_of(" \t\r\n") - first + 1); +} + +// the text of the first element of the tag between From and To, empty where there's none +std::string xml_element(std::string const &Xml, std::string const &Tag, std::size_t const From, std::size_t const To) +{ + auto const open{"<" + Tag + ">"}; + auto const start{Xml.find(open, From)}; + if (start == std::string::npos || start >= To) + return {}; + auto const end{Xml.find("", start)}; + if (end == std::string::npos || end > To) + return {}; + return xml_text(Xml.substr(start + open.size(), end - start - open.size())); +} + +} // namespace + +editor_orthophoto::service_info editor_orthophoto::describe(std::string const &Service) +{ + service_info info; + auto url{Service}; + if (url.find('?') == std::string::npos) + url += '?'; + else if (url.back() != '?' && url.back() != '&') + url += '&'; + url += "SERVICE=WMS&REQUEST=GetCapabilities"; + // a few tries, the services drop connections and time out now and then + std::vector data; + auto read{false}; + for (int attempt = 0; attempt < 3 && false == read; ++attempt) + read = editor_http_get(url, data, info.error); + if (false == read) + return info; + info.error.clear(); + std::string const xml(data.begin(), data.end()); + auto const service{xml.find("")}; + auto const capability{xml.find("")}; + if (service == std::string::npos || capability == std::string::npos) + { + info.error = "no WMS capabilities: " + xml.substr(0, std::min(xml.size(), 200)); + return info; + } + info.title = xml_element(xml, "Title", service, capability); + auto const fees{xml_element(xml, "Fees", service, capability)}; + auto const constraints{xml_element(xml, "AccessConstraints", service, capability)}; + for (auto const &part : {fees, constraints}) + if (false == part.empty() && part != "none" && part != "None" && part != "NONE") + info.terms += (info.terms.empty() ? "" : "\n") + part; + info.puwg = xml.find("EPSG:2180", capability) != std::string::npos; + // each layer up to the next one or to its end: a name of its own makes it one to ask for + auto const layer_at = [&](std::size_t From) { + for (auto at{xml.find("' || std::isspace(static_cast(xml[at + 6])))) + return at; + return std::string::npos; + }; + for (auto at{layer_at(capability)}; at != std::string::npos; at = layer_at(at + 6)) + { + auto const next{layer_at(at + 6)}; + auto const close{xml.find("", at)}; + auto const end{std::min(next, close)}; + auto const name{xml_element(xml, "Name", at, end)}; + if (false == name.empty()) + info.layers.emplace_back(name, xml_element(xml, "Title", at, end)); + } + if (info.layers.empty()) + info.error = "the service has no layer to ask for"; + return info; +} + bool editor_orthophoto::can_download() { #if defined(_WIN32) || defined(EU07_WITH_CURL) @@ -570,8 +760,10 @@ bool editor_orthophoto::owns(gfx::geometry_handle const &Geometry) std::string editor_orthophoto::mark(config const &Config, bool const Enabled, bool const Origin) { + // how the imagery is shown is the user's, kept with the editor (EditorSettings); the scenery keeps whether it's on and where the + // scenery lies char text[256]; - std::snprintf(text, sizeof(text), "on %d year %d opacity %.2f radius %d height %.2f drape %d lift %.2f hires %d scene %d", Enabled ? 1 : 0, Config.year, Config.opacity, Config.radius, Config.height, Config.drape ? 1 : 0, Config.lift, Config.hires ? 1 : 0, Config.in_scene ? 1 : 0); + std::snprintf(text, sizeof(text), "on %d", Enabled ? 1 : 0); std::string result{text}; if (Origin) { @@ -586,26 +778,11 @@ bool editor_orthophoto::read_mark(std::string const &Text, config &Config, bool std::istringstream words{Text}; std::string key; auto found{false}; + // the year, opacity, radius and the rest the lines of the older sceneries carry are passed over: the user's settings stand while (words >> key) { if (key == "on") words >> Enabled; - else if (key == "year") - words >> Config.year; - else if (key == "opacity") - words >> Config.opacity; - else if (key == "radius") - words >> Config.radius; - else if (key == "height") - words >> Config.height; - else if (key == "drape") - words >> Config.drape; - else if (key == "lift") - words >> Config.lift; - else if (key == "hires") - words >> Config.hires; - else if (key == "scene") - words >> Config.in_scene; else if (key == "origin") words >> Config.north >> Config.east; else @@ -614,8 +791,6 @@ bool editor_orthophoto::read_mark(std::string const &Text, config &Config, bool return found; found = true; } - Config.radius = std::clamp(Config.radius, 0, max_radius); - Config.opacity = std::clamp(Config.opacity, 0.0f, 1.0f); return found; } @@ -634,9 +809,6 @@ void editor_orthophoto::enabled(bool State) for (auto &entry : m_tiles) release_tile(entry.second); m_tiles.clear(); - for (auto &entry : m_maps) - release_tile(entry.second); - m_maps.clear(); ++m_generation; // results still in flight are now stale std::lock_guard lock(m_mutex); m_done.clear(); @@ -681,16 +853,11 @@ void editor_orthophoto::area(std::vector Polygon) { // the jobs for the tiles outside it forgotten, the tiles shown let go by the next update std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == 0 && 0 == m_area_tiles.count(Job.key); }), m_queue.end()); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return 0 == m_area_tiles.count(Job.key); }), m_queue.end()); } restart_prefetch(); } -bool editor_orthophoto::in_area(double const East, double const North) const -{ - return m_area.empty() || geometry::square_touches(m_area, East, North, 0.0); -} - void editor_orthophoto::area_paused(bool const Paused) { { @@ -703,7 +870,8 @@ void editor_orthophoto::area_paused(bool const Paused) void editor_orthophoto::restart_prefetch() { std::vector keys; - if (m_enabled && can_download()) + // a service of the user's is asked for no tile ahead + if (m_enabled && can_download() && m_config.service.empty()) keys.assign(m_area_tiles.begin(), m_area_tiles.end()); auto const centre{m_camera_tile}; auto const distance = [&](tile_key const &Key) { @@ -718,7 +886,7 @@ void editor_orthophoto::restart_prefetch() m_prefetch_next = 0; m_prefetch_failed.clear(); m_prefetch_cached.clear(); - m_prefetch_source = source{m_config.year, m_config.hires}; + m_prefetch_source = source{m_config.year, m_config.hires, m_config.service, m_config.layers}; ++m_prefetch_generation; } m_wakeup.notify_all(); @@ -801,8 +969,6 @@ void editor_orthophoto::cancel_pending() // jobs already picked up by a worker still deliver their result, which is accepted if the tile is still wanted for (auto &entry : m_tiles) entry.second.requested = 0; - for (auto &entry : m_maps) - entry.second.requested = 0; } void editor_orthophoto::clear_cache() @@ -811,9 +977,6 @@ void editor_orthophoto::clear_cache() for (auto &entry : m_tiles) release_tile(entry.second); m_tiles.clear(); - for (auto &entry : m_maps) - release_tile(entry.second); - m_maps.clear(); ++m_generation; { std::lock_guard lock(m_mutex); @@ -830,11 +993,6 @@ void editor_orthophoto::retry_failed() entry.second.failures = 0; entry.second.retry_at = 0.0; } - for (auto &entry : m_maps) - { - entry.second.failures = 0; - entry.second.retry_at = 0.0; - } // the tiles of the area which failed taken again { std::lock_guard lock(m_mutex); @@ -868,8 +1026,6 @@ editor_orthophoto::statistics editor_orthophoto::stats() const }; for (auto const &entry : m_tiles) count(entry.second); - for (auto const &entry : m_maps) - count(entry.second); return out; } @@ -902,7 +1058,8 @@ void editor_orthophoto::start_workers() { if (!m_workers.empty()) return; - for (int i = 0; i < worker_count; ++i) + // a service of the user's is asked fewer things at once than the orthophoto, which has the download of the area to do as well + for (int i = 0; i < (m_config.service.empty() ? worker_count : 2); ++i) m_workers.emplace_back(&editor_orthophoto::worker, this); } @@ -915,7 +1072,7 @@ editor_orthophoto::texture_slot editor_orthophoto::acquire_slot() return slot; } texture_slot slot; - slot.index = m_texture_count++; + slot.index = texture_slots++; slot.texture = GfxRenderer->Fetch_Texture(slot_name(slot.index), true); return slot; } @@ -933,12 +1090,12 @@ void editor_orthophoto::release_tile(tile &Tile) Tile.slot = texture_slot{}; } -void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha, int const Map) +void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha) { { std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key && Job.map == Map; }), m_queue.end()); - m_queue.push_back(job{Key, Map, Level, Alpha, source{m_config.year, m_config.hires}, m_generation.load()}); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key; }), m_queue.end()); + m_queue.push_back(job{Key, Level, Alpha, source{m_config.year, m_config.hires, m_config.service, m_config.layers}, m_generation.load()}); } Tile.requested = Level; Tile.requested_alpha = Alpha; @@ -955,17 +1112,10 @@ std::size_t editor_orthophoto::apply(result &Result, double Now) { if (Result.generation != m_generation.load()) return 0; - tile *found{nullptr}; - if (Result.map > 0) - { - if (auto const lookup = m_maps.find({Result.map, Result.key}); lookup != m_maps.end()) - found = &lookup->second; - } - else if (auto const lookup = m_tiles.find(Result.key); lookup != m_tiles.end()) - found = &lookup->second; - if (found == nullptr) + auto const found{m_tiles.find(Result.key)}; + if (found == m_tiles.end()) return 0; // left the area in the meantime; the imagery stays in the disk cache - tile &t = *found; + tile &t = found->second; bool const current = (t.requested == Result.level && t.requested_alpha == Result.alpha); if (current) t.requested = 0; @@ -981,7 +1131,7 @@ std::size_t editor_orthophoto::apply(result &Result, double Now) } // an image the tile no longer wants, as a 4K one it asked for before the camera moved on, isn't sent to the graphics card: the // one wanted now is on its way. A tile showing nothing yet takes it meanwhile - if (false == current && Result.map == 0 && t.level > 0) + if (false == current && t.level > 0) { ++m_stale; return 0; @@ -1027,7 +1177,7 @@ std::pair editor_orthophoto::grounded() const std::vector> editor_orthophoto::waiting() const { std::vector> result; - if (false == m_enabled || m_map_level > 0) + if (false == m_enabled) return result; for (auto const &entry : m_tiles) if (entry.second.level == 0 && entry.second.requested > 0 && false == entry.second.empty) @@ -1437,19 +1587,6 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) std::lock_guard lock(m_mutex); m_focus = centre; } - if (m_map_level > 0) - { - update_map(Camera, now); - return; - } - if (false == m_maps.empty()) - { - for (auto &entry : m_maps) - release_tile(entry.second); - m_maps.clear(); - std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [](job const &Job) { return Job.map > 0; }), m_queue.end()); - } int const radius = m_config.radius; auto const distance = [&](tile_key const &Key) { return std::max(std::abs(Key.first - centre.first), std::abs(Key.second - centre.second)); }; @@ -1479,7 +1616,7 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) if (evicted) { std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == 0 && distance(Job.key) > radius; }), m_queue.end()); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return distance(Job.key) > radius; }), m_queue.end()); } // tiles which need (different) imagery @@ -1500,90 +1637,49 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) tile &t = m_tiles[key]; if (!in_service_area(key.first, key.second)) t.empty = true; - int const level = level_for(std::max(std::abs(dx), std::abs(dy)), m_config.hires); + int level = level_for(std::max(std::abs(dx), std::abs(dy)), m_config.hires); + // a tile showing nothing yet takes the standard image first, from the disk cache at once mostly: the 4K one comes from a slow + // service a few at a time, the tiles around the camera stayed blank meanwhile though their standard images lay on the disk + if (level > standard_pixels && t.level < standard_pixels) + level = standard_pixels; if (t.empty || (t.level == level && t.alpha == m_alpha) || (t.requested == level && t.requested_alpha == m_alpha) || now < t.retry_at) continue; schedule(key, t, level, m_alpha); } - // ground fit, one tile per frame (it walks the scene geometry), nearest first + // ground fit, nearest first, as many tiles a frame as 4 ms let walk the scene geometry, one at least: a tile not fitted yet lies + // at the height of the layer, under the terrain of a scenery high above it, as if it hadn't come if (m_config.drape && m_ground && simulation::Region != nullptr) { - tile *nearest{nullptr}; - tile_key nearestkey{0, 0}; - int nearestdistance{INT_MAX}; - for (auto &entry : m_tiles) + auto const until{now + 0.004}; + for (;;) { - if (entry.second.grounded || entry.second.empty) - continue; - int const d = distance(entry.first); - if (d < nearestdistance) + tile *nearest{nullptr}; + tile_key nearestkey{0, 0}; + int nearestdistance{INT_MAX}; + for (auto &entry : m_tiles) { - nearestdistance = d; - nearest = &entry.second; - nearestkey = entry.first; + if (entry.second.grounded || entry.second.empty) + continue; + int const d = distance(entry.first); + if (d < nearestdistance) + { + nearestdistance = d; + nearest = &entry.second; + nearestkey = entry.first; + } } - } - if (nearest != nullptr) + if (nearest == nullptr) + break; sample_ground(nearestkey, *nearest); + if (steady_seconds() >= until) + break; + } } update_scene(); } -void editor_orthophoto::overview(double const Scale, double const Reach) -{ - // a pixel of a tile of the map is as large as one of the screen, or up to twice as small - m_map_level = Scale > 0.0 ? std::clamp(static_cast(std::floor(std::log2(Scale * map_pixels / tile_size))), 2, 9) : 0; - m_map_reach = Reach; -} - -void editor_orthophoto::update_map(glm::dvec3 const &Camera, double const Now) -{ - // the imagery gives way to the map, its tiles come back from the disk cache once the view is near again - if (false == m_tiles.empty()) - { - for (auto &entry : m_tiles) - release_tile(entry.second); - m_tiles.clear(); - std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [](job const &Job) { return Job.map == 0; }), m_queue.end()); - } - double const east = m_config.east - Camera.x; - double const north = m_config.north + Camera.z; - auto const in_view = [&](map_key const &Key) { - auto const size{tile_size * static_cast(1 << Key.first)}; - return (Key.second.first + 1) * size > east - m_map_reach && Key.second.first * size < east + m_map_reach && (Key.second.second + 1) * size > north - m_map_reach && - Key.second.second * size < north + m_map_reach; - }; - // the tiles of the other levels stay under the ones of the level shown until these come - auto const size{tile_size * static_cast(1 << m_map_level)}; - auto complete{true}; - for (int iy = static_cast(std::floor((north - m_map_reach) / size)); iy <= static_cast(std::floor((north + m_map_reach) / size)); ++iy) - for (int ix = static_cast(std::floor((east - m_map_reach) / size)); ix <= static_cast(std::floor((east + m_map_reach) / size)); ++ix) - { - map_key const key{m_map_level, {ix, iy}}; - tile &t = m_maps[key]; - if (t.level == 0 && false == t.empty) - complete = false; - if (t.empty || t.level == map_pixels || t.requested == map_pixels || Now < t.retry_at) - continue; - schedule(key.second, t, map_pixels, 255, m_map_level); - } - for (auto it = m_maps.begin(); it != m_maps.end();) - { - if (false == in_view(it->first) || std::abs(it->first.first - m_map_level) > 1 || (complete && it->first.first != m_map_level)) - { - release_tile(it->second); - it = m_maps.erase(it); - } - else - ++it; - } - std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map > 0 && m_maps.count({Job.map, Job.key}) == 0; }), m_queue.end()); -} - void editor_orthophoto::worker() { stbi_set_flip_vertically_on_load_thread(0); // the texture loader flips globally; tiles keep the north edge in the first row @@ -1602,17 +1698,16 @@ void editor_orthophoto::worker() // the nearest tile first, of the ones no other worker is on; none while enough workers are on tiles around the camera auto const nearest = [&]() { auto best = m_queue.end(); - if (m_running.size() >= camera_workers) + if (m_running.size() >= static_cast(worker_count)) return best; long long bestdistance = LLONG_MAX; for (auto it = m_queue.begin(); it != m_queue.end(); ++it) { - if (m_running.count({it->map, it->key}) > 0) + // a 4K image is 64 MB decoded: that many at most at once, the standard tiles by all the workers + if (m_running.count(it->key) > 0 || (it->level >= hires_pixels && m_running_hires >= camera_workers)) continue; - // a tile of the map is that many tiles of the imagery a side - long long const scale = 1ll << it->map; - long long const dx = it->key.first * scale + scale / 2 - m_focus.first; - long long const dy = it->key.second * scale + scale / 2 - m_focus.second; + long long const dx = it->key.first - m_focus.first; + long long const dy = it->key.second - m_focus.second; long long const d = dx * dx + dy * dy; if (d < bestdistance) { @@ -1630,7 +1725,9 @@ void editor_orthophoto::worker() { current = *best; m_queue.erase(best); - m_running.insert({current.map, current.key}); + m_running.insert(current.key); + if (current.level >= hires_pixels) + ++m_running_hires; } else { @@ -1662,7 +1759,6 @@ void editor_orthophoto::worker() result out; out.key = current.key; - out.map = current.map; out.level = current.level; out.generation = current.generation; out.alpha = current.alpha; @@ -1673,13 +1769,21 @@ void editor_orthophoto::worker() // a job of the same tile queued since: the tile wants another image now stop_check const superseded = [&]() { std::lock_guard lock(m_mutex); - return std::any_of(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == current.map && Job.key == current.key; }); + return std::any_of(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == current.key; }); }; - out.ok = current.map > 0 ? load_map(client, current.map, current.key.first, current.key.second, picture, error, stopping) : load_tile(client, request, picture, out.empty, error, stopping, superseded); + if (current.src.service.empty()) + out.ok = load_tile(client, request, picture, out.empty, error, stopping, superseded); + else + { + config service; + service.service = current.src.service; + service.layers = current.src.layers; + out.ok = load_service(client, service, request, picture, out.empty, error, stopping); + } if (m_stop) return; if (!out.ok && error != "superseded") - ErrorLog(std::string("Editor orthophoto: ") + (current.map > 0 ? "map " + std::to_string(current.map) + " " : std::string{}) + "tile " + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error); + ErrorLog(std::string("Editor orthophoto: tile ") + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error); if (out.ok && current.alpha < 255) for (std::size_t i = 3; i < picture.rgba.size(); i += 4) picture.rgba[i] = static_cast((picture.rgba[i] * current.alpha + 127u) / 255u); @@ -1690,7 +1794,9 @@ void editor_orthophoto::worker() { std::lock_guard lock(m_mutex); m_done.push_back(std::move(out)); - m_running.erase({current.map, current.key}); + m_running.erase(current.key); + if (current.level >= hires_pixels) + --m_running_hires; } // a job of the same tile may have waited for this one m_wakeup.notify_all(); @@ -1701,11 +1807,6 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const & { if (!m_enabled || ScreenWidth <= 0.0f || ScreenHeight <= 0.0f) return; - if (m_map_level > 0) - { - draw_map(ViewProjection, CameraPos, ScreenWidth, ScreenHeight); - return; - } if (m_config.in_scene || m_config.opacity <= 0.0f) return; @@ -1851,46 +1952,3 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const & drawlist->PopTextureID(); } } - -void editor_orthophoto::draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const -{ - // the map takes the place of the ground, opaque; the top view sees it straight down, a tile is one quad - ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); - auto const toscreen = [&](glm::vec4 const &Clip) { return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * ScreenWidth, (0.5f - Clip.y / Clip.w * 0.5f) * ScreenHeight); }; - float constexpr guard{1.02f}; - for (auto const shown : {false, true}) - for (auto const &entry : m_maps) - { - if ((entry.first.first == m_map_level) != shown) - continue; - tile const &t = entry.second; - if (t.level == 0 || t.empty || t.slot.texture == null_handle) - continue; - std::size_t const textureid = GfxRenderer->Texture(t.slot.texture).get_id(); - if (textureid == 0 || textureid == static_cast(static_cast(-1))) - continue; - auto const size{tile_size * static_cast(1 << entry.first.first)}; - glm::dvec2 const corner{m_config.east - entry.first.second.first * size, (entry.first.second.second + 1) * size - m_config.north}; - auto const at = [&](double U, double V) { return ViewProjection * glm::vec4(glm::vec3(glm::dvec3(corner.x - U * size, CameraPos.y - 1.0, corner.y - V * size) - CameraPos), 1.0f); }; - clip_vertex buffer0[12] = {{at(0.0, 0.0), {0.0f, 0.0f}}, {at(1.0, 0.0), {1.0f, 0.0f}}, {at(1.0, 1.0), {1.0f, 1.0f}}, {at(0.0, 1.0), {0.0f, 1.0f}}}; - clip_vertex buffer1[12]; - int count = clip_polygon(buffer0, 4, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.x; }); - count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.x; }); - count = clip_polygon(buffer0, count, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.y; }); - count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.y; }); - if (count < 3) - continue; - drawlist->PushTextureID((ImTextureID)(intptr_t)(textureid)); - drawlist->PrimReserve((count - 2) * 3, count); - auto const base = static_cast(drawlist->_VtxCurrentIdx); - for (int k = 1; k + 1 < count; ++k) - { - drawlist->PrimWriteIdx(base); - drawlist->PrimWriteIdx(static_cast(base + k)); - drawlist->PrimWriteIdx(static_cast(base + k + 1)); - } - for (int k = 0; k < count; ++k) - drawlist->PrimWriteVtx(toscreen(buffer0[k].position), ImVec2(buffer0[k].uv.x, buffer0[k].uv.y), IM_COL32_WHITE); - drawlist->PopTextureID(); - } -} diff --git a/editor/editorOrthophoto.hpp b/editor/editorOrthophoto.hpp index ed579701..d6fd43f8 100644 --- a/editor/editorOrthophoto.hpp +++ b/editor/editorOrthophoto.hpp @@ -65,6 +65,10 @@ class editor_orthophoto bool drape{true}; // fit the imagery onto the ground geometry instead of a flat plane float lift{0.05f}; // with drape: distance above the ground (in the scene it has to clear the ground to be seen) bool in_scene{false}; // render as opaque scene geometry, covered by whatever is in front of it + // a WMS of the user's in place of the orthophoto of geoportal.gov.pl: its address and the layers asked of it in EPSG:2180, PNG + // clear where it draws nothing. Its tiles are kept in memory only and asked for only around the camera, none ahead + std::string service; + std::string layers; }; using world_triangle = std::array; @@ -102,6 +106,8 @@ class editor_orthophoto // disabling drops the queue and returns the gpu memory of the resident tiles void enabled(bool State); config const &settings() const { return m_config; } + // the line of the scenery (//$o): whether the imagery is on and, with Origin, where the scenery lies; the rest of the settings + // are the user's static std::string mark(config const &Config, bool Enabled, bool Origin); static bool read_mark(std::string const &Text, config &Config, bool &Enabled); // changes of year / resolution invalidate resident tiles, the rest applies immediately @@ -116,7 +122,6 @@ class editor_orthophoto // tiles around the camera as before void area(std::vector Polygon); std::vector const &area() const { return m_area; } - bool in_area(double const East, double const North) const; // the download in the background held, or going on void area_paused(bool const Paused); // the squares of the tiles of the area in the world (x, z): 0 waiting, 1 in the disk cache, 2 failed @@ -146,10 +151,6 @@ class editor_orthophoto // true for geometry created by the layer, so ground queries can skip it static bool owns(gfx::geometry_handle const &Geometry); - // a top view of Scale metres a pixel shows the base map of OpenStreetMap in place of the imagery, as far as Reach from the - // camera; 0: the imagery - void overview(double const Scale, double const Reach); - bool overview() const { return m_map_level > 0; } // main thread, once per frame: schedules tiles around the camera and uploads finished ones void update(glm::dvec3 const &Camera); // main thread, inside an ImGui frame: emits the overlay into the background draw list. @@ -165,6 +166,17 @@ class editor_orthophoto statistics stats() const; // false when the build has no http client; cached tiles are still shown static bool can_download(); + // what a WMS tells of itself in its capabilities: its title, its fees and access constraints, its layers with a name and whether it + // draws in EPSG:2180, the one the tiles are laid out in. Blocking, for a thread of its own + struct service_info + { + std::string title; + std::string terms; + std::vector> layers; // name, title + bool puwg{false}; + std::string error; + }; + static service_info describe(std::string const &Service); static std::string cache_directory(); private: @@ -174,12 +186,13 @@ class editor_orthophoto { int year{0}; bool hires{false}; + std::string service; + std::string layers; }; struct job { tile_key key; - int map{0}; // level of a tile of the map, 256 m << map a side; 0: of the imagery int level{0}; // requested texture size in pixels std::uint8_t alpha{255}; // baked into the texture, for translucency in the scene source src; @@ -189,7 +202,6 @@ class editor_orthophoto struct result { tile_key key; - int map{0}; int level{0}; std::uint8_t alpha{255}; std::uint32_t generation{0}; @@ -235,7 +247,7 @@ class editor_orthophoto static int level_for(int Distance, bool Hires); tile_key camera_tile(glm::dvec3 const &Camera) const; glm::dvec2 tile_corner(tile_key const &Key) const; // world (x,z) of the north-west corner - void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha, int const Map = 0); + void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha); std::uint8_t texture_alpha() const; // alpha the textures should carry with the current settings std::size_t apply(result &Result, double Now); // returns bytes uploaded void release_tile(tile &Tile); @@ -247,8 +259,6 @@ class editor_orthophoto void remove_shape(tile &Tile); void release_geometry(tile &Tile); void update_scene(); - void update_map(glm::dvec3 const &Camera, double const Now); - void draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const; void start_workers(); void worker(); // the tiles of the area queued for the download in the background again, nearest to the camera first @@ -258,12 +268,7 @@ class editor_orthophoto bool m_enabled{false}; std::map m_tiles; // main thread only tile_key m_camera_tile{0, 0}; - using map_key = std::pair; // level, then the place on the grid of the tiles of that level - std::map m_maps; // main thread only - int m_map_level{0}; // of the map shown, 0: the imagery - double m_map_reach{0.0}; // m from the camera the view of the map reaches std::vector m_free_slots; // pool of textures, reused between tiles - int m_texture_count{0}; ground_query m_ground; height_query m_height; height_query m_guess; @@ -285,7 +290,8 @@ class editor_orthophoto std::deque m_queue; std::deque m_done; tile_key m_focus{0, 0}; // workers pick the queued tile closest to it - std::set> m_running; // (map, tile) a worker is on: another job of it waits till that one is done + std::set m_running; // tiles a worker is on: another job of one waits till that one is done + std::size_t m_running_hires{0}; // of them, the ones of a 4K image // the download of the area in the background: the tiles nearest to the camera first, taken when no tile around the camera waits std::vector m_prefetch; std::size_t m_prefetch_next{0}; diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index 4fbff5be..6abd632c 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -27,6 +27,8 @@ namespace // a switch, and the timbers it shares, may lie on a vertical curve: a concave one, or a convex one no sharper than this double constexpr switch_crest{2000.0}; // m +// per mille: the change of the grade on a switch taken for a break of it, 3 mm over 30 m of it; below it the switch lies in one plane +double constexpr switch_break{0.1}; double polyline(line const &Line, double const Chainage) { @@ -246,7 +248,9 @@ std::vector check(line const &Line, context const &Context) add(false, static_cast(k), points[k].chainage, format(STR_C("Point %zu: change of grade %.2f per mille needs a vertical curve"), k + 1, change)); else if (radius > 0.0 && radius < required - 1e-6) add(false, static_cast(k), points[k].chainage, format(STR_C("Point %zu: R %.0f m is below the required %.0f m for %.0f km/h"), k + 1, radius, required, Line.speed)); - if (radius <= 0.0 && change > 1e-3) + // a change as small as the grade lines laid one after another meet with, a few millimetres over the length of a switch, is no + // break of it: the tracks of a slip met so lie in one plane all the same (pila.scn: 0.05 per mille inside shared timbers) + if (radius <= 0.0 && change > switch_break) for (auto const &zone : Context.switches) // a break on the bound of a switch is let be, within what the chainages of routes laid over each other differ by if (points[k].chainage > zone.from + 0.05 && points[k].chainage < zone.to - 0.05) diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 69244625..bc3823d5 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include +#include #include #include #include @@ -48,16 +49,52 @@ template void parse(std::string const &Text, Type_ &Out) } const std::string origin_prefix{"ortho_origin@"}; +const std::string offset_prefix{"nmt_offset@"}; // settings are whitespace separated key/value pairs, so the scenery name used in the key can't contain spaces -std::string origin_key(std::string const &Scenery) +std::string scenery_key(std::string const &Prefix, std::string const &Scenery) { - std::string key = origin_prefix + Scenery; + std::string key = Prefix + Scenery; for (char &c : key) if (std::isspace(static_cast(c))) c = '_'; return key; } + +// a field of a WMS layer in the settings, which keep values with no blank in them: the blanks, the separator of the fields and the +// percent sign go as %XX +std::string encoded(std::string const &Text) +{ + std::string out; + for (unsigned char const c : Text) + { + if (std::isspace(c) || c == ';' || c == '%') + { + char hex[4]; + std::snprintf(hex, sizeof(hex), "%%%02X", c); + out += hex; + } + else + out += static_cast(c); + } + return out; +} + +std::string decoded(std::string const &Text) +{ + std::string out; + for (std::size_t i = 0; i < Text.size(); ++i) + { + if (Text[i] == '%' && i + 2 < Text.size() && std::isxdigit(static_cast(Text[i + 1])) && std::isxdigit(static_cast(Text[i + 2]))) + { + out += static_cast(std::strtoul(Text.substr(i + 1, 2).c_str(), nullptr, 16)); + i += 2; + } + else + out += Text[i]; + } + return out; +} } bool editorSettings::load() @@ -92,6 +129,8 @@ bool editorSettings::load() parse(value, m_orthophoto.lift); else if (key == "ortho_in_scene") m_orthophoto.in_scene = (value == "1"); + else if (key == "ortho_on") + m_orthophoto.on = (value == "1"); else if (key == "platform_edge") parse(value, m_platform_edge); else if (key == "signal_offset") @@ -108,6 +147,16 @@ bool editorSettings::load() m_terrain_offer = (value == "1"); else if (key == "track_tools_all") m_track_tools_more = (value == "1"); + else if (key == "terrain_while_laying") + m_terrain_while_laying = (value == "1"); + else if (key == "nmt_band") + parse(value, m_terrain_model.band); + else if (key == "nmt_around") + parse(value, m_terrain_model.around); + else if (key == "nmt_shape") + m_terrain_model.shape = (value == "1"); + else if (key == "nmt_follow") + m_terrain_model.follow = (value == "1"); else if (key == "top_ballast") m_top_tracks = value == "1" ? 0 : 1; else if (key == "top_tracks") @@ -130,6 +179,31 @@ bool editorSettings::load() // before, when they were the default, are left behind else if (key == "laying_heights") m_heights = value == "points" ? lay_heights::points : value == "level" ? lay_heights::level : lay_heights::fitted; + // visible;opacity;address;layers;title;terms + else if (key == "wms_layer") + { + std::vector fields; + for (std::size_t from{0};;) + { + auto const separator{value.find(';', from)}; + fields.push_back(decoded(value.substr(from, separator == std::string::npos ? std::string::npos : separator - from))); + if (separator == std::string::npos) + break; + from = separator + 1; + } + if (fields.size() < 4 || fields[2].empty() || fields[3].empty()) + continue; + wms_layer layer; + layer.visible = fields[0] == "1"; + parse(fields[1], layer.opacity); + layer.url = fields[2]; + layer.layers = fields[3]; + if (fields.size() > 4) + layer.title = fields[4]; + if (fields.size() > 5) + layer.terms = fields[5]; + m_wms_layers.push_back(layer); + } else if (key.rfind(origin_prefix, 0) == 0) { // northing;easting @@ -141,6 +215,16 @@ bool editorSettings::load() parse(value.substr(separator + 1), east); m_orthophoto_origins[key] = {north, east}; } + else if (key.rfind(offset_prefix, 0) == 0) + { + // offset;fitted + auto const separator = value.find(';'); + if (separator == std::string::npos) + continue; + double offset{0.0}; + parse(value.substr(0, separator), offset); + m_terrain_model_offsets[key] = {offset, value.substr(separator + 1) == "1"}; + } } return true; } @@ -168,6 +252,8 @@ bool editorSettings::save() stream << "ortho_drape " << (m_orthophoto.drape ? 1 : 0) << "\n"; stream << "ortho_lift " << m_orthophoto.lift << "\n"; stream << "ortho_in_scene " << (m_orthophoto.in_scene ? 1 : 0) << "\n"; + if (m_orthophoto.on.has_value()) + stream << "ortho_on " << (*m_orthophoto.on ? 1 : 0) << "\n"; stream << "platform_edge " << m_platform_edge << "\n"; stream << "signal_offset " << m_signal_offset << "\n"; stream << "nodebank_previews " << (m_nodebank_previews ? 1 : 0) << "\n"; @@ -176,19 +262,28 @@ bool editorSettings::save() stream << "first_steps " << (m_first_steps ? 1 : 0) << "\n"; stream << "terrain_offer " << (m_terrain_offer ? 1 : 0) << "\n"; stream << "track_tools_all " << (m_track_tools_more ? 1 : 0) << "\n"; + stream << "terrain_while_laying " << (m_terrain_while_laying ? 1 : 0) << "\n"; + stream << "nmt_band " << m_terrain_model.band << "\n"; + stream << "nmt_around " << m_terrain_model.around << "\n"; + stream << "nmt_shape " << (m_terrain_model.shape ? 1 : 0) << "\n"; + stream << "nmt_follow " << (m_terrain_model.follow ? 1 : 0) << "\n"; stream << "top_tracks " << m_top_tracks << "\n"; stream << "top_rails " << std::hex << (m_top_rails & 0xffffffu) << std::dec << "\n"; stream << "corner_radii " << m_corner_low << ";" << m_corner_high << "\n"; stream << "laying_heights " << (m_heights == lay_heights::points ? "points" : m_heights == lay_heights::level ? "level" : "fitted") << "\n"; + for (auto const &layer : m_wms_layers) + stream << "wms_layer " << (layer.visible ? 1 : 0) << ";" << layer.opacity << ";" << encoded(layer.url) << ";" << encoded(layer.layers) << ";" << encoded(layer.title) << ";" << encoded(layer.terms) << "\n"; stream << std::fixed << std::setprecision(3); for (auto const &origin : m_orthophoto_origins) stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n"; + for (auto const &offset : m_terrain_model_offsets) + stream << offset.first << " " << offset.second.first << ";" << (offset.second.second ? 1 : 0) << "\n"; return true; } bool editorSettings::orthophoto_origin(std::string const &Scenery, double &North, double &East) const { - auto const lookup = m_orthophoto_origins.find(origin_key(Scenery)); + auto const lookup = m_orthophoto_origins.find(scenery_key(origin_prefix, Scenery)); if (lookup == m_orthophoto_origins.end()) return false; North = lookup->second.first; @@ -198,5 +293,24 @@ bool editorSettings::orthophoto_origin(std::string const &Scenery, double &North void editorSettings::orthophoto_origin(std::string const &Scenery, double North, double East) { - m_orthophoto_origins[origin_key(Scenery)] = {North, East}; + m_orthophoto_origins[scenery_key(origin_prefix, Scenery)] = {North, East}; +} + +bool editorSettings::terrain_model_offset(std::string const &Scenery, double &Offset, bool &Fitted) const +{ + auto const lookup = m_terrain_model_offsets.find(scenery_key(offset_prefix, Scenery)); + if (lookup == m_terrain_model_offsets.end()) + return false; + Offset = lookup->second.first; + Fitted = lookup->second.second; + return true; +} + +void editorSettings::terrain_model_offset(std::string const &Scenery, double Offset, bool Fitted) +{ + // the model as it is, never fitted, is what a scenery without the line has + if (Offset == 0.0 && false == Fitted) + m_terrain_model_offsets.erase(scenery_key(offset_prefix, Scenery)); + else + m_terrain_model_offsets[scenery_key(offset_prefix, Scenery)] = {Offset, Fitted}; } diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index f98d965a..7f3bd839 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -2,8 +2,10 @@ #include #include +#include #include #include +#include class editorSettings { @@ -23,7 +25,8 @@ public: movement_scheme movement() const { return m_movement; } void movement(movement_scheme scheme) { m_movement = scheme; } - // geoportal orthophoto layer (editor_orthophoto); the origin is kept per scenery + // geoportal orthophoto layer (editor_orthophoto), how it's shown and whether it's on; the origin is kept per scenery, and the + // scenery keeps it too struct orthophoto_settings { int radius{2}; @@ -34,11 +37,25 @@ public: bool drape{true}; float lift{0.05f}; bool in_scene{false}; + // turned on or off by the user, for every scenery; none chosen yet: as the scenery keeps it, or the terrain model turns it on + std::optional on; }; orthophoto_settings &orthophoto() { return m_orthophoto; } // PUWG 1992 northing/easting of the scenery's (0,0,0) point; false when none was stored bool orthophoto_origin(std::string const &Scenery, double &North, double &East) const; void orthophoto_origin(std::string const &Scenery, double North, double East); + // the WMS layers the user added under the scenery, over the orthophoto, the top one first: the address of the service, the + // layers asked of it, its title and terms as its capabilities gave them. Kept with the editor, not with the scenery + struct wms_layer + { + std::string url; + std::string layers; + std::string title; + std::string terms; + float opacity{0.8f}; + bool visible{true}; + }; + std::vector &wms_layers() { return m_wms_layers; } double platform_edge() const { return m_platform_edge; } void platform_edge(double Distance) { m_platform_edge = Distance; } @@ -60,6 +77,22 @@ public: // a scenery begun with no terrain is offered the terrain model as the first track is laid bool terrain_offer() const { return m_terrain_offer; } void terrain_offer(bool Offer) { m_terrain_offer = Offer; } + // the terrain of the terrain model made under the tracks as they're laid, around the cursor from the first point placed + bool terrain_while_laying() const { return m_terrain_while_laying; } + void terrain_while_laying(bool const Make) { m_terrain_while_laying = Make; } + // the terrain model in the terrain panel: the band along the tracks it's loaded in, m; the chunks along a side of the square + // made around the camera; the formation and the slopes made along the tracks; the terrain following the tracks edited + struct terrain_model_settings + { + double band{300.0}; + int around{4}; + bool shape{true}; + bool follow{true}; + }; + terrain_model_settings &terrain_model() { return m_terrain_model; } + // the height offset of the terrain model to the tracks of the scenery, and whether it was fitted to them; false when none was stored + bool terrain_model_offset(std::string const &Scenery, double &Offset, bool &Fitted) const; + void terrain_model_offset(std::string const &Scenery, double Offset, bool Fitted); bool track_tools_more() const { return m_track_tools_more; } void track_tools_more(bool Show) { m_track_tools_more = Show; } // the ballast of the railway drawn in the top view @@ -90,6 +123,7 @@ private: movement_scheme m_movement{movement_scheme::wsad}; orthophoto_settings m_orthophoto; std::map> m_orthophoto_origins; + std::vector m_wms_layers; double m_platform_edge{1.725}; double m_signal_offset{2.25}; bool m_nodebank_previews{true}; @@ -98,6 +132,9 @@ private: bool m_first_steps{true}; bool m_terrain_offer{true}; bool m_track_tools_more{false}; + bool m_terrain_while_laying{true}; + terrain_model_settings m_terrain_model; + std::map> m_terrain_model_offsets; int m_top_tracks{0}; // the railway in the top view: whole, without the ballast, the rails alone std::uint32_t m_top_rails{0xffff6e28}; // colour of the rails alone, as ImGui keeps it: ABGR double m_corner_low{3000.0}; diff --git a/editor/editorSwitchCurve.cpp b/editor/editorSwitchCurve.cpp index e770a647..3a8fee4b 100644 --- a/editor/editorSwitchCurve.cpp +++ b/editor/editorSwitchCurve.cpp @@ -133,7 +133,7 @@ std::vector ramp_ends(std::vector const &Track) } switch_report check(std::vector const &Main, std::vector const &Branch, std::vector const &Line, double const From, double const To, double const Radius, - double const Ratio) + double const Ratio, double const Straight) { switch_report report; if (Main.size() < 2 || Branch.size() < 2 || Line.size() < 2) @@ -149,13 +149,15 @@ switch_report check(std::vector const &Main, std::vector::lowest()}; double sharpest{0.0}; }; + // of the part bent: up to the frog of a switch bent up to it auto const along = [&](std::vector const &Track) { span result; - auto const length{Track.back().station - Track.front().station}; + auto const finish{Track.back().station - (Straight > 0.5 ? Straight : 0.0)}; + auto const length{finish - Track.front().station}; auto const margin{std::min(1.0, length * 0.25)}; result.first = sign * curvature_at(Track, Track.front().station + margin); - result.last = sign * curvature_at(Track, Track.back().station - margin); - for (auto s{Track.front().station + margin}; s <= Track.back().station - margin + 1e-9; s += 0.5) + result.last = sign * curvature_at(Track, finish - margin); + for (auto s{Track.front().station + margin}; s <= finish - margin + 1e-9; s += 0.5) { auto const curvature{sign * curvature_at(Track, s)}; result.low = std::min(result.low, curvature); @@ -190,8 +192,14 @@ switch_report check(std::vector const &Main, std::vector kStraight && 1.0 / main.sharpest < limit->main - 1e-6) report.warnings.emplace_back(format(STR_C("The main track bent to R %.0f m, below the least %.0f m for the switch of R %.0f m (table 11.9)"), 1.0 / main.sharpest, limit->main, Radius)); } - if (Ratio > 0.0 && Ratio < 9.41) + // 1:9 itself is bent, as the Rz 300-1:9 of table 11.9; the ratio may come from the angle of the tracks, a bit off the catalogue, + // 1:9.403 told from 1:9.51 by half of what keeps them apart + if (Ratio > 0.0 && (Ratio < 8.95 || std::abs(Ratio - 9.403) < 0.05)) report.warnings.emplace_back(format(STR_C("A switch of 1:%.3f isn't bent in the running and the main tracks: none of 1:9.403 nor steeper than 1:9 (11.4 pkt 4)"), Ratio)); + // the basic switches of table 11.9 have the curve of the diverging track running through the frog; one with a straight frog is + // bent up to the frog, the frog and both tracks over it straight, the curvature jumping there + if (Straight > 0.5) + report.warnings.emplace_back(format(STR_C("Bent up to the frog, as the DB bends the switches with a straight frog: both tracks keep straight over its last %.1f m, the curvature jumps there; table 11.9 lists no such bent switch"), Straight)); } check_cant(Line, From, To, report.curved, report.cant_start, report.cant_end, report.notes, report.warnings); diff --git a/editor/editorSwitchCurve.hpp b/editor/editorSwitchCurve.hpp index 82789fcf..9b58e50e 100644 --- a/editor/editorSwitchCurve.hpp +++ b/editor/editorSwitchCurve.hpp @@ -51,9 +51,10 @@ struct switch_report // Main, Branch: the tracks of the switch from its start, the stations from 0 there; Line: the track it's laid into, From and To the // stations of the start and the end of the switch along it, From after To where it goes back along the line. Radius and Ratio: -// of the diverging track of the basic switch it's bent from +// of the diverging track of the basic switch it's bent from; Straight: how far both tracks keep straight at the end of the switch, over +// a straight frog, the switch bent up to it as the DB bends such switches switch_report check(std::vector const &Main, std::vector const &Branch, std::vector const &Line, double const From, double const To, double const Radius, - double const Ratio); + double const Ratio, double const Straight = 0.0); // a slip switch bent into a curve, as ST-T1-A6 sees it: in a side track only (11.4 pkt 4b), the curvature of each track through it of // one sign all along it (9.7 pkt 12), its slip curves no sharper than a side track may be (table 9.2), the cant as over a switch bent, diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index a509f051..7329686a 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -224,6 +224,12 @@ std::vector const &records() return g_records; } +bool has(std::string const &Name) +{ + auto const &all{records()}; + return std::any_of(all.begin(), all.end(), [&](record const &Record) { return Record.name == Name; }); +} + std::optional named(TTrack const &Track) { load(); diff --git a/editor/editorSwitchStore.hpp b/editor/editorSwitchStore.hpp index 15e4f144..3332f212 100644 --- a/editor/editorSwitchStore.hpp +++ b/editor/editorSwitchStore.hpp @@ -58,6 +58,8 @@ struct record void reset(); void load(); std::vector const &records(); +// a record of the name is kept +bool has(std::string const &Name); // the record of the switch, slip or crossing the track is a part of, while its first part keeps the shape the record says std::optional of(TTrack const &Track); // the record of the track by its name alone, also when the switch lost its shape diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 151aab8d..c7ce7b1f 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -598,15 +598,17 @@ void terrain_streamer::update(glm::dvec3 const &Camera) m_settled = false; } - // the top view of the tracks wants the terrain as far as the corners of the screen, as fine as its pixels need - auto const top{Global.EditorOrtho && Global.editor_tracks && false == Global.EditorOrthoMap}; + // the top view of the tracks wants the terrain as far as the corners of the screen, as fine as its pixels need, though no + // farther than the scenery reaches in the game: zoomed out, the tracks alone are drawn farther + auto const top{Global.EditorOrtho && Global.editor_tracks}; auto const aspect{Global.window_size.x / std::max(1.0, static_cast(Global.window_size.y))}; - auto const reach{top ? Global.EditorOrthoExtent * std::sqrt(1.0 + aspect * aspect) : 0.0}; + auto const scenery{static_cast(Global.BaseDrawRange) * std::max(1.0, static_cast(Global.fDistanceFactor))}; + auto const reach{top ? std::min(Global.EditorOrthoExtent * std::sqrt(1.0 + aspect * aspect), scenery) : 0.0}; m_pixel = top ? 2.0 * Global.EditorOrthoExtent / std::max(1, Global.window_size.y) : 0.0; // chunks wanted around the camera and around where it's heading; the top view only as far as the screen reaches, not the // range of the 3D view: zoomed in it would keep making anew the meshes of chunks far off the screen at each step of the zoom - auto const range{(top ? std::max(m_radius * heightmap::chunk_size, reach) : std::max({m_radius * heightmap::chunk_size, static_cast(Global.BaseDrawRange) * std::max(1.0, static_cast(Global.fDistanceFactor)), reach})) + heightmap::chunk_size * 0.5}; + auto const range{(top ? std::max(m_radius * heightmap::chunk_size, reach) : std::max({m_radius * heightmap::chunk_size, scenery, reach})) + heightmap::chunk_size * 0.5}; auto const radius{std::min(top ? top_radius : 64, static_cast(std::ceil(range / heightmap::chunk_size)))}; if (m_ringradius < radius) { diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 5f4ed495..c2d714a7 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -89,11 +89,7 @@ double path_radius(segment_data const &Path, double const T) { if (Path.points[segment_data::point::control1] == glm::dvec3{} && Path.points[segment_data::point::control2] == glm::dvec3{}) return 0.0; - bezier const curve{Path}; - auto const first{curve.first(T)}; - auto const second{curve.second(T)}; - auto const speed{std::hypot(first.x, first.z)}; - auto const curvature{std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed)}; + auto const curvature{std::abs(bezier{Path}.plan_curvature(T))}; return curvature > 1e-7 ? 1.0 / curvature : 0.0; } @@ -108,25 +104,118 @@ bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]}; if (glm::distance(point, Joint) > kSamePoint) continue; - auto const tangent{glm::normalize(path_tangent(path, atend))}; + // measured on the piece at the end where the path has them: the path only approximates them, its curvature off at its ends, + // R 300.9 at the end of the diverging track of a switch of R 300 + auto const &end{path.pieces.empty() ? path : atend ? path.pieces.back() : path.pieces.front()}; + auto const tangent{glm::normalize(path_tangent(end, atend))}; auto const along{Leaving != atend}; Direction = along ? tangent : -tangent; - Radius = path_radius(path, atend ? 1.0 : 0.0); + Radius = path_radius(end, atend ? 1.0 : 0.0); // the side it turns to as the chain runs, the path gone against its points turning the other way Turn = 0; if (Radius > 0.0) - { - bezier const curve{path}; - auto const first{curve.first(atend ? 1.0 : 0.0)}; - auto const second{curve.second(atend ? 1.0 : 0.0)}; - Turn = (first.x * second.z - first.z * second.x > 0.0) == along ? 1 : -1; - } + Turn = (bezier{end}.plan_curvature(atend ? 1.0 : 0.0) > 0.0) == along ? 1 : -1; return true; } } return false; } +// the element of a stored design a regular track lies along from end to end: its curvature as designed rather than as the path +// approximates it, an arc of its radius however short it is, or a transition. None where the track lies along no design, or across +// the joint of two of its elements +std::optional designed_element(TTrack const &Track) +{ + if (Track.eType != tt_Normal || Track.m_paths.empty()) + return std::nullopt; + auto const found{alignment_store::on_track(Track)}; + if (false == found.has_value()) + return std::nullopt; + bezier const curve{Track.m_paths.front()}; + for (auto const t : {0.1, 0.9}) + if (auto const at{alignment_store::on_record(found->record, curve.point(t), 0.05)}; false == at.has_value() || at->element != found->element) + return std::nullopt; + auto const &elements{alignment_store::records()[static_cast(found->record)].result.elements}; + if (found->element < 0 || found->element >= static_cast(elements.size())) + return std::nullopt; + return elements[static_cast(found->element)]; +} + +// a stretch of a curve along the run of its paths: its length, its curvature at its start and at its end, unsigned +struct curve_piece +{ + double length{0.0}; + double from{0.0}; + double to{0.0}; +}; + +// the curvature of an element of a design at its start and at its end, unsigned +std::pair element_curvatures(alignment::element const &Element) +{ + auto const [start, end]{alignment::end_curvatures(Element)}; + return {std::abs(start), std::abs(end)}; +} + +// the pieces of a curve along the run of its paths: of the elements of the stored design a path lies along, of the path itself where +// it lies along none, its curvature as at its middle where it barely changes along it. False where no path lies along a design +bool designed_pieces(std::vector> const &Run, std::vector &Pieces) +{ + Pieces.clear(); + auto designed{false}; + for (auto const &[track, forward] : Run) + { + auto const &path{track->m_paths.front()}; + auto const length{track->eType == tt_Switch ? bezier{path}.plan_length() : track->Length()}; + auto const &begin{path.points[forward ? segment_data::point::start : segment_data::point::end]}; + auto const &finish{path.points[forward ? segment_data::point::end : segment_data::point::start]}; + std::optional first; + std::optional last; + if (auto const found{track->eType == tt_Normal ? alignment_store::on_track(*track) : std::nullopt}) + { + first = alignment_store::on_record(found->record, begin, 0.05); + last = alignment_store::on_record(found->record, finish, 0.05); + } + if (false == first.has_value() || false == last.has_value() || first->record != last->record || std::abs(last->chainage - first->chainage) < 1e-3) + { + auto const curvature_at = [&](double const T) { + auto const radius{path_radius(path, forward ? T : 1.0 - T)}; + return radius > 0.0 ? 1.0 / radius : 0.0; + }; + auto const start{curvature_at(0.0)}; + auto const end{curvature_at(1.0)}; + if (std::abs(start - end) <= 0.1 * std::max(start, end)) + Pieces.push_back({length, curvature_at(0.5), curvature_at(0.5)}); + else + Pieces.push_back({length, start, end}); + continue; + } + designed = true; + auto const &elements{alignment_store::records()[static_cast(first->record)].result.elements}; + auto const low{std::min(first->chainage, last->chainage)}; + auto const high{std::max(first->chainage, last->chainage)}; + std::vector own; + for (auto const &element : elements) + { + auto const from{std::max(low, element.chainage)}; + auto const to{std::min(high, element.chainage + element.length)}; + if (to - from < 1e-6) + continue; + auto const [start, end]{element_curvatures(element)}; + auto const at = [&](double const Chainage) { return element.length > 1e-9 ? start + (end - start) * (Chainage - element.chainage) / element.length : start; }; + own.push_back({to - from, at(from), at(to)}); + } + // the design running against the run + if (last->chainage < first->chainage) + { + std::reverse(own.begin(), own.end()); + for (auto &piece : own) + std::swap(piece.from, piece.to); + } + Pieces.insert(Pieces.end(), own.begin(), own.end()); + } + return designed; +} + } editor_track::observer *editor_track::s_observer{nullptr}; @@ -628,21 +717,15 @@ std::vector> editor_track::connecte return result; } -namespace +glm::dvec2 editor_track::end_direction(segment_data const &Path, point_kind const Kind) { - -// direction of the path at the end, into the path -glm::dvec2 end_direction(segment_data const &Path, editor_track::point_kind const Kind) -{ - bool const start{Kind == editor_track::point_kind::start}; + bool const start{Kind == point_kind::start}; auto const &point{Path.points[start ? segment_data::point::start : segment_data::point::end]}; auto const &control{Path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; auto const direction{plan_of(control != glm::dvec3{} ? control : Path.points[start ? segment_data::point::end : segment_data::point::start] - point)}; return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; } -} // namespace - std::vector editor_track::joints(TTrack const &Track) { std::vector result; @@ -1625,7 +1708,14 @@ std::vector editor_track::find_straights(double const Mi bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve) { Curve = {}; - auto const curved = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && false == is_straight(*Other, Tolerance); }; + // a track too short to tell its bend from a straight is a part of the curve where it lies along an arc or a transition of a design + auto const designed_bend = [](TTrack const &Other) { + auto const element{designed_element(Other)}; + return element.has_value() && element->kind != alignment::element_kind::straight; + }; + auto const curved = [&](TTrack const *Other) { + return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && (false == is_straight(*Other, Tolerance) || designed_bend(*Other)); + }; if (false == curved(&Track)) return false; @@ -1814,6 +1904,48 @@ bool editor_track::analyse_curve(std::vector> const &R Curve.arcs = arcs; } } + // along the stored designs the curve is read from their elements, as it was laid, the paths along none by their own curvature: the + // curve of two lines laid one after another made of the arcs and the transitions of both, however short they are + if (std::vector pieces; Curve.reversals == 0 && designed_pieces(run, pieces)) + { + std::vector designedarcs; + double in{0.0}; + double between{0.0}; + auto inside{false}; // a straight between two arcs: no curve of one vertex + auto straight{false}; + for (auto const &piece : pieces) + { + auto const even{std::abs(piece.from - piece.to) <= 0.02 * std::max(piece.from, piece.to)}; + if (even && piece.from < 1e-6) + { + straight = false == designedarcs.empty(); + continue; + } + if (false == even) + { + (designedarcs.empty() ? in : between) += piece.length; + continue; + } + inside = inside || straight; + auto const radius{2.0 / (piece.from + piece.to)}; + if (false == designedarcs.empty() && between < 1e-6 && std::abs(designedarcs.back().radius - radius) <= 0.02 * radius) + designedarcs.back().turn += piece.length / radius; + else + designedarcs.push_back({radius, piece.length / radius, designedarcs.empty() ? 0.0 : between}); + between = 0.0; + } + if (false == designedarcs.empty() && false == inside) + { + Curve.laid = true; + Curve.transition_in = in; + Curve.transition_out = between; + Curve.radius = designedarcs.front().radius; + Curve.compound = designedarcs.size() > 1; + Curve.arcs.clear(); + if (Curve.compound) + Curve.arcs = designedarcs; + } + } if (Curve.reversals == 0) { if (auto const designed{alignment_store::vertex_of(Run)}) @@ -2853,6 +2985,8 @@ std::optional editor_track::template_of(switch_de entry.movable = Design.movable; entry.heel = Design.heel; entry.bladethrow = Design.bladethrow; + if (false == Design.parts.empty() && Design.parts.back().radius1 <= 0.0 && Design.parts.back().radius2 <= 0.0) + entry.frog_straight = Design.parts.back().length; entry.local = {shape.paths[0], shape.paths[1]}; entry.local[1].radius = static_cast(shape.radius); if (shape.paths.size() >= 5) @@ -3022,7 +3156,7 @@ std::vector editor_track::standard_switch_templat slip.rail = std::string{entry.name}.substr(0, std::string{entry.name}.find('-')); slip.label = std::string{entry.type} + ' ' + entry.name + format(" a %.3f", entry.half) + (slip.type == "Rkp" ? " single slip" : " double slip") + (entry.middle < 0.0 ? ", the blades outside of the diamond" : "") + - (slip.type == "Rkp" ? " (click at a crossing of two straights, on the side of the joined tracks)" : " (click at a crossing of two straights)"); + (slip.type == "Rkp" ? " (click at a crossing of two straights, on the side of the joined tracks, or drag it out on a straight)" : " (click at a crossing of two straights, or drag it out on a straight)"); result.push_back(slip); } struct crossing @@ -3060,7 +3194,7 @@ std::vector editor_track::standard_switch_templat plain.a = entry.half; plain.ratio = ratio_of(entry.name); plain.rail = std::string{entry.name}.substr(0, std::string{entry.name}.find('-')); - plain.label = std::string{"Skrz "} + entry.name + format(" a %.3f", entry.half) + " crossing (click at a crossing of two straights)"; + plain.label = std::string{"Skrz "} + entry.name + format(" a %.3f", entry.half) + " crossing (click at a crossing of two straights, or drag it out on a straight)"; result.push_back(plain); } return result; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 6799d35b..674d6364 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -106,6 +106,8 @@ class editor_track static void commit_parameters(TTrack &Track); static glm::dvec3 point_position(TTrack const &Track, point_ref const &Point); + // the direction of the path in the plan at its end, into the path + static glm::dvec2 end_direction(segment_data const &Path, point_kind const Kind); static void move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position); static void translate(TTrack &Track, glm::dvec3 const &Offset); static void rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle); @@ -342,6 +344,8 @@ class editor_track double transition{0.0}; }; std::vector arcs; + // the arcs read from the elements of the stored designs the paths lie along, as they were laid, not approximated by the paths + bool laid{false}; // the parameters of the vertex in the alignment kept in the scenery, in the direction of the run, when the paths follow one std::optional designed; }; @@ -451,6 +455,7 @@ class editor_track bool own{false}; bool movable{false}; // the frog with a movable nose double heel{0.0}; // where the moving blades end, 0: as long as the flangeway behind them needs + double frog_straight{0.0}; // the straight the diverging track ends with, through the frog; 0: its curve runs through the frog double bladethrow{0.0}; // how far the tips of the blades move over, 0: the usual throw int design{-1}; // in the list of the designs the template was built from double middle{0.0}; // a double slip with the blades outside of the diamond: the radius in the middle of its slip curves, below 0 a straight there diff --git a/editor/editorTrackShape.cpp b/editor/editorTrackShape.cpp index b56d27ae..81ecb0b3 100644 --- a/editor/editorTrackShape.cpp +++ b/editor/editorTrackShape.cpp @@ -680,7 +680,9 @@ std::optional editor_track::bend_at(TTrack const &Neighbour, result.direction = glm::normalize(chord) * sign; return result; } - bezier const curve{path}; + // on the piece at the end where the path has them: the path only approximates them, its curvature off at its ends, R 300.9 at + // the end of the diverging track of a switch of R 300 + bezier const curve{path.pieces.empty() ? path : atend ? path.pieces.back() : path.pieces.front()}; auto const first{curve.first(atend ? 1.0 : 0.0)}; auto const second{curve.second(atend ? 1.0 : 0.0)}; auto const speed{std::hypot(first.x, first.z)}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 8b8b50bb..b8083592 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -4500,12 +4500,6 @@ msgstr ", ostrzej niż rozjazd (R %.0f m)" msgid ", the switch moved %.2f m" msgstr ", rozjazd przesunięty o %.2f m" -msgid "take back the last piece" -msgstr "cofa ostatni odcinek" - -msgid "The last piece was taken back" -msgstr "Ostatni odcinek cofnięty" - msgid "R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m" msgstr "R %.0f m nie sięga kursora, łuk przechodzi przez niego z R %.0f m" @@ -6122,6 +6116,12 @@ msgstr "Niweleta dziedziczona z rozjazdu %s (%s): %.2f m, %+.2f ‰" msgid "No tracks to put onto the terrain" msgstr "Brak torów do położenia na terenie" +msgid "All the tracks are fitted to the terrain already" +msgstr "Wszystkie tory są już dopasowane do terenu" + +msgid "All the tracks are fitted to the terrain; a track changed since goes onto it again" +msgstr "Wszystkie tory są dopasowane do terenu; tor zmieniony od tego czasu pójdzie na niego znowu" + msgid "\n%zu lines part from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names them)" msgstr "\n%zu linii odchodzi od NMT o ponad 0.5 m: tam NMT albo tor się nie zgadza, teren idzie za torami (log je wymienia)" @@ -6131,8 +6131,8 @@ msgstr "Inne profile ustawisz we właściwościach toru" msgid "Rail profile %s set on %d tracks" msgstr "Profil szyny %s ustawiony na torach: %d" -msgid "Scenery moved %+.2f m; grade lines fitted to the terrain: %zu, refused by the checks: %zu, tracks left: %zu" -msgstr "Sceneria przesunięta o %+.2f m; niwelet dopasowanych do terenu: %zu, odrzuconych przez kontrole: %zu, zostało torów: %zu" +msgid "Terrain model offset %+.2f m to the scenery; grade lines fitted to the terrain: %zu, refused by the checks: %zu, tracks left: %zu" +msgstr "Model NMT przesunięty o %+.2f m do scenerii; niwelet dopasowanych do terenu: %zu, odrzuconych przez kontrole: %zu, zostało torów: %zu" msgid "Set...##railprofile" msgstr "Ustaw...##railprofile" @@ -6296,8 +6296,8 @@ msgstr ", bez nazwy, pominięte %d" msgid "The scenery is there" msgstr "Ustaw położenie" -msgid "The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported" -msgstr "NMT wczytuje się wzdłuż torów, a tory idą na niego, niezależnie od tego, na jakiej wysokości zbudowano scenerię: korona podsypki na wysokości NMT, niweleta każdej linii dopasowana do terenu w granicach norm linii, rozjazdy i tory obok na jednej niwelecie, torowisko i skarpy wzdłuż nich. Gdzie NMT się myli, tory zostają na swojej linii i jest to zgłaszane" +msgid "The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported. A turntable stays level in its pit at the height of the tracks running up to it, which reach it level. The tracks fitted already, by it or with the profile, and not changed since stay as they are, and the new lines meet them" +msgstr "NMT wczytuje się wzdłuż torów, a tory idą na niego, niezależnie od tego, na jakiej wysokości zbudowano scenerię: korona podsypki na wysokości NMT, niweleta każdej linii dopasowana do terenu w granicach norm linii, rozjazdy i tory obok na jednej niwelecie, torowisko i skarpy wzdłuż nich. Gdzie NMT się myli, tory zostają na swojej linii i jest to zgłaszane. Obrotnica zostaje pozioma w swoim kanale na wysokości dobiegających torów, które dochodzą do niej poziomo. Tory już dopasowane, tym przyciskiem albo w profilu, i od tego czasu niezmienione zostają, jak są, a nowe niwelety się z nimi łączą" msgid "Load the terrain under the tracks, they stay" msgstr "Wczytaj teren pod tory (tory zostają)" @@ -6746,8 +6746,8 @@ msgstr "Wszystkie tory schodzą albo wchodzą na model terenu, który wczytuje s msgid "Or:" msgstr "Albo:" -msgid "Only the tracks laid or changed since the last time; the ones fitted stay as they are" -msgstr "Tylko tory ułożone albo zmienione od ostatniego razu; dopasowane zostają, jak są" +msgid "The tracks laid or changed since the last time go onto the terrain model (%zu of %zu); the ones fitted stay as they are" +msgstr "Na NMT idą tory ułożone albo zmienione od ostatniego razu (%zu z %zu); dopasowane zostają, jak są" msgid "The tracks stay at their heights and the terrain comes up or down to them" msgstr "Tory zostają na swojej wysokości, a teren podnosi się albo obniża do nich" @@ -6755,11 +6755,11 @@ msgstr "Tory zostają na swojej wysokości, a teren podnosi się albo obniża do msgid "The terrain to some of the tracks only: select them (F3), Terrain in the inspector" msgstr "Teren tylko pod wybrane tory: zaznacz je (F3), sekcja Teren w inspektorze" -msgid "Make the terrain around the camera and ahead of the laying" -msgstr "Twórz teren wokół kamery i przed kładzionym torem" +msgid "Terrain of the model right away while laying" +msgstr "Teren z NMT od razu podczas kładzenia" -msgid "Where the terrain of the scenery ends, the terrain model is made on around the camera, and around the cursor of a track laid past it; the chunks there are stay as they are" -msgstr "Tam, gdzie kończy się teren scenerii, NMT jest dokładany wokół kamery i wokół kursora toru kładzionego poza nim; istniejące kafle zostają bez zmian" +msgid "Where the terrain of the scenery ends, the terrain model is made under the tracks as they're laid, around the cursor from the first point placed; the chunks there are stay as they are. Off, it's made only when asked for" +msgstr "Tam, gdzie kończy się teren scenerii, teren z NMT powstaje pod układanymi torami, wokół kursora od pierwszego postawionego punktu; istniejące kafle zostają bez zmian. Wyłączone: teren powstaje tylko na żądanie" msgid "Height offset of the model" msgstr "Przesunięcie wysokości modelu" @@ -6776,11 +6776,11 @@ msgstr "Niwelety dopasowane do terenu: %zu z %zu" msgid "A track is fitted when its grade line was fitted to the terrain model or applied with the profile, and it wasn't changed since" msgstr "Tor jest dopasowany, gdy jego niweletę dopasowano do NMT albo zastosowano w profilu i od tego czasu się nie zmienił" -msgid "Only the ones not fitted (%zu of %zu)" -msgstr "Tylko niedopasowane (%zu z %zu)" +msgid "All anew, the fitted ones too" +msgstr "Wszystkie od nowa, z dopasowanymi" -msgid "The same for the tracks whose grade line isn't fitted yet, or was changed since: the ones fitted stay as they are and the new lines meet them; the scenery isn't moved" -msgstr "To samo dla torów, których niweleta nie jest jeszcze dopasowana albo zmieniła się od tego czasu: dopasowane zostają bez zmian, a nowe niwelety się z nimi łączą; sceneria nie jest przesuwana" +msgid "All the tracks are fitted to the terrain model again, the ones fitted already too: their grade lines may change; the scenery isn't moved" +msgstr "Wszystkie tory są dopasowywane do NMT od nowa, także te już dopasowane: ich niwelety mogą się zmienić; sceneria nie jest przesuwana" msgid "By hand: the model as it is, the tracks stay" msgstr "Ręcznie: NMT bez zmian, tory zostają" @@ -9236,6 +9236,12 @@ msgstr[0] "\n%zu linia odchodzi od NMT o ponad 0.5 m: tam NMT albo tor się nie msgstr[1] "\n%zu linie odchodzą od NMT o ponad 0.5 m: tam NMT albo tor się nie zgadza, teren idzie za torami (log je wymienia)" msgstr[2] "\n%zu linii odchodzi od NMT o ponad 0.5 m: tam NMT albo tor się nie zgadza, teren idzie za torami (log je wymienia)" +msgid "\n%zu turntable set level at the height of the tracks running up to it, which reach it level" +msgid_plural "\n%zu turntables set level at the height of the tracks running up to them, which reach them level" +msgstr[0] "\n%zu obrotnica ustawiona poziomo na wysokości dobiegających torów, które dochodzą do niej poziomo" +msgstr[1] "\n%zu obrotnice ustawione poziomo na wysokości dobiegających torów, które dochodzą do nich poziomo" +msgstr[2] "\n%zu obrotnic ustawionych poziomo na wysokości dobiegających torów, które dochodzą do nich poziomo" + msgid "Stopped: the grade lines split off %zu path from %zu tracks, more than they should; Ctrl+Z takes the changes back" msgid_plural "Stopped: the grade lines split off %zu paths from %zu tracks, more than they should; Ctrl+Z takes the changes back" msgstr[0] "Zatrzymane: niwelety odcięły %zu tor z %zu, więcej niż powinny; Ctrl+Z cofa zmiany" @@ -11512,8 +11518,6 @@ msgstr "Jak widok z góry rysuje tory" msgid "The colour of the rails, to stand out on the imagery and the map" msgstr "Kolor szyn, żeby odcinały się od ortofotomapy i mapy" -msgid "(c) OpenStreetMap contributors, geoportal.gov.pl" -msgstr "(c) współtwórcy OpenStreetMap, geoportal.gov.pl" msgid "The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain" msgstr "Pochylenia całego toru kładzionego za jednym razem dopasowane do modelu terenu, tak jak w „Tory na teren”" @@ -11861,11 +11865,11 @@ msgstr "punkt po środku łuku, dwa ostatnie na prostej za nim" msgid "a bend of the straights, eased with a large curve" msgstr "załamanie prostych, wyokrąglone dużym łukiem" -msgid "lay the points clicked, then go on" -msgstr "kładzie kliknięte punkty i prowadzi dalej" +msgid "lay the points clicked" +msgstr "kładzie kliknięte punkty" -msgid "Lay track: each click a bend of the straights, eased with a large curve; Enter lays them and goes on from the end" -msgstr "Układanie toru: każdy klik to załamanie prostych, wyokrąglone dużym łukiem; Enter je kładzie i prowadzi tor dalej od końca" +msgid "Lay track: each click a bend of the straights, eased with a large curve; Enter lays them; a click on a free end goes on from it" +msgstr "Układanie toru: każdy klik to załamanie prostych, wyokrąglone dużym łukiem; Enter je kładzie; klik w wolny koniec prowadzi tor dalej od niego" msgid "Tab while laying" msgstr "Tab przy układaniu" @@ -11876,8 +11880,8 @@ msgstr "Następne punkty idą po środku łuku widocznego na zdjęciu, dwa ostat msgid "Type the length of the next straight, Enter puts the point" msgstr "Wpisz długość następnej prostej, Enter stawia punkt" -msgid "Laying track: lay the points clicked / take back the last point, then the last piece laid" -msgstr "Układanie toru: połóż kliknięte punkty / cofnij ostatni punkt, potem ostatni położony odcinek" +msgid "Laying track: lay the points clicked / take back the last point" +msgstr "Układanie toru: połóż kliknięte punkty / cofnij ostatni punkt" msgid "Fitting the curve to the points..." msgstr "Dopasowuję łuk do punktów..." @@ -12150,11 +12154,11 @@ msgstr "Klik na terenie albo na wolnym końcu: tu zaczyna się tor" msgid "Each next click: a bend of the line, the curve at it made by itself. The line shown isn't a track yet" msgstr "Każdy następny klik: załamanie linii, łuk na nim powstaje sam. Pokazana linia nie jest jeszcze torem" -msgid "Lays the track clicked; the clicks after it go on from its end" -msgstr "Układa wyklikany tor; kolejne kliki idą dalej od jego końca" +msgid "Lays the track clicked; to go on from its end, click the end" +msgstr "Układa wyklikany tor; żeby iść dalej od jego końca, kliknij ten koniec" -msgid "Takes the last point back, then the last piece laid" -msgstr "Cofa ostatni punkt, potem ostatni ułożony odcinek" +msgid "Takes the last point back" +msgstr "Cofa ostatni punkt" msgid "Ends laying; the points not laid yet go only with the second Esc" msgstr "Kończy układanie; nieułożone punkty znikają dopiero po drugim Esc" @@ -12186,8 +12190,8 @@ msgstr "Położenie: gdzie leży sceneria, klik na mapie albo wpisane miejsce" msgid "Load along the tracks, or around the camera: the model comes from geoportal.gov.pl" msgstr "Wczytaj wzdłuż torów albo wokół kamery: model pochodzi z geoportal.gov.pl" -msgid "Tracks onto the terrain: all the tracks go onto the model; the terrain to some tracks only: select them, Terrain in the inspector" -msgstr "Tory na teren: wszystkie tory idą na model; teren tylko pod wybrane tory: zaznacz je, sekcja Teren w inspektorze" +msgid "Tracks onto the terrain: the tracks not fitted yet go onto the model, the ones fitted stay; the terrain to some tracks only: select them, Terrain in the inspector" +msgstr "Tory na teren: tory jeszcze niedopasowane idą na model, dopasowane zostają; teren tylko pod wybrane tory: zaznacz je, sekcja Teren w inspektorze" msgid "Takes the changes back" msgstr "Cofa zmiany" @@ -12316,77 +12320,77 @@ msgstr[0] "Napęd %zu rozjazdu przestawiony na ostrza iglic" msgstr[1] "Napędy %zu rozjazdów przestawione na ostrza iglic" msgstr[2] "Napędy %zu rozjazdów przestawione na ostrza iglic" -msgid "Press and drag: a switch going on from this free end, sideways its side" -msgstr "Wciśnij i przeciągnij: rozjazd od tego wolnego końca, w bok jego strona" +msgid "Switch %s set to the diverging track, H throws it back" +msgstr "Rozjazd %s ustawiony na bok, H przekłada go z powrotem" -msgid "Press and drag: a switch with no track, along the direction of its main track, then sideways its side" -msgstr "Wciśnij i przeciągnij: rozjazd bez toru, wzdłuż kierunek toru zasadniczego, potem w bok jego strona" +msgid "Switch %s set to the main track, H throws it over" +msgstr "Rozjazd %s ustawiony na wprost, H przekłada go na bok" -msgid "Drag along the main track of the switch, then sideways to the side of the diverging one" -msgstr "Przeciągnij wzdłuż toru zasadniczego rozjazdu, potem w bok na stronę toru zwrotnego" +msgid "Throw (H)" +msgstr "Przełóż (H)" -msgid "Switch %s laid on from %s, its other ends free: lay on from them" -msgstr "Rozjazd %s ułożony od %s, pozostałe końce wolne: kładź od nich dalej" +msgid "Throw it over" +msgstr "Przełóż" -msgid "Switch %s laid with no track, its ends free: lay on from them" -msgstr "Rozjazd %s ułożony bez toru, końce wolne: kładź od nich dalej" +msgid "Set to the diverging track: throws it over to the main one. In the editor only, to see the blades and the route; the simulation starts with the switches set to the main track" +msgstr "Ustawiony na bok: przekłada go na wprost. Tylko w edytorze, żeby zobaczyć iglice i przebieg; symulacja zaczyna z rozjazdami na wprost" -msgid "Press and drag: this double slip is replaced by the one chosen" -msgstr "Wciśnij i przeciągnij: ten rozjazd krzyżowy zostanie zastąpiony wybranym" +msgid "Set to the main track: throws it over to the diverging one. In the editor only, to see the blades and the route; the simulation starts with the switches set to the main track" +msgstr "Ustawiony na wprost: przekłada go na bok. Tylko w edytorze, żeby zobaczyć iglice i przebieg; symulacja zaczyna z rozjazdami na wprost" -msgid "Press and drag: it goes here" -msgstr "Wciśnij i przeciągnij: stanie tutaj" +msgid "Throw the selected switch over to its other path, in the editor only" +msgstr "Przekłada zaznaczony rozjazd na drugi tor, tylko w edytorze" -msgid "Press+drag at a crossing" -msgstr "Wciśnij i przeciągnij na skrzyżowaniu" +msgid "A curve seen on the photo: click points along its middle, the curve ending at the last one; one of two or three radii comes out a compound curve" +msgstr "Łuk widoczny na zdjęciu: klikaj punkty wzdłuż jego środka, łuk kończy się na ostatnim; łuk o dwóch albo trzech promieniach wychodzi jako koszowy" -msgid "of two straights" -msgstr "dwóch prostych" +msgid "The points clicked next go along the middle of a curve seen on the photo, which ends at the last one; Tab again: back to the bends" +msgstr "Następne punkty idą po środku łuku widocznego na zdjęciu, który kończy się na ostatnim; ponownie Tab: z powrotem załamania" -msgid "Press+drag on the ground" -msgstr "Wciśnij i przeciągnij na terenie" +msgid "Curve: click points along the middle of the track, the curve ends at the last one. Tab: back to the straights" +msgstr "Łuk: klikaj punkty po środku toru, łuk kończy się na ostatnim. Tab: z powrotem proste" -msgid "a switch with no track, on from a free end near it" -msgstr "rozjazd bez toru, od pobliskiego wolnego końca" +msgid "Click points along the middle of a curve seen on the photo: the curve, compound or with a straight in it, is fitted to them and ends at the last one, heading as they do there. It comes from the straight before it, of the points clicked before or of the free end, and goes on with the curve of a free end, a switch too, where the points turn its way" +msgstr "Klikaj punkty po środku łuku widocznego na zdjęciu: łuk, także koszowy albo z prostą w środku, zostanie do nich dopasowany i kończy się na ostatnim, w kierunku, w jakim tam biegną. Wychodzi z prostej przed nim, wyznaczonej punktami klikniętymi wcześniej albo wolnym końcem, a gdy punkty skręcają w stronę łuku wolnego końca, także rozjazdu, ten łuk idzie dalej" -msgid "the direction of the main track, held from 4 m on" -msgstr "kierunek toru zasadniczego, od 4 m zatrzaśnięty" +msgid "Click points along the middle of the track on the photo; the curve ends at the last one" +msgstr "Klikaj punkty po środku toru na zdjęciu; łuk kończy się na ostatnim" -msgid "Back to the start" -msgstr "Z powrotem do początku" +msgid "a point along the middle of the curve, which ends at the last one" +msgstr "punkt po środku łuku, który kończy się na ostatnim" -msgid "the direction free again" -msgstr "kierunek znów wolny" +msgid "Press and drag: a switch on this track, along sets its direction, sideways its side. Click: select the track" +msgstr "Wciśnij LPM i przeciągnij: rozjazd na tym torze, wzdłuż ustala kierunek, w bok stronę. Kliknięcie: zaznacza tor" -msgid "Drag at a crossing" -msgstr "Przeciągnij na skrzyżowaniu" +msgid "Press+drag on a track" +msgstr "Wciśnij i przeciągnij na torze" -msgid "Drag at a crossing of two straights" -msgstr "Przeciągnij na skrzyżowaniu dwóch prostych" +msgid "a switch on it: along its direction, sideways its side" +msgstr "rozjazd na nim: wzdłuż kierunek, w bok strona" -msgid "Drag at a crossing of two straights, on the side of the tracks to join" -msgstr "Przeciągnij na skrzyżowaniu dwóch prostych, po stronie torów do połączenia" +msgid "Press+drag on another track" +msgstr "Wciśnij i przeciągnij na innym torze" -msgid "The straights cross at 1:%.2f, %s is made for 1:%.2f: %s fits that crossing" -msgstr "Proste krzyżują się pod 1:%.2f, %s jest na 1:%.2f: do tego skrzyżowania pasuje %s" +msgid "a switch on it" +msgstr "rozjazd na nim" -msgid "The straights cross at 1:%.2f, %s is made for 1:%.2f and no slip template is made for that" -msgstr "Proste krzyżują się pod 1:%.2f, %s jest na 1:%.2f i żaden szablon rozjazdu krzyżowego nie jest na taki skos" +msgid "Puts switches into a straight or a curve: press on it and drag along the way the switch goes. Edits the geometry of the selected switch" +msgstr "Wstawia rozjazdy w prostą albo łuk: wciśnij LPM na torze i przeciągnij w stronę, w którą idzie rozjazd. Edytuje geometrię zaznaczonego rozjazdu" -msgid "Drag at a crossing of two straights at its skos, on the side of the tracks to join" -msgstr "Przeciągnij na skrzyżowaniu dwóch prostych pod jego skosem, po stronie torów do połączenia" +msgid "The diverging track of the basic switch ends with a straight of %.1f m through the frog: the switches bent in table 11.9 have their curve run through it; bent, the frog has to be made to the curve" +msgstr "Tor zwrotny rozjazdu podstawowego kończy się prostą %.1f m przez krzyżownicę: rozjazdy łukowane z tabeli 11.9 mają łuk przechodzący przez krzyżownicę; po wygięciu krzyżownica musi być wykonana pod łuk" -msgid "Drag at a crossing of two straights at its skos" -msgstr "Przeciągnij na skrzyżowaniu dwóch prostych pod jego skosem" +msgid "Bent up to the frog, as the DB bends the switches with a straight frog: both tracks keep straight over its last %.1f m, the curvature jumps there; table 11.9 lists no such bent switch" +msgstr "Wygięty do krzyżownicy, jak DB łukuje rozjazdy z prostą krzyżownicą: oba tory zostają proste na ostatnich %.1f m, krzywizna skacze tam; tabela 11.9 nie przewiduje takiego rozjazdu łukowego" -msgid "Rkp or Rkpd at a crossing of two straights at its skos" -msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowaniu dwóch prostych pod jego skosem" +msgid "Track %s going on from the switch fitted to its end, which leaves the curve straight over the frog: %.1f cm off it, turned by %.2f deg" +msgstr "Tor %s za rozjazdem dociągnięty do jego końca, który nad krzyżownicą schodzi prosto z łuku: %.1f cm od niego, obrót o %.2f°" -msgid "of two straights at the skos of the slip: it goes there" -msgstr "dwóch prostych pod skosem rozjazdu: tam stanie" +msgid "The slip laid with its crossing track at 1:%.1f, the ends of that track free: lay on from them" +msgstr "Rozjazd krzyżowy ułożony z torem przecinającym pod 1:%.1f, końce tego toru wolne: kładź od nich dalej" -msgid "of two straights at the skos of the slip" -msgstr "dwóch prostych pod skosem rozjazdu" +msgid "with nothing crossing it: the slip with its crossing track, which goes through the quarter of the cursor" +msgstr "gdy nic jej nie przecina: rozjazd krzyżowy z torem przecinającym, który idzie przez ćwiartkę kursora" msgid "The curvature of a track through the slip turns from one side to the other within it, which isn't allowed (9.7 pkt 12)" msgstr "Krzywizna toru przez rozjazd krzyżowy zmienia stronę w jego obrębie, co jest niedopuszczalne (9.7 pkt 12)" @@ -12424,14 +12428,95 @@ msgstr "Wciśnij i przeciągnij na łuku" msgid "the slip bent into it, in a side track only (11.4 pkt 4b)" msgstr "rozjazd krzyżowy wygięty w łuk, tylko w torze bocznym (11.4 pkt 4b)" +msgid "Rkp or Rkpd dragged out on one straight with its crossing track, at a crossing of two straights at its skos, or bent into a curve of a side track" +msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd): przeciągnięty na jednej prostej z torem przecinającym, na skrzyżowaniu dwóch prostych pod jego skosem albo wygięty w łuk toru bocznego" + msgid "Drag on a curve" msgstr "Przeciągnij na łuku" msgid "bent into it, in a side track only (11.4 pkt 4b)" msgstr "wygięty w łuk, tylko w torze bocznym (11.4 pkt 4b)" -msgid "Rkp or Rkpd at a crossing of two straights at its skos, or bent into a curve of a side track" -msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowaniu dwóch prostych pod jego skosem albo wygięty w łuk toru bocznego" +msgid "Press+drag on a straight" +msgstr "Wciśnij i przeciągnij na prostej" + +msgid "The straights cross at 1:%.2f, %s is made for 1:%.2f and no slip template for that: drag the slip out on one straight, its crossing track comes along at its skos" +msgstr "Proste krzyżują się pod 1:%.2f, %s jest na 1:%.2f i żaden szablon rozjazdu krzyżowego tu nie pasuje: przeciągnij rozjazd na jednej prostej, tor przecinający przyjdzie z nim pod jego skosem" + +msgid "The straights cross at 1:%.2f, %s is made for 1:%.2f: %s fits that crossing" +msgstr "Proste krzyżują się pod 1:%.2f, %s jest na 1:%.2f: do tego skrzyżowania pasuje %s" + +msgid "Drag on a straight" +msgstr "Przeciągnij na prostej" + +msgid "the slip with its crossing track, through the quarter of the cursor" +msgstr "rozjazd krzyżowy z torem przecinającym, przez ćwiartkę kursora" + +msgid "of two straights at the skos of the slip: it goes there" +msgstr "dwóch prostych pod skosem rozjazdu: tam stanie" + +msgid "Drag it out on one straight: its crossing track comes along at its skos, through the quarter of the cursor" +msgstr "Przeciągnij go na jednej prostej: tor przecinający przyjdzie z nim pod jego skosem, przez ćwiartkę kursora" + +msgid "Or drag at a crossing of two straights at its skos, on the side of the tracks to join" +msgstr "Albo przeciągnij na skrzyżowaniu dwóch prostych pod jego skosem, po stronie torów do połączenia" + +msgid "Or drag at a crossing of two straights at its skos" +msgstr "Albo przeciągnij na skrzyżowaniu dwóch prostych pod jego skosem" + +msgid "Press+drag at a crossing" +msgstr "Wciśnij i przeciągnij na skrzyżowaniu" + +msgid "of two straights at the skos of the slip" +msgstr "dwóch prostych pod skosem rozjazdu" + +msgid "Press and drag: a switch going on from this free end, sideways its side" +msgstr "Wciśnij i przeciągnij: rozjazd od tego wolnego końca, w bok jego strona" + +msgid "Press and drag: a switch with no track, along the direction of its main track, then sideways its side" +msgstr "Wciśnij i przeciągnij: rozjazd bez toru, wzdłuż kierunek toru zasadniczego, potem w bok jego strona" + +msgid "Drag along the main track of the switch, then sideways to the side of the diverging one" +msgstr "Przeciągnij wzdłuż toru zasadniczego rozjazdu, potem w bok na stronę toru zwrotnego" + +msgid "Switch %s laid on from %s, its other ends free: lay on from them" +msgstr "Rozjazd %s ułożony od %s, pozostałe końce wolne: kładź od nich dalej" + +msgid "Switch %s laid with no track, its ends free: lay on from them" +msgstr "Rozjazd %s ułożony bez toru, końce wolne: kładź od nich dalej" + +msgid "Press and drag: this double slip is replaced by the one chosen" +msgstr "Wciśnij i przeciągnij: ten rozjazd krzyżowy zostanie zastąpiony wybranym" + +msgid "Press and drag: it goes here" +msgstr "Wciśnij i przeciągnij: stanie tutaj" + +msgid "Press and drag: the slip with its crossing track, through the quarter of the cursor" +msgstr "Wciśnij i przeciągnij: rozjazd krzyżowy z torem przecinającym, przez ćwiartkę kursora" + +msgid "of two straights" +msgstr "dwóch prostych" + +msgid "Press+drag on the ground" +msgstr "Wciśnij i przeciągnij na terenie" + +msgid "a switch with no track, on from a free end near it" +msgstr "rozjazd bez toru, od pobliskiego wolnego końca" + +msgid "the direction of the main track, held from 4 m on" +msgstr "kierunek toru zasadniczego, od 4 m zatrzaśnięty" + +msgid "Back to the start" +msgstr "Z powrotem do początku" + +msgid "the direction free again" +msgstr "kierunek znów wolny" + +msgid "Drag at a crossing" +msgstr "Przeciągnij na skrzyżowaniu" + +msgid "Drag at a crossing of two straights" +msgstr "Przeciągnij na skrzyżowaniu dwóch prostych" msgid "The slip is replaced by its two tracks crossing, Shift+Del removes all of it" msgstr "Rozjazd krzyżowy zastąpiony skrzyżowaniem jego dwóch torów, Shift+Del usuwa wszystko" @@ -12445,8 +12530,149 @@ msgstr "Tor przecinający rozjazdu krzyżowego jest dalej połączony: rozjazd z msgid "Slip %s moved %.2f m along its track, its crossing track with it" msgstr "Rozjazd krzyżowy %s przesunięty o %.2f m wzdłuż toru, razem z torem przecinającym" +msgid "Drag at a crossing of two straights, on the side of the tracks to join" +msgstr "Przeciągnij na skrzyżowaniu dwóch prostych, po stronie torów do połączenia" + +msgid "The straights cross at 1:%.2f, %s is made for 1:%.2f and no slip template is made for that" +msgstr "Proste krzyżują się pod 1:%.2f, %s jest na 1:%.2f i żaden szablon rozjazdu krzyżowego nie jest na taki skos" + +msgid "Drag at a crossing of two straights at its skos, on the side of the tracks to join" +msgstr "Przeciągnij na skrzyżowaniu dwóch prostych pod jego skosem, po stronie torów do połączenia" + +msgid "Drag at a crossing of two straights at its skos" +msgstr "Przeciągnij na skrzyżowaniu dwóch prostych pod jego skosem" + +msgid "Rkp or Rkpd at a crossing of two straights at its skos" +msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowaniu dwóch prostych pod jego skosem" + +msgid "Rkp or Rkpd at a crossing of two straights at its skos, or bent into a curve of a side track" +msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowaniu dwóch prostych pod jego skosem albo wygięty w łuk toru bocznego" + +msgid "Base layers, under the scenery" +msgstr "Podkłady, pod scenerią" + +msgid "Show or hide the layer" +msgstr "Pokaż lub ukryj warstwę" + +msgid "Layers: %s" +msgstr "Warstwy: %s" + +msgid "Terms of the service:" +msgstr "Warunki usługi:" + +msgid "Down" +msgstr "W dół" + +msgid "Orthophoto (geoportal.gov.pl)" +msgstr "Ortofotomapa (geoportal.gov.pl)" + +msgid "Add a WMS layer" +msgstr "Dodaj warstwę WMS" + +msgid "The address of a WMS service drawing in PUWG 1992 (EPSG:2180). Its tiles are kept in memory only and asked for only around the camera; what the service allows is up to you, its terms are shown below" +msgstr "Adres usługi WMS rysującej w PUWG 1992 (EPSG:2180). Jej kafle są trzymane tylko w pamięci i pobierane tylko wokół kamery; za korzystanie zgodne z warunkami usługi odpowiadasz sam, jej warunki są pokazane niżej" + +msgid "Read the layers" +msgstr "Odczytaj warstwy" + +msgid "Reading the capabilities of the service..." +msgstr "Odczytywanie opisu usługi..." + +msgid "The service can't be read: %s" +msgstr "Nie da się odczytać usługi: %s" + +msgid "The service doesn't draw in PUWG 1992 (EPSG:2180), the editor can't lay it out" +msgstr "Usługa nie rysuje w PUWG 1992 (EPSG:2180), edytor nie umie jej rozłożyć" + +msgid "Filter the layers" +msgstr "Filtruj warstwy" + msgid "Behind the switch the curvature changes at once by a sudden change of the cant deficiency of %.0f mm, over the %.0f mm allowed at %.0f km/h (table 9.15)" msgstr "Za rozjazdem krzywizna zmienia się skokowo: nagła zmiana niedomiaru przechyłki %.0f mm, ponad dopuszczalne %.0f mm przy %.0f km/h (tab. 9.15)" msgid "The curve behind the switch begins %.1f m after the end of its curve, closer than the %.1f m which take the sudden changes of the cant deficiency apart: together %.0f mm, over the %.0f mm allowed at %.0f km/h (tables 9.15, 9.16)" msgstr "Łuk za rozjazdem zaczyna się %.1f m za końcem jego łuku, bliżej niż %.1f m odcinka pośredniego, który rozdziela nagłe zmiany niedomiaru przechyłki: łącznie %.0f mm, ponad dopuszczalne %.0f mm przy %.0f km/h (tab. 9.15, 9.16)" + +msgid "Remove the selected (Del)" +msgstr "Usuń zaznaczone (Del)" + +msgid "Takes the selected chunks out of the terrain of the scenery, and out of its files on save; Ctrl+Z brings them back" +msgstr "Usuwa zaznaczone kafle z terenu scenerii, a przy zapisie także z jego plików; Ctrl+Z je przywraca" + +msgid "%zu chunk removed, Ctrl+Z brings it back" +msgid_plural "%zu chunks removed, Ctrl+Z brings them back" +msgstr[0] "Usunięto %zu kafel, Ctrl+Z go przywraca" +msgstr[1] "Usunięto %zu kafle, Ctrl+Z je przywraca" +msgstr[2] "Usunięto %zu kafli, Ctrl+Z je przywraca" + +msgid "Drag on past its end" +msgstr "Przeciągnij dalej za jego koniec" + +msgid "bends the main track toward the cursor" +msgstr "wygina tor zasadniczy w stronę kursora" + +msgid "the diverging track the other way or the same way" +msgstr "tor zwrotny w drugą stronę albo w tę samą" + +msgid "At a free end" +msgstr "Na wolnym końcu" + +msgid "the main track leads into it, with Shift the diverging one" +msgstr "dochodzi do niego tor zasadniczy, z Shift zwrotny" + +msgid "Switch %s laid on from %s bent into R %.0f m, %s" +msgstr "Rozjazd %s ułożony od %s, wygięty w R %.0f m %s" + +msgid "one-sided" +msgstr "jednostronnie" + +msgid "two-sided" +msgstr "dwustronnie" + +msgid "one-sided: main R %.0f m, diverging R %.0f m" +msgstr "jednostronny: zasadniczy R %.0f m, zwrotny R %.0f m" + +msgid "two-sided: main R %.0f m, diverging R %.0f m" +msgstr "dwustronny: zasadniczy R %.0f m, zwrotny R %.0f m" + +msgid "Click: select this switch, drag: slide it along the curve" +msgstr "LPM: zaznacz ten rozjazd, przeciągnij: przesuń go wzdłuż łuku" + +msgid "Click: edit this switch, drag: slide it along the curve" +msgstr "LPM: edytuj ten rozjazd, przeciągnij: przesuń go wzdłuż łuku" + +msgid "Slide along the curve" +msgstr "Przesuń wzdłuż łuku" + +msgid "Moves the switch along the curve it's bent into by the distance typed, + towards the frog; the tracks of the curve on both sides get shorter or longer, one taken up whole goes and the switch joins what it joined, P4 of another switch say. Dragging the switch with the mouse does the same" +msgstr "Przesuwa rozjazd wzdłuż łuku, w który jest wygięty, o wpisaną odległość, + w stronę krzyżownicy; tory łuku po obu stronach skracają się albo wydłużają, tor zajęty w całości znika, a rozjazd łączy się z tym, z czym on był połączony, np. z P4 innego rozjazdu. To samo robi przeciągnięcie rozjazdu myszą" + +msgid "Room on the curve: %.2f m back, %.2f m ahead" +msgstr "Miejsce na łuku: %.2f m do tyłu, %.2f m do przodu" + +msgid "Switch %s moved %.2f m along the curve" +msgstr "Rozjazd %s przesunięty o %.2f m wzdłuż łuku" + +msgid "The switch goes %.2f m back and %.2f m on along the curve at most" +msgstr "Rozjazd przesunie się wzdłuż łuku najwyżej o %.2f m do tyłu i %.2f m do przodu" + +msgid "Press and drag: a switch going on from this end of the switch, sideways its side" +msgstr "Wciśnij i przeciągnij: rozjazd od tego końca rozjazdu, w bok jego strona" + +msgid "%s taken up whole, the switch joins what it joined" +msgstr "%s zajęty w całości, rozjazd łączy się z tym, z czym on był połączony" + +msgid "Camera to the plot" +msgstr "Kamera do profilu" + +msgid "Moves the camera to the part of the route the plot shows" +msgstr "Przenosi kamerę na odcinek trasy widoczny na profilu" + +msgid "Plot to the camera" +msgstr "Profil do kamery" + +msgid "Moves the plot to the part of the route where the camera is, its zoom kept" +msgstr "Przesuwa profil na miejsce trasy, gdzie jest kamera, bez zmiany powiększenia" + +msgid "The switch drawn to the joint of the tracks" +msgstr "Rozjazd dosunięty do styku torów" diff --git a/launcher/scenery_wizard.cpp b/launcher/scenery_wizard.cpp index 092b4b91..4ba86011 100644 --- a/launcher/scenery_wizard.cpp +++ b/launcher/scenery_wizard.cpp @@ -13,7 +13,6 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorFormat.hpp" #include "editor/editorGeoMap.hpp" #include "editor/editorOrthophoto.hpp" -#include "editor/editorSettings.hpp" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/translation.h" @@ -183,18 +182,9 @@ bool ui::scenerywizard_panel::create_scenery() std::snprintf(reference, sizeof(reference), "//$g PUWG1992 %.3f %.3f", m_map.point().x / 1000.0, m_map.point().y / 1000.0); if (m_realistic) { - auto const &stored{EditorSettings.orthophoto()}; - editor_orthophoto::config map; - map.radius = stored.radius; - map.height = stored.height; - map.opacity = stored.opacity; - map.year = stored.year; - map.hires = stored.hires; - map.drape = stored.drape; - map.lift = stored.lift; - map.in_scene = stored.in_scene; + // the orthophoto on; it's shown as the settings of the editor have it, its origin is the one of //$g output << reference << "\r\n"; - output << "//$o " << editor_orthophoto::mark(map, true, false) << "\r\n"; + output << "//$o " << editor_orthophoto::mark(editor_orthophoto::config{}, true, false) << "\r\n"; } else std::snprintf(reference, sizeof(reference), "fictional"); diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 1de01345..89a3b209 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -64,13 +64,17 @@ editor_sleepers_hidden() return EditorModeFlag && Global.EditorOrtho && Global.EditorOrthoTracks > 1; } -// the top view draws the scenery as far as the tracks; its models keep their own ranges +// far off the camera the tracks of the editor are drawn alone, the terrain, the models and the rest only as far as the +// scenery reaches in the game; the top view measures it off its centre on the ground, its camera being high above it bool -editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) +editor_far_tracks_only(bool const Apply, scene::bounding_area const &Area, glm::dvec3 const &Camera, double const SceneryLimit) { - if (false == Apply || Global.EditorOrtho) + if (false == Apply) return false; - double nearest = glm::length(Cell->m_area.center - Camera) - static_cast(Cell->m_area.radius); + auto offset{Area.center - Camera}; + if (Global.EditorOrtho) + offset.y = 0.0; + double nearest = glm::length(offset) - static_cast(Area.radius); if (nearest < 0.0) nearest = 0.0; return nearest > SceneryLimit; @@ -2637,6 +2641,11 @@ void opengl33_renderer::Render(section_sequence::iterator First, section_sequenc break; } + // the ground of the sections far off, the terrain of the editor among it, isn't drawn with the tracks reaching there + auto const scenery_limit{static_cast(Global.BaseDrawRange) * Global.fDistanceFactor + 250.0}; + bool const far_paths{EditorModeFlag && Global.editor_tracks && (m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery)}; + glm::dvec3 const camera{m_renderpass.pass_camera.position()}; + while (First != Last) { @@ -2651,8 +2660,7 @@ void opengl33_renderer::Render(section_sequence::iterator First, section_sequenc case rendermode::shadows: case rendermode::pickscenery: { - // the top view of the editor far out has the map in place of the ground - if (false == section->m_shapes.empty() && false == (EditorModeFlag && Global.EditorOrtho && Global.EditorOrthoMap)) + if (false == section->m_shapes.empty() && false == editor_far_tracks_only(far_paths, section->m_area, camera, scenery_limit)) { // since all shapes of the section share center point we can optimize out a few calls here ::glPushMatrix(); @@ -2745,7 +2753,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite auto *cell = First->second; // przeliczenia animacji torów w sektorze cell->RaAnimate(m_framestamp); - bool const tracks_only{editor_far_tracks_only(far_paths, cell, camera, scenery_limit)}; + bool const tracks_only{editor_far_tracks_only(far_paths, cell->m_area, camera, scenery_limit)}; switch (m_renderpass.draw_mode) { @@ -2855,7 +2863,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite case rendermode::color: case rendermode::shadows: { - if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) + if (editor_far_tracks_only(far_paths, cell->m_area, camera, scenery_limit)) break; // TBD, TODO: refactor in to a method to reuse in branch below? // opaque parts of instanced models -- accumulate this cell's buckets @@ -2937,7 +2945,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite } case rendermode::pickscenery: { - if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) + if (editor_far_tracks_only(far_paths, cell->m_area, camera, scenery_limit)) break; // opaque parts of instanced models // same procedure like with regular render, but each node receives custom colour used for picking. @@ -4877,6 +4885,11 @@ void opengl33_renderer::Render_Alpha(cell_sequence::reverse_iterator First, cell { // NOTE: this method is launched only during color pass therefore we don't bother with mode test here + // the cells far off with the tracks alone drawn there have nothing translucent drawn either + auto const scenery_limit{static_cast(Global.BaseDrawRange) * Global.fDistanceFactor + 250.0}; + bool const far_paths{EditorModeFlag && Global.editor_tracks}; + glm::dvec3 const camera{m_renderpass.pass_camera.position()}; + auto const distant = [&](scene::basic_cell const *Cell) { return editor_far_tracks_only(far_paths, Cell->m_area, camera, scenery_limit); }; // first pass draws elements which we know are located in section banks, to reduce vbo switching { auto first{First}; @@ -4885,7 +4898,7 @@ void opengl33_renderer::Render_Alpha(cell_sequence::reverse_iterator First, cell auto const *cell = first->second; - if (false == cell->m_shapestranslucent.empty()) + if (false == cell->m_shapestranslucent.empty() && false == distant(cell)) { // since all shapes of the cell share center point we can optimize out a few calls here ::glPushMatrix(); @@ -4911,6 +4924,11 @@ void opengl33_renderer::Render_Alpha(cell_sequence::reverse_iterator First, cell { auto const *cell = first->second; + if (distant(cell)) + { + ++first; + continue; + } // translucent parts of instanced models for (auto *instance : cell->m_instancetranslucent) @@ -4938,7 +4956,7 @@ void opengl33_renderer::Render_Alpha(cell_sequence::reverse_iterator First, cell auto const *cell = first->second; - if ((false == cell->m_traction.empty() || (false == cell->m_lines.empty()))) + if ((false == cell->m_traction.empty() || (false == cell->m_lines.empty())) && false == distant(cell)) { // since all shapes of the cell share center point we can optimize out a few calls here ::glPushMatrix(); diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 52b79a28..7bb89a44 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -52,13 +52,17 @@ editor_track_view_range(float const Range, bool const Apply) return Range > extra ? Range : extra; } -// the top view draws the scenery as far as the tracks; its models keep their own ranges +// far off the camera the tracks of the editor are drawn alone, the terrain, the models and the rest only as far as the +// scenery reaches in the game; the top view measures it off its centre on the ground, its camera being high above it bool -editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) +editor_far_tracks_only(bool const Apply, scene::bounding_area const &Area, glm::dvec3 const &Camera, double const SceneryLimit) { - if( false == Apply || Global.EditorOrtho ) + if( false == Apply ) return false; - double nearest = glm::length( Cell->m_area.center - Camera ) - static_cast( Cell->m_area.radius ); + auto offset{ Area.center - Camera }; + if( Global.EditorOrtho ) + offset.y = 0.0; + double nearest = glm::length( offset ) - static_cast( Area.radius ); if( nearest < 0.0 ) nearest = 0.0; return nearest > SceneryLimit; @@ -2005,6 +2009,11 @@ opengl_renderer::Render( section_sequence::iterator First, section_sequence::ite break; } } + // the ground of the sections far off, the terrain of the editor among it, isn't drawn with the tracks reaching there + auto const scenery_limit{ static_cast( Global.BaseDrawRange ) * Global.fDistanceFactor + 250.0 }; + bool const far_paths{ EditorModeFlag && Global.editor_tracks && ( m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery ) }; + glm::dvec3 const camera{ m_renderpass.camera.position() }; + while( First != Last ) { auto *section = *First; @@ -2017,7 +2026,7 @@ opengl_renderer::Render( section_sequence::iterator First, section_sequence::ite case rendermode::shadows: case rendermode::cabshadows: case rendermode::pickscenery: { - if( false == section->m_shapes.empty() ) { + if( false == section->m_shapes.empty() && false == editor_far_tracks_only( far_paths, section->m_area, camera, scenery_limit ) ) { // since all shapes of the section share center point we can optimize out a few calls here ::glPushMatrix(); auto const originoffset { section->m_area.center - m_renderpass.camera.position() }; @@ -2098,7 +2107,7 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator auto *cell = First->second; // przeliczenia animacji torów w sektorze cell->RaAnimate( m_framestamp ); - bool const tracks_only{ editor_far_tracks_only( far_paths, cell, camera, scenery_limit ) }; + bool const tracks_only{ editor_far_tracks_only( far_paths, cell->m_area, camera, scenery_limit ) }; switch( m_renderpass.draw_mode ) { case rendermode::color: { @@ -2245,7 +2254,7 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator case rendermode::color: case rendermode::shadows: case rendermode::cabshadows: { - if( editor_far_tracks_only( far_paths, cell, camera, scenery_limit ) ) + if( editor_far_tracks_only( far_paths, cell->m_area, camera, scenery_limit ) ) break; // TBD, TODO: refactor in to a method to reuse in branch below? // opaque parts of instanced models @@ -2289,7 +2298,7 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator break; } case rendermode::pickscenery: { - if( editor_far_tracks_only( far_paths, cell, camera, scenery_limit ) ) + if( editor_far_tracks_only( far_paths, cell->m_area, camera, scenery_limit ) ) break; // opaque parts of instanced models // same procedure like with regular render, but each node receives custom colour used for picking @@ -3531,6 +3540,11 @@ void opengl_renderer::Render_Alpha( cell_sequence::reverse_iterator First, cell_sequence::reverse_iterator Last ) { // NOTE: this method is launched only during color pass therefore we don't bother with mode test here + // the cells far off with the tracks alone drawn there have nothing translucent drawn either + auto const scenery_limit{ static_cast( Global.BaseDrawRange ) * Global.fDistanceFactor + 250.0 }; + bool const far_paths{ EditorModeFlag && Global.editor_tracks }; + glm::dvec3 const camera{ m_renderpass.camera.position() }; + auto const distant = [&]( scene::basic_cell const *Cell ) { return editor_far_tracks_only( far_paths, Cell->m_area, camera, scenery_limit ); }; // first pass draws elements which we know are located in section banks, to reduce vbo switching { auto first { First }; @@ -3538,7 +3552,7 @@ opengl_renderer::Render_Alpha( cell_sequence::reverse_iterator First, cell_seque auto const *cell = first->second; - if( false == cell->m_shapestranslucent.empty() ) { + if( false == cell->m_shapestranslucent.empty() && false == distant( cell ) ) { // since all shapes of the cell share center point we can optimize out a few calls here ::glPushMatrix(); auto const originoffset{ cell->m_area.center - m_renderpass.camera.position() }; @@ -3559,6 +3573,10 @@ opengl_renderer::Render_Alpha( cell_sequence::reverse_iterator First, cell_seque while( first != Last ) { auto const *cell = first->second; + if( distant( cell ) ) { + ++first; + continue; + } // translucent parts of instanced models for( auto *instance : cell->m_instancetranslucent ) { Render_Alpha( instance ); } @@ -3588,7 +3606,8 @@ opengl_renderer::Render_Alpha( cell_sequence::reverse_iterator First, cell_seque auto const *cell = first->second; if( ( false == cell->m_traction.empty() - || ( false == cell->m_lines.empty() ) ) ) { + || ( false == cell->m_lines.empty() ) ) + && false == distant( cell ) ) { // since all shapes of the cell share center point we can optimize out a few calls here ::glPushMatrix(); auto const originoffset { cell->m_area.center - m_renderpass.camera.position() }; diff --git a/tests/unit/test_alignment.cpp b/tests/unit/test_alignment.cpp index 54e1cfe0..3b091f43 100644 --- a/tests/unit/test_alignment.cpp +++ b/tests/unit/test_alignment.cpp @@ -949,3 +949,198 @@ TEST_CASE("alignment: curves turning the same way with straights between them, t CHECK(fit.rms < 0.02 + each.noise); } } + +// the points along a line from the origin along x through the segments, every 10 m, the last one at its end +std::vector points_to_end(std::vector> const &Segments, line_end &End) { + auto points{walked_points(Segments, 10.0, End)}; + points.resize(points.size() - 2); + End.position -= heading_of(End.heading) * 60.0; + if (false == points.empty() && glm::distance(points.back(), End.position) < 2.0) + points.pop_back(); + points.push_back(End.position); + return points; +} + +// the stretch from the end of the diverging track of a switch of R 300 turning to the left, at the origin along x +alignment::design from_switch(line_end const &End, double const Speed) { + alignment::design base; + base.speed = Speed; + base.start = {0.0, 0.0, 0.0}; + base.start_direction = heading(0.0); + base.start_radius = 300.0; + base.start_turn = 1; + base.start_curved = true; + base.end = {End.position.x, 0.0, End.position.y}; + base.end_direction = heading(End.heading); + return base; +} + +TEST_CASE("alignment: a curve traced on from the diverging track of a switch is a compound curve with it, ending at the last point") { + double const k{1.0 / 300.0}; + double const m{1.0 / 150.0}; + line_end end; + auto const points{points_to_end({{k, m, 20.0}, {m, m, 30.0}, {m, 0.0, 25.0}}, end)}; + auto const base{from_switch(end, 60.0)}; + auto const fit{alignment::fit_going_on(base, points)}; + REQUIRE(fit.valid); + REQUIRE(fit.vertices.size() == 1); + CHECK(fit.vertices.front().radius == doctest::Approx(150.0).epsilon(0.03)); + CHECK(fit.vertices.front().transition_in == doctest::Approx(20.0).epsilon(0.15)); + CHECK(fit.vertices.front().transition_out == doctest::Approx(25.0).epsilon(0.15)); + CHECK(fit.rms < 0.02); + // laid from the curvature of the switch, no jump at the joint; ending right at the last point, heading as the line does there + auto laid{base}; + laid.vertices = fit.vertices; + laid.end_direction = fit.leaving; + auto const line{alignment::compute(laid)}; + REQUIRE(line.valid); + CHECK(curvature_of(line, 0.25) == doctest::Approx(k).epsilon(0.05)); + auto const last{alignment::evaluate(line, line.length)}; + CHECK(glm::distance(glm::dvec2{last.position.x, last.position.z}, end.position) < 0.01); + CHECK(std::atan2(fit.leaving.y, fit.leaving.x) == doctest::Approx(end.heading).epsilon(0.01)); + CHECK(curvature_of(line, line.length - 0.25) == doctest::Approx(0.0).epsilon(1e-3)); + + // two points only, the radius between them unknown: the transitions as long as the norms ask for, the curvature going on from the + // switch all the same + std::vector const two{points[points.size() / 2], points.back()}; + auto const few{alignment::fit_going_on(base, two)}; + REQUIRE(few.valid); + auto shortlaid{base}; + shortlaid.vertices = few.vertices; + shortlaid.end_direction = few.leaving; + auto const shortline{alignment::compute(shortlaid)}; + REQUIRE(shortline.valid); + CHECK(curvature_of(shortline, 0.25) == doctest::Approx(k).epsilon(0.05)); + CHECK(few.worst < 0.05); + CHECK(few.vertices.front().transition_in > 5.0); +} + +TEST_CASE("alignment: a curve traced on from a switch turning hard at its end takes two arcs, a flatter one first") { + double const k{1.0 / 300.0}; + double const m{1.0 / 400.0}; + double const n{1.0 / 40.0}; + line_end end; + auto const points{points_to_end({{k, m, 15.0}, {m, m, 40.0}, {m, n, 20.0}, {n, n, 30.0}, {n, 0.0, 20.0}}, end)}; + auto const base{from_switch(end, 40.0)}; + auto const fit{alignment::fit_going_on(base, points)}; + REQUIRE(fit.valid); + REQUIRE(fit.vertices.size() == 1); + REQUIRE(fit.vertices.front().compound); + REQUIRE(fit.vertices.front().arcs.size() == 1); + CHECK(fit.vertices.front().radius > 250.0); + CHECK(fit.vertices.front().arcs.front().radius == doctest::Approx(40.0).epsilon(0.1)); + CHECK(fit.rms < 0.1); + auto laid{base}; + laid.vertices = fit.vertices; + laid.end_direction = fit.leaving; + auto const line{alignment::compute(laid)}; + REQUIRE(line.valid); + CHECK(curvature_of(line, 0.25) == doctest::Approx(k).epsilon(0.05)); + auto const last{alignment::evaluate(line, line.length)}; + CHECK(glm::distance(glm::dvec2{last.position.x, last.position.z}, end.position) < 0.01); +} + +TEST_CASE("alignment: a curve traced on from a switch with points clicked on the straight after it ends where the straight begins") { + double const k{1.0 / 300.0}; + double const m{1.0 / 200.0}; + line_end end; + auto const along{walked_points({{k, m, 20.0}, {m, m, 40.0}, {m, 0.0, 30.0}}, 10.0, end)}; + auto const base{from_switch(end, 60.0)}; + auto const fit{alignment::fit_going_on(base, along)}; + REQUIRE(fit.valid); + CHECK(fit.vertices.front().radius == doctest::Approx(200.0).epsilon(0.03)); + CHECK(fit.rms < 0.02); + CHECK(std::atan2(fit.leaving.y, fit.leaving.x) == doctest::Approx(end.heading).epsilon(0.01)); +} + +TEST_CASE("alignment: no curve goes on from a switch to a point on the other side of its heading") { + line_end end; + end.position = {60.0, -3.0}; + end.heading = -0.1; + auto const base{from_switch(end, 60.0)}; + CHECK_FALSE(alignment::fit_going_on(base, {{30.0, -1.0}, {60.0, -3.0}}).valid); +} + +// held: the curve going on from the diverging track of a switch keeps its radius, with no transition between them +alignment::design held_from_switch(line_end const &End, double const Speed) { + auto base{from_switch(End, Speed)}; + base.start_held = true; + return base; +} + +TEST_CASE("alignment: a curve going on from a switch at its radius begins right there by the transition out of it") { + double const k{1.0 / 300.0}; + // R 300 on for 40 m, then a transition of 30 m out to the straight: the radius whatever the vertex was given, none in, the one out found + auto design{curved_from(300.0, {{k, k, 40.0}, {k, 0.0, 30.0}}, 40.0)}; + design.start_held = true; + auto &vertex{design.vertices.front()}; + vertex.radius = 1000.0; + vertex.transition_in = 20.0; + auto const line{alignment::compute(design)}; + REQUIRE(line.valid); + REQUIRE(line.curves.size() == 1); + auto const &curve{line.curves.front()}; + CHECK(curve.radius == doctest::Approx(300.0).epsilon(1e-9)); + CHECK(curve.transition_in == doctest::Approx(0.0)); + CHECK(curve.transition_out == doctest::Approx(30.0).epsilon(0.05)); + CHECK(curvature_of(line, 0.25) == doctest::Approx(k).epsilon(0.01)); + auto const start{alignment::evaluate(line, 0.0)}; + CHECK(std::abs(start.position.x) < 1e-6); + CHECK(std::abs(start.position.z) < 1e-6); + auto const end{alignment::evaluate(line, line.length)}; + CHECK(glm::distance(glm::dvec2{end.position.x, end.position.z}, glm::dvec2{design.end.x, design.end.z}) < 1e-3); + + // a vertex nearer than the arc of the switch reaches with no transition: no such curve + auto nearer{curved_from(300.0, {{k, k, 40.0}, {k, 0.0, 30.0}}, 40.0)}; + nearer.start_held = true; + nearer.end = {nearer.end.x, 0.0, nearer.end.z + 5.0}; + CHECK_FALSE(alignment::compute(nearer).valid); +} + +TEST_CASE("alignment: a curve traced on from a switch goes on at its radius, not through a transition into another") { + double const k{1.0 / 300.0}; + line_end end; + auto const points{points_to_end({{k, k, 60.0}, {k, 0.0, 25.0}}, end)}; + auto const base{held_from_switch(end, 100.0)}; + auto const fit{alignment::fit_going_on(base, points)}; + REQUIRE(fit.valid); + REQUIRE(fit.vertices.size() == 1); + CHECK_FALSE(fit.vertices.front().compound); + CHECK(fit.vertices.front().radius == doctest::Approx(300.0).epsilon(1e-9)); + CHECK(fit.vertices.front().transition_in == doctest::Approx(0.0)); + CHECK(fit.rms < 0.05); + auto laid{base}; + laid.vertices = fit.vertices; + laid.end_direction = fit.leaving; + auto const line{alignment::compute(laid)}; + REQUIRE(line.valid); + CHECK(line.curves.front().radius == doctest::Approx(300.0).epsilon(1e-6)); + CHECK(curvature_of(line, 0.25) == doctest::Approx(k).epsilon(0.01)); + auto const last{alignment::evaluate(line, line.length)}; + CHECK(glm::distance(glm::dvec2{last.position.x, last.position.z}, end.position) < 0.01); + + // the points along its arc alone, as they're clicked: the arc of the switch as far as abreast of the last of them, no transition out + line_end on; + auto const arc{points_to_end({{k, k, 50.0}}, on)}; + auto const alone{alignment::fit_going_on(held_from_switch(on, 100.0), arc)}; + REQUIRE(alone.valid); + REQUIRE(alone.vertices.size() == 1); + CHECK(alone.vertices.front().radius == doctest::Approx(300.0).epsilon(1e-9)); + CHECK(alone.vertices.front().transition_out == doctest::Approx(0.0)); + CHECK(alone.rms < 0.01); + CHECK(glm::distance(alone.ending, on.position) < 0.01); + + // the points along a flatter curve after it: the arc of the switch first, then the flatter one + double const m{1.0 / 600.0}; + line_end flatter; + auto const along{points_to_end({{k, m, 30.0}, {m, m, 60.0}, {m, 0.0, 30.0}}, flatter)}; + auto const two{alignment::fit_going_on(held_from_switch(flatter, 60.0), along)}; + REQUIRE(two.valid); + REQUIRE(two.vertices.size() == 1); + REQUIRE(two.vertices.front().compound); + REQUIRE(two.vertices.front().arcs.size() == 1); + CHECK(two.vertices.front().radius == doctest::Approx(300.0).epsilon(1e-9)); + CHECK(two.vertices.front().transition_in == doctest::Approx(0.0)); + CHECK(two.vertices.front().arcs.front().radius == doctest::Approx(600.0).epsilon(0.1)); + CHECK(two.rms < 0.1); +} diff --git a/tests/unit/test_switch_curve.cpp b/tests/unit/test_switch_curve.cpp index 0a05a89d..05324443 100644 --- a/tests/unit/test_switch_curve.cpp +++ b/tests/unit/test_switch_curve.cpp @@ -100,6 +100,52 @@ TEST_CASE("switch on a curve: the ends of a cant ramp at the ends of the switch CHECK(what.empty()); } +TEST_CASE("switch on a curve: one with a straight frog bent up to the frog, both tracks straight over it") { + auto const track{line(0.0, 1.0, 400.0, 1500.0, 0.0)}; + for (auto const side : {1, -1}) { + CAPTURE(side); + auto [main, branch] {laid(track, 150.0, 27.0, 190.0, side)}; + // the last 6 m of both straight, the diverging one with no curve of its own there either: the frog of the switch + auto const frog{21.0}; + auto const straighten = [&](std::vector &Track) { + auto const at{std::find_if(Track.begin(), Track.end(), [&](auto const &Point) { return Point.station >= frog; })}; + for (auto point{at}; point != Track.end(); ++point) + point->heading = at->heading; + }; + straighten(main); + straighten(branch); + auto const report{switch_curve::check(main, branch, track, 150.0, 177.0, 190.0, 9.0, 27.0 - frog)}; + CHECK(report.curved); + // the curvature of the bent part only: no transition curve under it, the diverging track turning one way all along + CHECK_FALSE(report.transition); + CHECK(false == says(report.warnings, "KZW II")); + CHECK(says(report.warnings, "Bent up to the frog")); + } +} + +TEST_CASE("switch on a curve: none of 1:9.403 nor steeper than 1:9 bent in the running and the main tracks, 1:9 itself is") { + auto const track{line(0.0, 1.0, 400.0, 1500.0, 0.0)}; + auto const [main, branch] {laid(track, 150.0, 33.0, 300.0, 1)}; + auto const banned = [&](double const Ratio) { + CAPTURE(Ratio); + auto const report{switch_curve::check(main, branch, track, 150.0, 183.0, 300.0, Ratio)}; + REQUIRE(report.curved); + return says(report.warnings, "(11.4 pkt 4)"); + }; + // 1:9 of the catalogue and as read off the angle of the tracks + CHECK_FALSE(banned(9.0)); + CHECK_FALSE(banned(8.998)); + CHECK_FALSE(banned(9.002)); + CHECK(banned(9.403)); + CHECK(banned(9.41)); + CHECK_FALSE(banned(9.51)); + CHECK_FALSE(banned(10.0)); + CHECK_FALSE(banned(12.0)); + CHECK(banned(7.5)); + CHECK(banned(6.6)); + CHECK(banned(4.8)); +} + namespace { // a track of one curvature from its start, heading as given there, a point every half metre diff --git a/utilities/Globals.h b/utilities/Globals.h index 50c5118a..36be95b6 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -175,7 +175,6 @@ struct global_settings { bool EditorOrtho{ false }; float EditorOrthoExtent{ 150.f }; int EditorOrthoTracks{ 0 }; // the railway in the top view of the editor: 0 whole, 1 without the ballast, 2 the rails alone - bool EditorOrthoMap{ false }; // the top view of the editor far out shows the base map in place of the ground, which isn't drawn then GLint iMaxTextureSize{ 4096 }; // maksymalny rozmiar tekstury GLint iMaxCabTextureSize{ 4096 }; // largest allowed texture in vehicle cab int iMultisampling{ 2 }; // tryb antyaliasingu: 0=brak,1=2px,2=4px,3=8px,4=16px diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 0c18e002..b79a92ad 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -73,6 +73,24 @@ std::filesystem::path user_config_path(const std::string &filename) return {}; // env var missing → caller falls back to CWD-relative filename } +std::filesystem::path user_cache_path(const std::string &filename) +{ + namespace fs = std::filesystem; +#if defined(_WIN32) + if (const char *local = std::getenv("LOCALAPPDATA")) + return fs::path(local) / "MaSzyna" / "cache" / filename; +#elif defined(__APPLE__) + if (const char *home = std::getenv("HOME")) + return fs::path(home) / "Library" / "Caches" / "MaSzyna" / filename; +#else + if (const char *cache = std::getenv("XDG_CACHE_HOME"); cache != nullptr && *cache != '\0') + return fs::path(cache) / "MaSzyna" / filename; + if (const char *home = std::getenv("HOME")) + return fs::path(home) / ".cache" / "MaSzyna" / filename; +#endif + return {}; +} + // zwraca różnicę czasu // jeśli pierwsza jest aktualna, a druga rozkładowa, to ujemna oznacza opóżnienie // na dłuższą metę trzeba uwzględnić datę, jakby opóżnienia miały przekraczać 12h (towarowych) diff --git a/utilities/utilities.h b/utilities/utilities.h index d98b7bc9..265ad87a 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -28,6 +28,9 @@ template constexpr T sq(T v) // returns the per-user config path for `filename` (platform-specific dir), // or an empty path if the home/appdata env var is unavailable std::filesystem::path user_config_path(const std::string &filename); +// the per-user path for `filename` among the files kept only to be fetched again faster, which don't roam with the profile: +// %LOCALAPPDATA%\MaSzyna\cache, ~/Library/Caches/MaSzyna, $XDG_CACHE_HOME/MaSzyna or ~/.cache/MaSzyna; empty as user_config_path +std::filesystem::path user_cache_path(const std::string &filename); // TODO: Shouldn't this be in globals? namespace paths diff --git a/world/Segment.cpp b/world/Segment.cpp index 720e259b..798274dc 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -923,6 +923,11 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig auto const onwall { paired ? ( false == open( partner ) && clamped( partner ) == side ) : moved == count }; if( onwall ) { wall = ( side > 0 ? outerwall : innerwall ); + // a face of the profile squeezed onto the wall from the other side of the rail keeps facing that side: the thin plate a + // planed blade gets to near its tip is lit from the middle of the track as its face there, not as the one against the stock rail + if( static_cast( Span.side * side ) * ShapePoints[ j ].normal.x < 0.f ) { + wall = -wall; + } } } if( drop > 0.0 && shape.position.y > Span.head.x && Span.head.y > Span.head.x ) {