diff --git a/CMakeLists.txt b/CMakeLists.txt index 6128ad61..a99a2b3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,6 +212,9 @@ set(SOURCES "editor/editorGroundFollow.cpp" "editor/editorAlignmentStore.cpp" "editor/editorSwitchStore.cpp" +"editor/editorSwitchCurve.cpp" +"editor/editorKilometrage.cpp" +"editor/editorStation.cpp" "editor/editorSwitchDesign.cpp" "editor/editorConnections.cpp" "application/editormodeconnection.cpp" @@ -225,6 +228,7 @@ set(SOURCES "editor/editorOrthophoto.cpp" "editor/editorHttp.cpp" "editor/editorNmt.cpp" +"editor/editorGeoMap.cpp" "editor/editorPreviews.cpp" "editor/editorIcons.cpp" "editor/editorClips.cpp" @@ -250,6 +254,8 @@ set(SOURCES "application/editormodegauge.cpp" "application/editormodeinfra.cpp" "application/editormodespeed.cpp" +"application/editormodesrk.cpp" +"application/editormodekm.cpp" "application/editormodejoints.cpp" "application/editormodeparallel.cpp" "application/editormodelineside.cpp" diff --git a/application/application.cpp b/application/application.cpp index ad507c00..b23fe8c3 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -990,7 +990,8 @@ GLFWwindow *eu07_application::window(int const Windowindex, bool visible, int wi if (keep_ownership) m_windows.emplace_back(childwindow); - glfwFocusWindow(m_windows.front()); // restore focus to main window + if (glfwGetWindowAttrib(m_windows.front(), GLFW_VISIBLE)) + glfwFocusWindow(m_windows.front()); // restore focus to main window return childwindow; } @@ -1324,7 +1325,9 @@ int eu07_application::create_main_window() Global.bFullScreen = true; } - auto *mainwindow = window(-1, true, Global.window_size.x, Global.window_size.y, Global.bFullScreen ? monitor : nullptr, true, false); + // a selftest of the editor run while the computer is in use for something else draws into a window never shown nor focused + bool const shown{std::getenv("EU07_EDITOR_SELFTEST_HIDDEN") == nullptr}; + auto *mainwindow = window(-1, shown, Global.window_size.x, Global.window_size.y, Global.bFullScreen && shown ? monitor : nullptr, true, false); if (mainwindow == nullptr) { diff --git a/application/editormode.cpp b/application/editormode.cpp index ff4daced..0ce777d1 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -82,6 +82,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; @@ -203,6 +235,12 @@ namespace triangle_grid(std::vector Triangles, glm::dvec2 const &Min, glm::dvec2 const &Max) : m_triangles(std::move(Triangles)), m_min(Min) { glm::dvec2 const extent = glm::max(Max - Min, glm::dvec2(1.0)); + if (false == std::isfinite(extent.x) || false == std::isfinite(extent.y)) + { + m_triangles.clear(); + m_cells.resize(1); + return; + } // ~8 m cells, coarser for big areas so large terrain triangles don't flood the grid m_cellsize = std::max(8.0, std::max(extent.x, extent.y) / 128.0); m_columns = static_cast(std::ceil(extent.x / m_cellsize)) + 1; @@ -244,11 +282,13 @@ namespace private: int column(double const X) const { - return static_cast(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast(m_columns - 1))); + double const cell = std::floor((X - m_min.x) / m_cellsize); + return cell > 0.0 ? static_cast(std::min(cell, static_cast(m_columns - 1))) : 0; } int row(double const Z) const { - return static_cast(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast(m_rows - 1))); + double const cell = std::floor((Z - m_min.y) / m_cellsize); + return cell > 0.0 ? static_cast(std::min(cell, static_cast(m_rows - 1))) : 0; } std::vector m_triangles; @@ -505,6 +545,7 @@ void editor_mode::editormode_input::poll() editor_mode::editor_mode() { alignment_store::reset(); switch_store::reset(); + kilometrage::reset(); m_userinterface = std::make_shared(); // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); @@ -522,7 +563,7 @@ editor_mode::editor_mode() { std::string reason; if (Node->from_template() || false == scene::Layers.editable(Node, &reason)) { - ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), (Node->name().empty() ? std::string{STR_C("(no name)")} : Node->name()).c_str(), reason.c_str()), true); + ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), name_of(*Node).c_str(), reason.c_str()), true); return; } m_track_window_open = false; @@ -546,15 +587,30 @@ 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 - // 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) { - 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); }); + 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 + 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); + }); + 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 + 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 + if (m_nmt.placed()) + m_nmt.request({X - 1.0, Z - 1.0}, {X + 1.0, Z + 1.0}); + return false; + }); +} + editor_mode::~editor_mode() { editor_track::observe(nullptr); @@ -641,7 +697,14 @@ editor_ui *editor_mode::ui() const bool editor_mode::init() { EditorSettings.load(); - Global.EditorOrthoBallast = EditorSettings.top_ballast(); + 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); @@ -868,6 +931,13 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) if (!node) return; + if (ui()->node() == node) + ui()->set_node(nullptr); + infra_forget(node); + for (auto *tool : m_tools) + tool->forget(node); + + auto const followed = [node](infra::object_state const &State) { return State.saved.node == node; }; for (auto &s : m_history) { if (s.node_ptr == node) @@ -877,12 +947,14 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) if (copy.model == node) copy.model = nullptr; } + s.infra.erase(std::remove_if(s.infra.begin(), s.infra.end(), followed), s.infra.end()); } for (auto &s : g_redo) { if (s.node_ptr == node) s.node_ptr = nullptr; + s.infra.erase(std::remove_if(s.infra.begin(), s.infra.end(), followed), s.infra.end()); } // deleted nodes also drop out of the "undo last fill" set @@ -900,6 +972,9 @@ void editor_mode::terrain_history_step() { if (false == m_streamer.active()) return; + // the terrain loaded from the model is taken back as a whole + if (nmt_busy() && false == m_history_busy) + return; if (m_streamer.recording()) { if (terrain_brush() && mouseHold && false == m_history_busy) @@ -938,6 +1013,8 @@ void editor_mode::restore_terrain(EditorSnapshot &Snapshot) void editor_mode::undo_last() { first_step_done(first_step::undo); + // the warning by the cursor told of the step taken back now: "Ctrl+Z takes it back" is done + ui()->drop_refusal(); // a change of the terrain made a moment ago is a step of its own before it's taken back m_history_busy = true; terrain_history_step(); @@ -962,11 +1039,33 @@ void editor_mode::undo_last() } } +std::string editor_mode::refused(std::string Text) +{ + ui()->set_status(Text, true); + return Text; +} + +void editor_mode::drop_step(std::size_t const Step) +{ + if (Step == 0 || m_history.size() != Step) + return; + auto const redo{g_redo.size()}; + undo_one(); + if (g_redo.size() > redo) + g_redo.erase(g_redo.begin() + redo, g_redo.end()); +} + void editor_mode::undo_one() { if (m_history.empty()) return; + m_nmt_state.restoring = true; + struct restored + { + bool &flag; + ~restored() { flag = false; } + } const restoring{m_nmt_state.restoring}; EditorSnapshot snap = m_history.back(); m_history.pop_back(); @@ -977,6 +1076,13 @@ void editor_mode::undo_one() return; } + if (snap.action == EditorSnapshot::Action::Kilometrage) + { + restore_kilometrage(snap); + g_redo.push_back(std::move(snap)); + return; + } + if (snap.action == EditorSnapshot::Action::Bend) { restore_bend(snap, true); @@ -1082,9 +1188,7 @@ void editor_mode::undo_one() if (auto *model = dynamic_cast(target)) { - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); m_node = nullptr; ui()->set_node(nullptr); } @@ -1132,6 +1236,12 @@ void editor_mode::redo_one() if (g_redo.empty()) return; + m_nmt_state.restoring = true; + struct restored + { + bool &flag; + ~restored() { flag = false; } + } const restoring{m_nmt_state.restoring}; EditorSnapshot snap = g_redo.back(); g_redo.pop_back(); @@ -1142,6 +1252,13 @@ void editor_mode::redo_one() return; } + if (snap.action == EditorSnapshot::Action::Kilometrage) + { + restore_kilometrage(snap); + m_history.push_back(std::move(snap)); + return; + } + if (snap.action == EditorSnapshot::Action::Bend) { restore_bend(snap, false); @@ -1210,9 +1327,7 @@ void editor_mode::redo_one() { if (auto *model = dynamic_cast(target)) { - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); m_node = nullptr; ui()->set_node(nullptr); } @@ -1277,17 +1392,42 @@ void editor_mode::redo_one() bool editor_mode::update() { + frame_steps steps; // the catenary doesn't need the far tracks drawn (50 km of them cost a lot in a big scenery) Global.editor_tracks = (current_work_area() == work_area::tracks); selftest_step(); - if (m_vehicle.leave) + 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) + { + EditorIncludes.start(); + if (EditorIncludes.scanned()) + { + m_drives_checked = true; + if (auto const moved{drives_to_blade_tips()}; moved > 0) + { + auto const text{format(STR_P("The drive of %zu switch is moved to the tips of its blades", "The drives of %zu switches are moved to the tips of their blades", static_cast(moved)), moved)}; + ui()->set_status(text, false); + WriteLog("Editor: " + text, logtype::generic); + } + } + } + // after the tool has told its own status + if (false == m_orphan_note.empty()) + { + ui()->set_notice(m_orphan_note); + WriteLog("Editor: " + m_orphan_note); + m_orphan_note.clear(); + } + if (m_vehicle.take_leave()) { - m_vehicle.leave = false; Application.pop_mode(); return true; } + steps.step("start"); Timer::UpdateTimers(true); + move_thrown_switches(); simulation::State.update_clocks(); simulation::Environment.update(); @@ -1302,6 +1442,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 @@ -1328,6 +1471,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 @@ -1358,8 +1502,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)) @@ -1396,6 +1542,7 @@ bool editor_mode::update() audio::renderer.update(Global.iPause ? 0.0 : deltarealtime); GfxRenderer->Update(deltarealtime); + steps.step("renderer"); simulation::is_ready = true; @@ -1411,9 +1558,34 @@ bool editor_mode::update() draw_terrain_overlay(); // --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays --- + // 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); draw_orthophoto(); + draw_nmt_pending(); + area_update(); + draw_download_area(); + steps.step("imagery"); // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); @@ -1421,7 +1593,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) draw_track_lines(); if (selected_track()) draw_track_overlay(); @@ -1429,17 +1608,19 @@ 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(); if (m_track_tab == track_tab::turntable) draw_turntable_overlay(); - if (m_track_tab == track_tab::signals) + if (m_track_tab == track_tab::srk) { signal_refresh(); signal_move_update(); @@ -1450,29 +1631,46 @@ bool editor_mode::update() catenary_drag_update(); draw_catenary_overlay(); } - draw_parallel_preview(); + m_parallel.draw_overlay(); if (m_track_tab == track_tab::lineside) { - draw_hekto_overlay(); - draw_fouling_overlay(); - draw_vehicle_marker(); + m_hekto.draw_overlay(); + m_fouling.draw_overlay(); + 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(); - speed_step(); - joints_step(); + 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(); draw_infra_overlay(); - draw_speed_overlay(); - draw_joints_overlay(); + m_speed.draw_overlay(); + m_joints.draw_overlay(); + m_srk.draw_overlay(); + m_km.draw_overlay(); + steps.step("overlays"); } else { @@ -1481,8 +1679,8 @@ bool editor_mode::update() bend_drag_update(); draw_bend_overlay(); } - update_gauge(); - render_gauge_window(); + m_gauge.update(); + m_gauge.render_windows(); // roads: trajectories of the lanes, and the tools of the road window update_road_tool(); @@ -1490,12 +1688,14 @@ 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(); // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) draw_area_fill_outline(); + background->PopClipRect(); draw_ortho_compass(); render_new_scenery_popup(); @@ -1503,7 +1703,7 @@ bool editor_mode::update() render_shortcuts_window(); render_first_steps(); render_nmt_prompt(); - render_orthophoto_window(); + render_nmt_banner(); render_switch_designer(); render_environment_window(); if (m_terrain_open) @@ -1512,6 +1712,7 @@ bool editor_mode::update() m_chunklist.scanned = false; // the files are read again when the terrain is the field of work again draw_selection_overlay(); render_context_menu(); + steps.step("windows"); // --- ImGui: Editor Settings & History windows --- if(m_settings_open) @@ -1595,14 +1796,16 @@ void editor_mode::render_terrain_ui() { if (false == m_streamer.active()) { - ImGui::TextWrapped("%s", STR_C("The scenery has no terrain yet. Make an empty one, or load it from the terrain model below")); - if (ImGui::Button(STR_C("Make heightmap terrain for this scenery"))) - ensure_terrain(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Kept in the folder terrain// and written there with the scenery (Ctrl+S)")); + ImGui::TextUnformatted(STR_C("The scenery has no terrain yet")); // the terrain model makes the terrain itself if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"), ImGuiTreeNodeFlags_DefaultOpen)) render_nmt_ui(); + ImGui::Spacing(); + ImGui::TextDisabled("%s", STR_C("Or without the real ground:")); + if (ImGui::Button(STR_C("Make an empty, flat terrain"))) + ensure_terrain(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Kept in the folder terrain// and written there with the scenery (Ctrl+S)")); return; } // the tools are chosen in the toolbar, here are their settings @@ -1640,6 +1843,8 @@ void editor_mode::render_terrain_ui() } } + if (nmt_busy()) + ImGui::SetNextItemOpen(true); if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"))) render_nmt_ui(); @@ -1681,6 +1886,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" @@ -2224,6 +2433,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(); @@ -2250,10 +2465,24 @@ 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 + m_orthophoto_area_mark = scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$a"); + m_orthophoto.area(editor_orthophoto::read_area(m_orthophoto_area_mark)); + if (false == m_orthophoto.area().empty()) + WriteLog(format("Editor: the download area of the scenery, %zu points, %d tiles of the orthophoto", m_orthophoto.area().size(), m_orthophoto.stats().area_tiles)); } void editor_mode::save_orthophoto_settings() @@ -2273,6 +2502,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(); @@ -2281,11 +2546,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")); @@ -2350,7 +2610,7 @@ void editor_mode::render_orthophoto_ui() }; auto const stats = m_orthophoto.stats(); - if (stats.failed > 0) + if (stats.failed > 0 || stats.area_failed > 0) { ImGui::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed); ImGui::SameLine(); @@ -2358,6 +2618,10 @@ void editor_mode::render_orthophoto_ui() m_orthophoto.retry_failed(); } + ImGui::Separator(); + render_download_area_ui(stats); + ImGui::Separator(); + if (ImGui::TreeNode(STR_C("More settings"))) { if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) @@ -2377,6 +2641,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")); @@ -2470,7 +2736,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 @@ -2479,7 +2745,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); + 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 @@ -2533,6 +2803,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); @@ -2974,8 +3256,9 @@ bool editor_mode::save() } first_step_done(first_step::save); - // the heightmap terrain is kept in files of its own, the scenery only needs the directive which loads it + // the heightmap terrain is kept in files of its own, the scenery only needs the directive which loads it, in a layer of its own std::vector rootstatements; + std::map> statements; if (m_streamer.active()) { std::string error; @@ -2985,10 +3268,16 @@ bool editor_mode::save() return false; } if (false == scene::Layers.terrain_directive()) - rootstatements.emplace_back("heightmap_terrain " + m_streamer.name() + " endheightmap_terrain"); + { + auto const directive{"heightmap_terrain " + m_streamer.name() + " endheightmap_terrain"}; + if (auto const layer{output_layer("teren")}; layer != null_handle) + statements[layer].push_back(directive); + else + rootstatements.push_back(directive); + } } - auto const terrainadded{false == rootstatements.empty()}; + auto const terrainadded{false == rootstatements.empty() || false == statements.empty()}; if (m_profile.changed) profile_store(); switch_store::tidy(); @@ -2997,7 +3286,6 @@ bool editor_mode::save() // the catenary (cantilevers on the poles) lives in files of its own; a file made in the editor needs the directive, in the file // of the layer it was made on (the scenario file when that layer can't take it) std::vector catenarynew; - std::map> layerstatements; auto const same_file = [](std::string const &A, std::string const &B) { auto a{std::filesystem::path(A).lexically_normal().generic_string()}, b{std::filesystem::path(B).lexically_normal().generic_string()}; std::transform(a.begin(), a.end(), a.begin(), [](unsigned char const C) { return static_cast(std::tolower(C)); }); @@ -3043,11 +3331,12 @@ bool editor_mode::save() { auto const layer{static_cast(catenary_scene_file_layer(file))}; if (layer > 1 && scene::Layers.accepts(layer)) - layerstatements[scene::Layers.resolve(layer)].emplace_back("catenary " + file + " endcatenary"); + statements[scene::Layers.resolve(layer)].emplace_back("catenary " + file + " endcatenary"); else rootstatements.emplace_back("catenary " + file + " endcatenary"); catenarynew.push_back(file); } + m_srk.store(); if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) { m_saved_camera = std::make_pair(glm::dvec3{Camera.Pos}, glm::vec3{Camera.Angle}); @@ -3058,19 +3347,18 @@ bool editor_mode::save() m_orthophoto_mark = map; scene::Layers.mark(scene::layer_handle{1}, "//$o", {map}); } + if (auto const area{editor_orthophoto::area_lines(m_orthophoto.area())}; false == m_orthophoto_scenery.empty() && area != m_orthophoto_area_mark) + { + m_orthophoto_area_mark = area; + scene::Layers.mark(scene::layer_handle{1}, "//$a", area); + } auto const controls{turntable_controls(rootstatements)}; auto const noses{nose_controls(rootstatements)}; std::vector trailing; - std::erase_if(m_vehicle.trainsets, [](auto const &Trainset) { - auto const *track{simulation::Paths.find(Trainset.track)}; - return track == nullptr || track->m_editorremoved; - }); - auto const placed{m_vehicle.trainsets.size()}; - for (auto const &trainset : m_vehicle.trainsets) - trailing.insert(trailing.end(), trainset.lines.begin(), trainset.lines.end()); + auto const placed{m_vehicle.trainset_lines(trailing)}; if (placed == 0) starter_trainset(trailing); - auto const result = scene::Layers.save(rootstatements, trailing, layerstatements, &catenaryblocks); + auto const result = scene::Layers.save(rootstatements, trailing, statements, &catenaryblocks); if (result.success) for (auto const &file : catenaryinline) catenary_scene_file_saved(file); @@ -3079,10 +3367,7 @@ bool editor_mode::save() if (result.success && noses) m_nose_included = true; if (result.success && placed > 0) - { - m_vehicle.trainsets.clear(); - m_vehicle.status = format(STR_C("%d trainset(s) written to the scenery"), static_cast(placed)); - } + m_vehicle.trainsets_written(); if (result.success && placed == 0 && trailing.size() == 3) { std::istringstream header{trailing.front()}; @@ -3198,13 +3483,21 @@ void editor_mode::select_include(scene::instance_handle const Instance) void editor_mode::set_include_directive(scene::instance_handle const Instance, std::string const &Directive) { - if (false == scene::Layers.modify(Instance, Directive)) + std::string file; + std::vector values; + if (scene::Layers.tracked(Instance) && editor_includes::parse_directive(Directive, file, values) && scene::Layers.instance(Instance).file != nullptr && + editor_includes::normalized(file) == editor_includes::normalized(*scene::Layers.instance(Instance).file)) + file.clear(); + if (false == scene::Layers.modify(Instance, Directive, file)) return; std::vector retired; simulation::State.rebuild_include(Instance, retired); // the pick of the renderer is resolved a few frames after it's requested, and can still deliver one of these for (auto *model : retired) + { + nullify_history_pointers(model); m_retired.emplace_back(model, 120); + } } void editor_mode::apply_include() @@ -3541,6 +3834,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 @@ -3617,12 +3929,10 @@ void editor_mode::capture_terrain() std::string as_text; model->export_as_text(as_text); push_snapshot(model, EditorSnapshot::Action::Delete, as_text); - nullify_history_pointers(model); - remove_from_hierarchy(model); m_node = nullptr; m_dragging = false; ui()->set_node(nullptr); - simulation::State.delete_model(model); + erase_model(model); } // the ground under a point the cursor landed on. the orthophoto laid over the ground catches the cursor like any @@ -3738,13 +4048,21 @@ void editor_mode::read_georeference() std::error_code error; if (false == std::filesystem::exists(file, error)) { - for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + for (std::filesystem::directory_iterator entry{Global.asCurrentSceneryPath, error}, end; entry != end; entry.increment(error)) { - auto name{entry.path().filename().string()}; + std::string name; + try + { + name = entry->path().filename().string(); + } + catch (std::exception const &) + { + continue; + } std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); if (name == Global.SceneryFile) { - file = entry.path(); + file = entry->path(); break; } } @@ -3779,21 +4097,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() @@ -3804,18 +4387,29 @@ void editor_mode::render_view_menu() toggle_ortho(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The view straight down without perspective, as on a map; again for the camera as it was")); - if (ImGui::MenuItem(STR_C("Ballast in the top view"), nullptr, &Global.EditorOrthoBallast)) + if (ImGui::BeginMenu(STR_C("Tracks in the top view"))) { - EditorSettings.top_ballast(Global.EditorOrthoBallast); - EditorSettings.save(); + render_top_tracks_choice(false); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu(STR_C("Track colours"))) + { + char const *const names[]{"Plain", "By the radius", "By the speed", "By the grade"}; + for (int i = 0; i < 4; ++i) + if (ImGui::MenuItem(STR_C(names[i]), nullptr, static_cast(m_track_colouring) == i)) + m_track_colouring = static_cast(i); + ImGui::EndMenu(); } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Off: the top view draws the rails and sleepers of the tracks without the ballast, so the ground under them shows")); + ImGui::SetTooltip("%s", STR_C("The tracks of the track mode coloured by their radius, speed or grade, with the legend in the corner")); + ImGui::MenuItem(STR_C("Track labels in the top view"), nullptr, &m_track_labels); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Zoomed in from above, the curves get their radius and length and the switches their name and type, as on a plan; where they'd crowd, some are left out")); ImGui::MenuItem(STR_C("Show lanes"), nullptr, &m_roadtool.lanes); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The lanes of the roads drawn as the paths the vehicles take")); - if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.open)) - m_gauge.open = !m_gauge.open; + if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.is_open())) + m_gauge.toggle(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)")); ImGui::Separator(); @@ -3985,10 +4579,19 @@ void editor_mode::render_open_scenery_popup() m_openscenery_list.clear(); m_openscenery_choice.clear(); std::error_code error; - for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + for (std::filesystem::directory_iterator entry{Global.asCurrentSceneryPath, error}, end; entry != end; entry.increment(error)) { - auto const name{entry.path().filename().string()}; - if (entry.is_regular_file(error) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$') + std::string name; + try + { + name = entry->path().filename().string(); + } + catch (std::exception const &) + { + continue; + } + std::error_code status; + if (entry->is_regular_file(status) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$') m_openscenery_list.emplace_back(name); } std::sort(m_openscenery_list.begin(), m_openscenery_list.end(), [&](std::string const &A, std::string const &B) { return lower(A) < lower(B); }); @@ -4051,7 +4654,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(); } @@ -4096,9 +4699,7 @@ void editor_mode::undo_last_area_fill() m_node = nullptr; ui()->set_node(nullptr); } - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); } m_fill_status = format(STR_C("Removed %zu objects of the last fill"), created.size()); } @@ -4519,9 +5120,7 @@ void editor_mode::shape_array() copies.pop_back(); if (model == nullptr) continue; - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); } std::string definition; @@ -4606,9 +5205,7 @@ void editor_mode::restore_array_snapshot(EditorSnapshot Snapshot, std::vectorset_node(nullptr); } - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); } else { @@ -4901,7 +5498,7 @@ void editor_mode::render_help_menu() ImGui::SetTooltip("%s", STR_C("The keys of the editor, for each field of work")); ImGui::MenuItem(STR_C("Tutorial"), nullptr, &m_first_steps_open); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Twelve short lessons with clips and drawings: the camera, laying track, curves, correcting a curve, joining, switches, signals and posts, fixing, the grade line, terrain, older sceneries, saving and driving")); + ImGui::SetTooltip("%s", STR_C("Thirteen short lessons with clips and drawings: the camera, laying track, curves, correcting a curve, joining, switches, signals and posts, fixing, the gizmo, the grade line, terrain, older sceneries, saving and driving")); ImGui::EndMenu(); } @@ -5096,7 +5693,7 @@ void tutorial_signal_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const Hig List.AddText(ImVec2(foot.x - height * 0.06f - ImGui::CalcTextSize("S1").x, foot.y - line), IM_COL32(255, 255, 255, 230), "S1"); } gesture_mouse(List, ImVec2(cursor.x + 2.0f, cursor.y + 2.0f), std::round(height * 0.16f), "LMB"); - List.AddText(ImVec2(Low.x + 6.0f, Low.y + 4.0f), IM_COL32(230, 230, 230, 230), STR_C("H: by the track, drag along: the way the trains run")); + List.AddText(ImVec2(Low.x + 6.0f, Low.y + 4.0f), IM_COL32(230, 230, 230, 230), STR_C("LMB by the track, drag along: the way the trains run")); return; } // the hectometre posts of the line, one after the other @@ -5188,85 +5785,6 @@ void tutorial_trainset_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const H List.AddText(ImVec2(cursor.x + height * 0.16f, cursor.y), IM_COL32(255, 220, 120, 255), "x2"); } -// the plan of a curve over the band of its curvature: the plateau of the arc goes up and down under the mouse, the curve tightens -// and opens with it over the plan kept faint as it was -void tutorial_curve_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) -{ - auto const width{High.x - Low.x}; - auto const height{High.y - Low.y}; - auto const lift{static_cast(0.5 + 0.5 * std::sin(Time * 1.3))}; - auto const curvature = [](float const Share, float const Peak) { - if (Share < 0.15f || Share > 0.85f) - return 0.0f; - if (Share < 0.35f) - return Peak * (Share - 0.15f) / 0.2f; - if (Share > 0.65f) - return Peak * (0.85f - Share) / 0.2f; - return Peak; - }; - auto const colour = [](float const Share) { - if (Share < 0.15f || Share > 0.85f) - return IM_COL32(255, 255, 255, 230); - if (Share < 0.35f || Share > 0.65f) - return IM_COL32(255, 140, 30, 255); - return IM_COL32(60, 230, 90, 255); - }; - int constexpr steps{90}; - auto const plan_height{height * 0.5f}; - float scale{1.0f}; - ImVec2 start{0.0f, 0.0f}; - auto const plan = [&](float const Peak) { - std::vector points; - float heading{0.0f}, x{0.0f}, y{0.0f}; - for (int i = 0; i <= steps; ++i) - { - points.emplace_back(start.x + x * scale, start.y + y * scale); - auto const share{(i + 0.5f) / steps}; - heading += curvature(share, Peak) / steps; - x += std::cos(heading) / steps; - y -= std::sin(heading) / steps; - } - return points; - }; - // the scale fits the plan of the sharpest curve, the least sharp one is the widest - auto const sharpest{plan(2.8f)}; - auto const widest{plan(1.6f)}; - scale = std::min(width * 0.85f / std::max(1e-3f, widest.back().x), plan_height / std::max(1e-3f, -sharpest.back().y)); - start = ImVec2(Low.x + (width - widest.back().x * scale) * 0.5f, Low.y + height * 0.06f + plan_height); - auto const before{plan(1.6f)}; - List.AddPolyline(before.data(), static_cast(before.size()), IM_COL32(170, 170, 170, 90), 0, 1.5f); - auto const peak{1.6f + 1.2f * lift}; - auto const now{plan(peak)}; - for (int i = 0; i < steps; ++i) - List.AddLine(now[i], now[i + 1], colour((i + 0.5f) / steps), 3.0f); - // the band under the plan - auto const band_top{Low.y + height * 0.66f}; - auto const band_bottom{High.y - height * 0.06f}; - auto const left{Low.x + width * 0.06f}; - auto const band_width{width * 0.88f}; - List.AddRectFilled(ImVec2(left - 6.0f, band_top - 6.0f), ImVec2(left + band_width + 6.0f, band_bottom + 4.0f), ImGui::GetColorU32(ImGuiCol_FrameBg), 4.0f); - List.AddLine(ImVec2(left, band_bottom), ImVec2(left + band_width, band_bottom), ImGui::GetColorU32(ImGuiCol_TextDisabled)); - auto const k_scale{(band_bottom - band_top) * 0.9f / 2.8f}; - for (int i = 0; i < steps; ++i) - { - auto const a{static_cast(i) / steps}; - auto const b{static_cast(i + 1) / steps}; - ImVec2 const p{left + a * band_width, band_bottom - curvature(a, peak) * k_scale}; - ImVec2 const q{left + b * band_width, band_bottom - curvature(b, peak) * k_scale}; - auto const tint{colour((a + b) * 0.5f)}; - auto const flags{List.Flags}; - List.Flags &= ~ImDrawListFlags_AntiAliasedFill; - List.AddQuadFilled(ImVec2(p.x, band_bottom), p, q, ImVec2(q.x, band_bottom), (tint & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 50)); - List.Flags = flags; - List.AddLine(p, q, tint, 2.0f); - } - char text[32]; - std::snprintf(text, sizeof(text), "R %.0f", std::round(900.0f / peak / 5.0f) * 5.0f); - ImVec2 const plateau{left + band_width * 0.5f, band_bottom - peak * k_scale}; - List.AddText(ImVec2(plateau.x - ImGui::CalcTextSize(text).x * 0.5f, plateau.y - ImGui::GetTextLineHeight() - 2.0f), IM_COL32(60, 230, 90, 255), text); - gesture_mouse(List, ImVec2(plateau.x + band_width * 0.08f, plateau.y - height * 0.06f), height * 0.16f, "LMB"); -} - // the terrain model under the tracks: the track laid level sinks onto the ground along, the formation and the slopes under it void tutorial_terrain_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) { @@ -5328,6 +5846,10 @@ void editor_mode::render_first_steps() first_step_done(first_step::terrain); if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && false == m_route.chain.tracks.empty()) first_step_done(first_step::plan); + if (ui()->mode() == nodebank_panel::TRACK && m_track_gizmo_on) + first_step_done(first_step::gizmo); + if (m_track_gizmo_using || (m_point_drag.moved && m_track_point.valid() && false == editor_track::is_end(m_track_point.kind))) + first_step_done(first_step::vector); enum class trial { none, top, lay, turnout, select, profile, terrain, plan, trainset, signals }; struct input { @@ -5363,53 +5885,52 @@ void editor_mode::render_first_steps() "lay", {{"F3", "The Tracks field of work"}, {"L", "The lay tool"}, - {"LMB", "A click on the ground: the first point; the next click: a straight up to it"}, - {"LMB", "Each next click lays a piece on from the end of the last one"}, - {"Ctrl + wheel", "The radius of the curve, while laying on from an end"}, - {"Shift + wheel", "The shape: arc then straight, straight then arc, arc, straight"}, - {"Backspace", "Takes the last piece back"}, - {"Esc", "Ends laying"}}, - {{first_step::tracks, "Open the Tracks field of work"}, {first_step::lay, "Turn the lay tool on"}, {first_step::laid, "Lay a track"}}, + {"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; 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", - {{"Ctrl + wheel", "The radius of the curve, while laying on from an end"}, - {"Shift + wheel", "The shape: arc then straight, straight then arc, arc, straight"}, + {{"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 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 piece, Enter lays it"}, + {"0 1 2 3", "Type the length of the next straight, Enter puts the point"}, {"wheel", "Alone it zooms, as everywhere"}}, - {{first_step::wheel, "Change the radius with Ctrl+wheel"}, {first_step::shape, "Change the shape with Shift+wheel"}}, + {{first_step::bend, "Lay a track with a bend in it"}, {first_step::trace, "Switch to tracing a curve with Tab"}}, trial::lay}, {"Correcting a curve", - nullptr, + "plan", {{"G", "The plan tool: a click on a curve shows its design, the vertex and the radius to drag in the view"}, - {"LMB + move", "In the panel, the band of the curvature: drag the plateau of the arc up or down for its radius"}, + {"LMB + move", "In the panel, the band of the curvature: drag the plateau of an arc up or down for its radius, each arc of a compound curve its own"}, {"LMB + move", "Drag a ramp sideways: the transition curve gets longer or shorter"}, {"wheel", "Over the band: the radius by 5%, a transition by 5 m"}, {"LMB", "Apply to the scenery: the tracks are laid by the design"}}, {{first_step::plan, "Open the plan of a curve"}, {first_step::plan_band, "Change the curve in the band"}, {first_step::plan_apply, "Apply it to the scenery"}}, - trial::plan, - tutorial_curve_sketch}, + trial::plan}, {"Joining tracks", "join", {{"LMB + move", "Press on a free end (a magenta ring) and drag: a new track goes out of it"}, {"LMB", "Let go on another free end: a curve joins the two tracks"}, {"Ctrl + wheel", "While over the other end: the radius of the joining curve"}, {"Shift + wheel", "While over the other end: another way to join"}}, - {{first_step::join, "Join two free ends"}}, + {{first_step::join, "Join two free ends"}, {first_step::wheel, "Change the radius of the joining curve with Ctrl+wheel"}}, trial::lay}, {"Switches", "switch", {{"T", "The switch tool; at the top: what to place"}, - {"LMB", "Click a track: the switch goes into it"}, - {"LMB + move", "Press on it and drag: along the direction, sideways the side"}, - {"LMB + move", "Connection: drag from a straight onto the other track"}, + {"LMB", "Click a track: the one the switch goes into"}, + {"LMB + move", "Press on it and drag: along the direction, sideways the side; let go: the switch is laid"}, + {"LMB + move", "Connection: drag from a straight onto the other track; a crossover, a double one or scissors, as chosen at the top"}, + {"LMB + move", "Switch ladder: drag from the straight to the side the tracks go off to; the ones missing are laid with it"}, {"Esc", "Back to selecting"}}, {{first_step::turnout, "Put a switch into a track"}}, trial::turnout}, {"Signals and posts", nullptr, - {{"H", "The signals tool: the type of the signal in the panel, from the templates of the scenery"}, + {{"I", "The signalling tool: click a type of signal in its Signals, from the templates of the scenery"}, {"LMB", "A click by a track places the signal on that side"}, {"LMB + move", "Press by the track and drag along it: the trains running that way see the signal"}, {"LMB + move", "Drag a standing signal: it moves along its track"}, @@ -5420,15 +5941,25 @@ void editor_mode::render_first_steps() {"Selecting and fixing", "edit", {{"Esc", "The select tool"}, - {"LMB", "Click a track: it is selected, with its points"}, - {"LMB + move", "Drag a point: the end or the curve follows"}, - {"Q W E", "Move, turn or scale with the gizmo"}, - {"Gizmo", "Tool options, Gizmo, Off: the track won't move by chance"}, + {"LMB", "Click a track: it is selected, with its ends"}, + {"LMB + move", "Drag an end: at a joint the neighbours follow, from a free end a new track goes out, with Ctrl the end moves"}, {"RMB", "A click on a track: what can be done with it"}, {"Del", "Deletes the selected"}, - {"Ctrl + Z", "Takes the last change back; Ctrl+Y brings it back again"}}, + {"Ctrl + Z", "Takes the last change back; Ctrl+Y brings it back again"}, + {"Q W E", "The gizmo, to move, turn or bend the track: the next lesson"}}, {{first_step::select, "Select a track"}, {first_step::undo, "Take a change back"}}, trial::select}, + {"Editing with the gizmo", + "gizmo", + {{"Q", "The gizmo on the selected track, hidden until then: arrows which move it"}, + {"W", "Rings which turn it: its direction, its grade and its cant; the tracks joined to it follow"}, + {"E", "Handles which lengthen it at its free end, the curve keeping its radius"}, + {"LMB + move", "With the gizmo out the control vectors are there too: drag one, the curve follows it"}, + {"LMB", "A click on a point of the track: the gizmo goes to it, that point alone is moved or turned"}, + {"Ctrl", "Held while dragging: the gizmo goes by its step"}, + {"Esc", "Releases the point; the arrow above the view hides the gizmo again"}}, + {{first_step::gizmo, "Bring out the gizmo with Q, W or E"}, {first_step::vector, "Bend a track by a control vector, or move it with the gizmo"}}, + trial::select}, {"The grade line", nullptr, {{"P", "The profile tool: a click on a track opens the grade line of its whole line in the strip below"}, @@ -5443,11 +5974,12 @@ void editor_mode::render_first_steps() tutorial_route_sketch}, {"Terrain from the terrain model", nullptr, - {{"F5", "The Terrain field of work: the terrain model (NMT) is in its tool options"}, - {"1", "Location: where the scenery lies in PUWG 1992; a scenery begun from nothing asks for it with its first track"}, - {"2", "What gives way: the tracks go onto the terrain, or the terrain is drawn to the tracks"}, - {"3", "Load: the model comes from geoportal.gov.pl, the terrain is made along the tracks with the formation and the slopes"}, - {"Ctrl + Z", "Takes the changes of the tracks back"}}, + {{"L", "In a scenery begun from nothing the first L asks: the real terrain of the place, or the flat plane"}, + {"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: 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, tutorial_terrain_sketch}, @@ -5482,13 +6014,14 @@ void editor_mode::render_first_steps() m_tutorial_since = ImGui::GetTime(); } auto const scale{Global.ui_scale}; - auto const width{520.0f * scale}; ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowBgAlpha(1.0f); ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 0.0f), ImVec2(FLT_MAX, ImGui::GetIO().DisplaySize.y - 60.0f)); keep_on_screen("###tutorial"); if (ImGui::Begin((std::string(STR_C("Tutorial")) + "###tutorial").c_str(), &m_first_steps_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { + // floating it's as wide as the clips; docked it takes the width of its dock, the texts wrapped to it + auto const width{ImGui::IsWindowDocked() ? std::max(160.0f * scale, ImGui::GetContentRegionAvail().x) : 520.0f * scale}; ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + width); ImGui::TextDisabled(STR_C("Lesson %d of %d"), m_tutorial_page + 1, static_cast(lessons.size())); ImGui::SameLine(); @@ -5559,23 +6092,23 @@ void editor_mode::render_first_steps() } case trial::signals: { - ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::signals); - if (ImGui::Button(STR_C("The signals tool (H)"))) + ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::srk); + if (ImGui::Button(STR_C("The signalling tool (I)"))) { choose_work_area(work_area::tracks); - show_track_tab(track_tab::signals); + show_track_tab(track_tab::srk); } ImGui::EndDisabled(); break; } case trial::trainset: { - ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::lineside && m_vehicle.open); + ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::lineside && m_vehicle.tab().open); if (ImGui::Button(STR_C("The trainset tool"))) { choose_work_area(work_area::tracks); show_track_tab(track_tab::lineside); - m_vehicle.expand = true; + m_vehicle.tab().expand = true; } ImGui::EndDisabled(); break; @@ -5691,10 +6224,10 @@ void editor_mode::render_shortcuts_window() {"F", "Fly to the selected"}, {"O", "Top view, orthographic"}}}, {"Gizmo", - {{"Q / W / E", "Translate / rotate / scale"}, + {{"Q / W / E", "Translate / rotate / scale; in the tracks they bring the hidden gizmo out"}, {"R", "Local space"}, {"Ctrl held while dragging", "Snap to the step set in the gizmo settings"}, - {"Tool options, Gizmo, Off", "Hides the gizmo, so nothing moves by chance"}}}, + {"Tool options, Gizmo, Off", "Hides the gizmo, so nothing moves by chance; in the tracks it is hidden until Q, W or E"}}}, {"Surroundings", {{"1 2 3 4 5", "Select, insert, brush, area fill, copy to bank"}, {"LMB", "Select a model, or place one in the insert and brush modes"}, @@ -5709,22 +6242,25 @@ 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"}, - {"H / B", "Signals / objects along the track"}, + {"I / B", "Signalling / objects along the track"}, {"P / V / J", "Profile / speed / joints"}, {"Ctrl+F", "Find a track by its name"}, {"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"}, - {"L, LMB, LMB...", "Lay track: each click lays a piece and goes on from its end"}, - {"Drag from a free end", "Lead a track out of it; onto another free end: join the two"}, - {"Ctrl+wheel", "Laying: radius"}, - {"Shift+wheel", "Laying: arc then straight, straight then arc, arc, straight; at a free end: another way to join"}, - {"0-9 while laying", "Type the length (or the radius) of the next piece, Enter lays it"}, + {"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; 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"}, + {"0-9 while laying", "Type the length of the next straight, Enter puts the point"}, {"Alt held while laying", "The direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track; the first click with Alt goes onto the axis of the track"}, {"N", "Parallel track: LMB on a track, then the side and the spacing"}, {"Shift+LMB on a track in L", "The Parallel tool (N) with it"}, - {"Enter / Backspace", "Laying track: lay up to the cursor / take the last piece back"}, + {"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", {{"G, LMB on a curve", "Its design: the vertex and the radius to drag in the view, the curvature in the panel"}, @@ -5782,7 +6318,7 @@ void editor_mode::render_shortcuts_window() if (false == ImGui::BeginTable(entry.title, 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) continue; ImGui::TableSetupColumn("##keys", ImGuiTableColumnFlags_WidthFixed, 250.0f * Global.ui_scale); - ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch, 1.0f); for (auto const &item : entry.shortcuts) { if (false == matches(item)) @@ -5812,6 +6348,11 @@ void editor_mode::render_shortcuts_window() ImGui::End(); } +bool editor_mode::gizmo_shown() const +{ + return ui()->mode() == nodebank_panel::TRACK ? m_track_gizmo_on : m_gizmo_enabled; +} + void editor_mode::render_gizmo_options() { if (ImGui::Button(Global.EditorOrtho ? STR_C("3D view (O)") : STR_C("Top view, orthographic (O)"))) @@ -5826,29 +6367,24 @@ void editor_mode::render_gizmo_options() if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Wheel zooms. Hold the wheel and drag to pan.")); ImGui::SameLine(); - if (ImGui::Checkbox(STR_C("Ballast"), &Global.EditorOrthoBallast)) - { - EditorSettings.top_ballast(Global.EditorOrthoBallast); - EditorSettings.save(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Off: the top view draws the rails and sleepers of the tracks without the ballast, so the ground under them shows")); + render_top_tracks_choice(true); } - if (ui()->mode() == nodebank_panel::TRACK && m_track_tab != track_tab::path && m_track_tab != track_tab::turnout && m_track_tab != track_tab::turntable && m_track_tab != track_tab::signals && + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab != track_tab::path && m_track_tab != track_tab::turnout && m_track_tab != track_tab::turntable && m_track_tab != track_tab::srk && m_track_tab != track_tab::lineside) return; - int op{m_gizmo_enabled ? static_cast(m_gizmo_op) : -1}; + auto &enabled{ui()->mode() == nodebank_panel::TRACK ? m_track_gizmo_on : m_gizmo_enabled}; + int op{enabled ? static_cast(m_gizmo_op) : -1}; if (segmented("gizmo", op, {{-1, STR_C("Off"), "The gizmo hidden: the selected things won't move by chance"}, {static_cast(gizmo_operation::translate), STR_C("Translate (Q)"), "Arrows which move the selected thing"}, {static_cast(gizmo_operation::rotate), STR_C("Rotate (W)"), "Rings which turn the selected thing"}, {static_cast(gizmo_operation::scale), STR_C("Scale (E)"), "Handles which scale the selected thing; a track is lengthened"}})) { - m_gizmo_enabled = op >= 0; - if (m_gizmo_enabled) + enabled = op >= 0; + if (enabled) m_gizmo_op = static_cast(op); } - if (!m_gizmo_enabled) + if (!enabled) return; if (terrain_brush() || terrain_clicking()) { @@ -5892,9 +6428,10 @@ void editor_mode::render_gizmo() } // the transform gizmo is suppressed while editing terrain, so the brush/chunk tool owns the mouse - if (!m_gizmo_enabled || terrain_brush() || terrain_clicking()) + if (!gizmo_shown() || terrain_brush() || terrain_clicking()) { m_gizmo_using = false; + m_track_gizmo_using = false; return; } @@ -6088,7 +6625,7 @@ void editor_mode::render_include_gizmo(glm::mat4 const &View, glm::mat4 const &P // in the signal tool a press on the mast drags the signal along its track: the gizmo, whose up axis runs along the // mast, gives way over a signal and while one is dragged - auto const yield{ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::signals && false == ImGuizmo::IsUsing() && (m_signal.moving || m_signal.placing || signal_hit() >= 0)}; + auto const yield{ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::srk && false == ImGuizmo::IsUsing() && (m_signal.moving || m_signal.placing || signal_hit() >= 0)}; ImGuizmo::Enable(false == yield); ImGuizmo::Manipulate(glm::value_ptr(View), glm::value_ptr(Projection), static_cast(operation), mode, glm::value_ptr(matrix), nullptr, snap); ImGuizmo::Enable(true); @@ -6207,6 +6744,9 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset) // no nearer than 2 m to the point, not through it if (notches > 0.0) step = std::max(0.0, std::min(step, distance - 2.0)); + // a point under the cursor which isn't a number, as one on a broken path, moves the camera nowhere + if (false == std::isfinite(step) || glm::any(glm::isnan(direction)) || glm::any(glm::isinf(direction))) + return; Camera.Pos += direction * step; Global.pCamera = Camera; return; @@ -6263,6 +6803,24 @@ void editor_mode::update_camera(double const Deltatime) // reset window state (will be set again if UI requires it) Global.CabWindowOpen = false; + // a camera at no finite place, as a zoom or a framing aimed at a broken path could put it, draws nothing sane and can + // stall the graphics card: it goes back to where it last was + auto const number = [](auto const &Vector) { return false == glm::any(glm::isnan(Vector)) && false == glm::any(glm::isinf(Vector)); }; + if (number(Camera.Pos) && number(Camera.Angle)) + { + m_camera_finite_pos = Camera.Pos; + m_camera_finite_angle = Camera.Angle; + m_camera_finite = true; + } + else if (m_camera_finite) + { + WriteLog("Editor: the camera went to no finite place, back where it was", logtype::generic); + Camera.Pos = m_camera_finite_pos; + Camera.Angle = m_camera_finite_angle; + Camera.Velocity = glm::dvec3(0.0); + m_focus_active = false; + } + // publish camera back to global copy Global.pCamera = Camera; } @@ -6341,7 +6899,8 @@ void editor_mode::enter() std::vector found; auto const ground{ground_heights({glm::dvec3{0.0, 5000.0, 0.0}}, true, &found).front()}; Camera.Pos = glm::dvec3{0.0, (found.front() != 0 ? ground : 0.0) + 60.0, 0.0}; - Camera.Angle = glm::vec3(glm::radians(-45.0f), 0.0f, 0.0f); + // facing north, the top view with the north up as on a map: the yaw 0 looks along -z, to the south + Camera.Angle = glm::vec3(glm::radians(-45.0f), glm::pi(), 0.0f); Camera.m_owner = nullptr; FreeFlyModeFlag = true; if (false == Global.EditorOrtho) @@ -6360,6 +6919,8 @@ void editor_mode::enter() EditorModeFlag = true; load_orthophoto_settings(); + m_orthophoto.area_paused(m_area_paused); + m_nmt.prefetch_paused(m_area_paused); Application.set_cursor(GLFW_CURSOR_NORMAL); } @@ -6386,9 +6947,18 @@ void editor_mode::exit() g_redo.clear(); m_history.clear(); + m_orphans.clear(); 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); scene::Layers.show_all(); // likewise for hidden scenery layers, the other modes get the scenery as it was defined // drop selection so a stale/dangling node pointer isn't used on the next editor session @@ -6417,15 +6987,17 @@ void editor_mode::delete_model(TAnimModel *model, bool const Joined) m_history.back().joined = Joined; WriteLog(debug, logtype::generic); - // clear history pointers referencing this model before actually deleting it + erase_model(model); +} + +void editor_mode::erase_model(TAnimModel *model) +{ nullify_history_pointers(model); remove_from_hierarchy(model); - if (m_node == model) { m_node = nullptr; m_dragging = false; - ui()->set_node(nullptr); } simulation::State.delete_model(model); } @@ -6520,9 +7092,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in switch (Key) { case GLFW_KEY_O: toggle_ortho(); break; - case GLFW_KEY_Q: m_gizmo_op = gizmo_operation::translate; break; - case GLFW_KEY_W: m_gizmo_op = gizmo_operation::rotate; break; - case GLFW_KEY_E: m_gizmo_op = gizmo_operation::scale; break; + // in the tracks they also bring the gizmo out, hidden until then + case GLFW_KEY_Q: m_gizmo_op = gizmo_operation::translate; m_track_gizmo_on |= ui()->mode() == nodebank_panel::TRACK; break; + case GLFW_KEY_W: m_gizmo_op = gizmo_operation::rotate; m_track_gizmo_on |= ui()->mode() == nodebank_panel::TRACK; break; + case GLFW_KEY_E: m_gizmo_op = gizmo_operation::scale; m_track_gizmo_on |= ui()->mode() == nodebank_panel::TRACK; break; case GLFW_KEY_R: m_gizmo_local = !m_gizmo_local; break; case GLFW_KEY_B: handled = ui()->mode() != nodebank_panel::TRACK && bend_shortcut(); break; case GLFW_KEY_F1: m_shortcuts_open = !m_shortcuts_open; break; @@ -6661,7 +7234,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; @@ -6669,6 +7245,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // an open list or menu takes this Esc for itself, the selection behind it stays if (ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) break; + // the download area being drawn given up, the one before stays + if (is_press(Action) && m_area_drawing.active) + { + area_finish(false); + break; + } if (is_press(Action)) { road_cancel(); @@ -6676,8 +7258,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // the first Esc drops what is going on in the track mode, the next one goes back to the selection if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) { - if (m_lay.active && false == m_lay.points.empty()) - lay_cancel(); + if (m_tape.active) + m_tape = {}; + else if (m_route.trace.active && route_active()) + route_trace_end(false); + else if (m_lay.active && false == m_lay.points.empty()) + lay_escape(); else if (m_track_style.picking) m_track_style.picking = false; else if (track_busy()) @@ -6685,6 +7271,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in cancel_track_tools(); m_straights.tool = 0; } + else if (m_track_tab == track_tab::srk && m_signal.armed) + m_signal.armed = false; else if (m_track_tab == track_tab::path && m_track_set.size() >= 2) select_tracks({}); else if (m_track_tab != track_tab::path) @@ -6698,18 +7286,51 @@ 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(); + break; + case GLFW_KEY_BACKSPACE: + // the download area: the last point taken back + if (is_press(Action) && m_area_drawing.active) + { + if (false == m_area_drawing.points.empty()) + m_area_drawing.points.pop_back(); + break; + } // area fill: remove the last outline point if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) m_fill_points.pop_back(); if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active && !m_lay.points.empty()) lay_back(); + if (is_press(Action) && m_route.trace.active && route_active() && !m_route.trace.points.empty()) + { + m_route.trace.points.pop_back(); + route_trace_fit(); + } break; case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: + // the download area closed, from three points on + if (is_press(Action) && m_area_drawing.active) + { + if (m_area_drawing.points.size() >= 3) + area_finish(true); + break; + } if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active) lay_enter(); + if (is_press(Action) && m_route.trace.active && route_active()) + route_trace_end(true); break; case GLFW_KEY_K: @@ -6766,6 +7387,17 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } + // the download area drawn on the map takes the left button while it's drawn + if (m_area_drawing.active && Button == GLFW_MOUSE_BUTTON_LEFT) + { + if (is_press(Action)) + area_click(Global.shiftState); + else + m_area_drawing.dragged = -1; + m_input.mouse.button(Button, Action); + return; + } + // the chunk, spacing and water tools of the terrain work on a click (Shift: the other way round) if (terrain_clicking() && Button == GLFW_MOUSE_BUTTON_LEFT) { @@ -6865,6 +7497,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const auto *const previous{m_node}; m_node = nullptr; select_include(0); + ui()->set_node(additive ? previous : nullptr); // in a scenery opened for editing selection has its limits. nodes of locked layers are off limits. // nodes defined by a scenery template can't be rewritten one by one on save, so a click on one @@ -6888,7 +7521,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const } else if (node->from_template() || false == scene::Layers.editable(node, &reason)) { - ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), (node->name().empty() ? std::string{STR_C("(no name)")} : node->name()).c_str(), reason.c_str()), true); + ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), name_of(*node).c_str(), reason.c_str()), true); node = nullptr; } } @@ -6970,7 +7603,22 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (m_extend.active) finish_extend(); if (m_switch.placing) - finish_switch_placement(); + { + // 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) + { + m_switch.placing = false; + if (m_switch.free) + { + m_switch.free = false; + track_click_select(nullptr); + } + } + else + finish_switch_placement(); + } if (m_turntable.placing) turntable_finish_placement(); if (m_signal.placing || m_signal.moving) @@ -7036,7 +7684,7 @@ void editor_mode::render_change_history(){ int maxsize = m_max_history_size; if (ImGui::InputInt(STR_C("Max history size"), &maxsize)) { - m_max_history_size = std::max(0, maxsize); + m_max_history_size = std::max(1, maxsize); if ((int)m_history.size() > m_max_history_size && m_max_history_size >= 0) { auto remove_count = (int)m_history.size() - m_max_history_size; @@ -7071,7 +7719,8 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : s.action == EditorSnapshot::Action::Array ? "ARR" : - s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH", + s.action == EditorSnapshot::Action::Terrain ? "TER" : + s.action == EditorSnapshot::Action::Kilometrage ? "KM" : "OTH", s.node_name.empty() ? STR_C("(no name)") : s.node_name.c_str(), s.position.x, s.position.y, s.position.z); @@ -7171,3 +7820,51 @@ void editor_mode::starter_trainset(std::vector &Trailing) return; } } + +void editor_mode::render_top_tracks_choice(bool const Combo) +{ + static char const *const labels[]{"Whole", "Without ballast", "Rails only"}; + static char const *const tooltips[]{"The tracks as in the 3D view", "The rails and the sleepers, the ground under them shows", "The rails alone, without the sleepers and the timbers of the switches, as on a plan"}; + auto chosen{std::clamp(Global.EditorOrthoTracks, 0, 2)}; + auto const pick = [&](int const Choice) { + Global.EditorOrthoTracks = Choice; + EditorSettings.top_tracks(Choice); + EditorSettings.save(); + }; + if (false == Combo) + { + for (int choice = 0; choice < 3; ++choice) + { + if (ImGui::MenuItem(STR_C(labels[choice]), nullptr, chosen == choice)) + pick(choice); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(tooltips[choice])); + } + return; + } + ImGui::SetNextItemWidth(130.0f); + if (ImGui::BeginCombo(STR_C("Tracks"), STR_C(labels[chosen]))) + { + for (int choice = 0; choice < 3; ++choice) + { + if (ImGui::Selectable(STR_C(labels[choice]), chosen == choice)) + pick(choice); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(tooltips[choice])); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How the top view draws the tracks")); + if (chosen < 2) + return; + ImGui::SameLine(); + auto colour{ImGui::ColorConvertU32ToFloat4(EditorSettings.top_rails())}; + if (ImGui::ColorEdit3("##toprails", &colour.x, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) + { + EditorSettings.top_rails(ImGui::ColorConvertFloat4ToU32(ImVec4(colour.x, colour.y, colour.z, 1.0f))); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The colour of the rails, to stand out on the imagery and the map")); +} diff --git a/application/editormode.h b/application/editormode.h index 810bbf19..0a768c54 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -14,20 +14,33 @@ http://mozilla.org/MPL/2.0/. #include "input/editormouseinput.h" #include "input/editorkeyboardinput.h" #include "vehicle/Camera.h" +#include "application/editormodegauge.h" +#include "application/editormodejoints.h" +#include "application/editormodelineside.h" +#include "application/editormodeparallel.h" +#include "application/editormodespeed.h" +#include "application/editormodesrk.h" +#include "application/editormodekm.h" +#include "application/editormodevehicle.h" +#include "application/editortool.h" #include "scene/sceneeditor.h" #include "scene/scenenode.h" #include "editor/editorTerrain.hpp" #include "editor/editorTerrainStreamer.hpp" #include "editor/editorOrthophoto.hpp" #include "editor/editorNmt.hpp" +#include "editor/editorGeoMap.hpp" #include "editor/editorTrack.hpp" #include "editor/editorConnections.hpp" #include "editor/editorAlignment.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" +#include "editor/editorSwitchCurve.hpp" +#include "editor/editorKilometrage.hpp" #include "editor/editorSpeed.hpp" #include "editor/editorRoad.hpp" #include "editor/editorProfile.hpp" +#include "editor/editorGroundFollow.hpp" #include "editor/editorInfra.hpp" #include "editor/editorGauge.hpp" #include "editor/editorClips.hpp" @@ -37,8 +50,10 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include +#include #include #include #include @@ -51,42 +66,13 @@ class TAnimModel; // a tool of a field of work of the editor, a button in the toolbar and over the 3d view struct editor_tool_button; class TTrack; -namespace ui -{ -class vehicles_bank; -struct vehicle_desc; -struct skin_set; -} struct catenary_editor; // application/editormodecatenary.h -class editor_mode : public application_mode, private editor_track::observer +class editor_mode : public application_mode, private editor_track::observer, private editor_services { public: - // model entering the structure gauge, at its deepest place - struct gauge_hit - { - std::string model; - glm::dvec3 point{0.0}; - double depth{0.0}; // m - TTrack const *track{nullptr}; // nearest one, may be gone by now - int path{0}; - glm::dvec2 direction{0.0, 1.0}; // of the track there, in the plan - }; - // joint of the paths found wrong by the joints check - struct joint_issue - { - enum class kind { gap, step, kink, cant, grade, model, open }; - kind type{kind::gap}; - TTrack *track{nullptr}; - editor_track::point_ref point; - TTrack *other{nullptr}; - editor_track::point_ref theirs; - glm::dvec3 position{0.0}; - double value{0.0}; // m, deg, mm, per mille; m of the track over the terrain model - bool fixed{false}; - }; // constructors editor_mode(); ~editor_mode() override; @@ -144,7 +130,7 @@ class editor_mode : public application_mode, private editor_track::observer struct stored_profile; struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain, Kilometrage }; // copy of a model an array made struct array_copy { @@ -182,9 +168,16 @@ class editor_mode : public application_mode, private editor_track::observer std::vector profile_library; std::optional> alignments; // the kept horizontal alignments changed too, these were the ones before std::optional> switch_records; // the kept switches changed too, these were the ones before + std::optional kilometrage; // the kilometrage before the change (Action::Kilometrage) std::vector copies; // models an array added to the one it was made of bool joined{false}; // undone and redone together with the change before it std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) + std::uint64_t serial{next_serial()}; // of each step made, for the revision of the history + static std::uint64_t next_serial() + { + static std::uint64_t counter{0}; + return ++counter; + } }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -198,12 +191,16 @@ class editor_mode : public application_mode, private editor_track::observer void redo_last(); // a single step of the history, see undo_last() void undo_one(); + void drop_step(std::size_t const Step); + std::string refused(std::string Text); void redo_one(); // the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush, // or what a frame did; the next change is recorded from now on void terrain_history_step(); // puts the terrain back as the step recorded it, the step left with the state it replaced void restore_terrain(EditorSnapshot &Snapshot); + // swaps the kilometrage with the one the snapshot keeps + void restore_kilometrage(EditorSnapshot &Snapshot); bool m_history_busy{false}; // undo or redo under way void handle_brush_mouse_hold(int Action, int Button); void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value); @@ -220,6 +217,10 @@ class editor_mode : public application_mode, private editor_track::observer glm::vec3 m_focus_target_angle{0.0f}; // camera pitch/yaw/roll facing the focused object double m_focus_time{0.0}; double m_focus_duration{0.6}; + // the last place and angles of the camera which were numbers: a camera gone to no finite place goes back there + glm::dvec3 m_camera_finite_pos{0.0, 0.0, 0.0}; + glm::vec3 m_camera_finite_angle{0.0f}; + bool m_camera_finite{false}; double fTime50Hz{0.0}; // bufor czasu dla komunikacji z PoKeys scene::basic_editor m_editor; @@ -251,7 +252,7 @@ class editor_mode : public application_mode, private editor_track::observer int m_selected_history_idx{-1}; glm::dvec3 clamp_mouse_offset_to_max(const glm::dvec3 &offset); // the ground under a point the cursor landed on, for things which are to stand on the ground - glm::dvec3 placement_on_ground(glm::dvec3 Location); + glm::dvec3 placement_on_ground(glm::dvec3 Location) override; // puts every instance of the model the selected node shows on the ground under it void drop_model_instances(); void render_object_menu(); @@ -261,9 +262,9 @@ class editor_mode : public application_mode, private editor_track::observer void render_help_menu(); void render_shortcuts_window(); bool m_shortcuts_open{false}; - enum class first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; + using first_step = editor_first_step; void render_first_steps(); - void first_step_done(first_step const Step); + void first_step_done(first_step const Step) override; bool m_first_steps_open{false}; std::array(first_step::count)> m_first_steps_done{}; int m_tutorial_page{0}; @@ -327,16 +328,7 @@ class editor_mode : public application_mode, private editor_track::observer bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0); // the terrain from the digital terrain model of GUGiK (editormodenmt.cpp) - struct nmt_place - { - glm::dvec3 position{0.0}; // on the axis of the path - double formation{0.0}; // the height of the formation under it, the foot of the ballast - double crown{0.0}; // the height of the top of the ballast under the rails: what the terrain model measures on a track - double half_width{3.0}; // from the axis to the foot of the ballast - TTrack *track{nullptr}; - int path{0}; - bool shapes{true}; // not a bridge nor a tunnel - }; + using nmt_place = ::nmt_place; struct nmt_state { double offset{0.0}; // added to the heights of the model, so they meet the tracks of the scenery @@ -348,16 +340,30 @@ class editor_mode : public application_mode, private editor_track::observer double band{300.0}; // m from the tracks the chunks made along them reach bool shape{true}; // the formation and the slopes of the tracks as modifiers of the terrain bool grow{true}; // while a track is laid, the terrain is made around the cursor where there's none + bool laying{false}; // the tracks committed are being laid: they leave the terrain as it is + bool follow{true}; // a track with its formation edited takes it along, the formation led anew bool grown{false}; // the chunks waiting were all asked for by growing: the formations aren't made anew after them + bool reshape{false}; // the formations of the tracks laid anew once the chunks waiting are made, as the tracks are put onto the terrain + bool unfitted{false}; // the tracks put onto the terrain are only the ones whose grade line isn't fitted yet double grow_time{-10.0}; 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 + std::size_t foreign{0}; // chunks not made ahead: the terrain beside isn't of the model + std::optional ahead; // how much the terrain made ahead lies over the model + std::set changed; // tracks whose formations are to follow them + std::set gone; // names of the tracks removed, whose formations go with them + double changed_time{0.0}; + bool restoring{false}; // the history is taken back or forward: the terrain comes back with it 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 { @@ -367,17 +373,45 @@ class editor_mode : public application_mode, private editor_track::observer }; 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 std::vector done; // tracks of the lines fitted already: the lines fitted later end on them and don't change them // the rail tops of the lines fitted already, in squares of the plan: the tracks of a station lie in one plane, a line laid - // near them takes their height for its ground too, the more closely the nearer it is - std::map, std::vector> laid; + // near them takes their height for its ground too, the more closely the nearer it is, and a line crossing one of them + // on its level its height on the crossing + struct laid_point + { + glm::dvec3 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + double was{0.0}; // the height before the line was fitted + double rails{0.0}; // the height of the rails of the track + TTrack const *track{nullptr}; + }; + std::map, std::vector> laid; + // the points of all the paths as they were laid, for the crossings + std::map, std::vector> paths; + // ends which meet in the plan unlinked, laid apart in height, that the joining of the ends couldn't join as neither of + // them can be changed alone, as of two switches: each with the other one, which a line ending there takes over + 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}; @@ -385,16 +419,15 @@ class editor_mode : public application_mode, private editor_track::observer std::map, std::vector> anchor_cells; // the anchors in squares of the plan bool offered{false}; // the terrain was offered to the scenery begun without it bool prompt{false}; // the offer is shown - double north{0.0}; // the place typed into the offer, PUWG 1992 - double east{0.0}; - int mode{0}; // 0: the tracks go onto the terrain, 1: the terrain is drawn to the tracks - bool moving{false}; // the place being typed again + bool moving{false}; // the place being picked again + char place[96]{}; // typed: degrees or PUWG 1992 + int shown{-1}; // the frame the terrain panel showed how far the work got }; editor_nmt m_nmt; nmt_state m_nmt_state; - std::vector nmt_track_places(double const Step) const; - void nmt_locate(); - bool nmt_choice_card(int const Index, char const *Title, char const *Text, float const Width, float const TextHeight, bool const Chosen) const; + editor_map m_nmt_map; + std::vector nmt_track_places(double const Step) const override; + void nmt_locate() override; void nmt_fit(); void nmt_finish_fit(); // the terrain under the tracks, the tracks left as they are: the model drawn to each of them, its relief kept @@ -404,28 +437,105 @@ class editor_mode : public application_mode, private editor_track::observer double nmt_offset_at(double const X, double const Z) const; std::vector nmt_keys_around() const; std::vector nmt_keys_along_tracks() const; - // Rebuild: the chunks there already are made anew too, and the formation laid before is forgotten, as when the tracks moved - void nmt_make(std::vector const &Keys, bool const Rebuild = false); - bool add_chunk_from_nmt(heightmap::chunk_key const &Key); + // only the chunks there are none of yet: a chunk there is, made from the model or by hand, edited or not, is never made anew. + // Reshape: the formations of the tracks laid anew as the tracks lie now, as when they moved + void nmt_make(std::vector const &Keys, bool const Reshape = false); + enum class nmt_chunk + { + waiting, // for its squares of the model + made, + empty // the model has no data there + }; + // 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; + // the same along the edges the chunk shares with the chunks beside it in memory + std::optional nmt_seam_offset(heightmap::chunk_key const &Key, bool &Foreign) const; + // the terrain beside the formation of the track is the one made from the model; Waiting: its squares of the model aren't in yet + // the tracks changed by hand are told to the terrain: their formations laid anew, of the tracks removed taken away, a + // track laid onto the terrain of the model given its own, once the mouse lets go; in the step of the history of the change + void nmt_track_changed(std::vector const &Tracks); + void nmt_track_gone(TTrack const &Track); + void nmt_follow_tracks(); + // the height the ground will have where the terrain is made ahead of the track laid and isn't there yet + bool nmt_ahead(double const X, double const Z, double &Height) const; + // the same, else the height of the model with the offset of the scenery where its square is in: the ground where there's no + // terrain made yet, as under the base map of the top view + bool nmt_ground(double const X, double const Z, double &Height) const; + // the squares of the model under the rectangle are in, none of them waiting to be asked for again + bool nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max); // Only: the paths of these tracks are shaped anew, the modifiers of those removed taken away; null: all of them. the number // of the modifiers laid std::size_t nmt_shape_tracks(std::vector const *Only = nullptr); void nmt_update(); void nmt_grow(); void render_nmt_ui(); + // where the scenery lies: the map to click on, the place typed; true once it's placed + bool render_nmt_place(bool const Moving, bool const Button); + // the work under way: what's left of it and the button to stop it; Compact: one line, for the 3d view + void render_nmt_progress(bool const Compact); + // the formations of the tracks led into the terrain, down or up to it, or taken out of it; one step of the history each + void nmt_pull_terrain(std::vector const &Tracks); + void nmt_restore_terrain(std::vector const &Tracks); + // the terrain of the selected tracks in the panel of the tracks + void render_track_terrain(std::vector const &Tracks); + // all the tracks of the scenery onto the terrain model, or it under them: step 3 of the terrain model (Terrain_panel), and in + // the panel of the tracks + void render_nmt_all_tracks(bool const Terrain_panel); + // the chunks waiting for the terrain model and the tiles of the imagery asked for outlined where they will be, so it's seen + // what's on its way + void draw_nmt_pending() const; + // the download area: a polygon drawn on the map, the boundary of what the orthophoto and the terrain model download in the + // background, none of them asking for anything outside it (application/editormodenmt.cpp) + void area_start(); + // Keep: the area drawn taken, else the one before stays + void area_finish(bool const Keep); + // a press of the left button while drawing: a point added, or the one under the cursor taken to drag, Shift: removed + void area_click(bool const Shift); + void area_update(); + // a point of the area (x, z) where it's drawn: over the map of the top view, on the ground in the 3d view + glm::dvec3 area_point(glm::dvec2 const &Point) const; + void draw_download_area() const; + void render_download_area_ui(editor_orthophoto::statistics const &Stats); + // the chunk of the terrain (x, z) touches the area, or there's none + bool in_download_area(glm::dvec2 const &Low, glm::dvec2 const &High) const; + // over the 3d view while the terrain panel isn't seen + void render_nmt_banner(); + bool nmt_busy() const; + // the terrain made so far stays, what's waiting is dropped, the grade lines not fitted yet are left + void nmt_stop(); // a scenery begun from nothing, with no tracks nor terrain, is asked as the first track is laid whether it wants the terrain void nmt_offer(); void render_nmt_prompt(); - // the place typed in the terrain window: the terrain model and the orthophoto take it - void nmt_place_scenery(); + // the place picked in the terrain window: the terrain model and the orthophoto take it + 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(); - void nmt_auto_start(); + // 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(); - // the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be - bool nmt_fit_line(TTrack &Track); + // the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be. + // The line runs over the tracks not Fitted, the ones fitted stay as they are and the line meets them. Where the model falls + // away under the line by the abutments the tracks go over on a bridge; Laid: the tracks just laid, the formation fills it and + // they are cut only where they can't follow the line otherwise + bool nmt_fit_line(TTrack &Track, std::function const &Fitted, bool const Laid); + // the tracks laid in one go since the history step From put onto the terrain model as one line, as the tracks go onto the + // terrain: the track ending at End then the one the laying goes on from + bool nmt_fit_laid(std::size_t const From, glm::dvec3 const End); + // the tracks laid one after another on one straight or one arc, cut by the grade lines fitted before, made one again + void nmt_merge_laid(std::unordered_set &Laid); + // the points of the paths of the track about a metre apart, in squares of the plan + static void nmt_sample_paths(TTrack const &Track, std::map, std::vector> &Points); // the tool of the terrain the left mouse button works enum class terrain_tool { @@ -455,6 +565,8 @@ class editor_mode : public application_mode, private editor_track::observer void render_grass_ui(); // 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; } @@ -464,6 +576,8 @@ class editor_mode : public application_mode, private editor_track::observer 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 { @@ -640,8 +754,16 @@ class editor_mode : public application_mode, private editor_track::observer 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 @@ -668,17 +790,52 @@ class editor_mode : public application_mode, private editor_track::observer 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 std::string m_orthophoto_mark; + std::vector m_orthophoto_area_mark; // the lines of the download area as the scenery keeps them + // the download area being drawn on the map: its points in the world (x, z), the one dragged + struct area_drawing + { + bool active{false}; + std::vector points; + int dragged{-1}; + } m_area_drawing; + std::vector m_nmt_prefetched; // the download area the terrain model was given + bool m_area_paused{false}; // the download of the area held by the button; out of the editor it's held anyway glm::dvec2 m_orthophoto_origin_edit{0.0}; // origin being typed in (northing, easting); applied when editing ends // hierarchy management void add_to_hierarchy(scene::basic_node *node); void remove_from_hierarchy(scene::basic_node *node); + void erase_model(TAnimModel *model); scene::basic_node* find_in_hierarchy(const std::string &uuid_str); scene::basic_node* find_node_by_any(scene::basic_node *node_ptr, const std::string &uuid_str, const std::string &name); @@ -760,6 +917,8 @@ class editor_mode : public application_mode, private editor_track::observer void render_gizmo(); // gizmo settings, drawn in the toolset window void render_gizmo_options(); + // how the top view draws the tracks: as a combo with the colour of the rails, or as items of a menu + void render_top_tracks_choice(bool const Combo); // the tools of the field of work in the toolbar under the menu void render_toolbar(); // an edit mode of the node bank as the field of work, from the toolbar or its key (1-5) @@ -777,12 +936,20 @@ class editor_mode : public application_mode, private editor_track::observer void choose_work_area(work_area const Area); void render_workspaces(); bool m_gizmo_enabled{true}; // master switch for the in-viewport gizmo + bool m_track_gizmo_on{false}; // the gizmo in the tracks: off until Q, W or E, so a track isn't moved by chance + bool gizmo_shown() const; // the switch of the field of work in use bool m_gizmo_using{false}; // tracks an ongoing drag, so a single undo snapshot is taken per drag bool m_gizmo_local{false}; // manipulate in the object's local space instead of world space gizmo_operation m_gizmo_op{gizmo_operation::translate}; // current transform mode (translate/rotate/scale) float m_gizmo_snap{1.0f}; // translation snap step (metres) applied while Ctrl is held - TTrack *selected_track() const; + TTrack *selected_track() const override; + bool track_editing() const override; + std::size_t history_revision() const override; + glm::dvec3 camera_position() const override; + track_hover const &hovered_track() const override; + std::vector const &track_set() const override; + editor_track_tab current_track_tab() const override; void render_path_ui(); void draw_track_overlay() const; editor_track::point_ref track_handle_hit() const; @@ -790,7 +957,14 @@ class editor_mode : public application_mode, private editor_track::observer void render_track_gizmo(); void commit_track_drag(bool const Force); void cancel_track_tools(); - void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}); + void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) override; + void push_kilometrage_snapshot() override; + void set_current_node(scene::basic_node *Node) override; + alignment::limits const &route_norms() const override; + void clear_track_point() override; + editor_nmt &terrain_model() override; + double terrain_model_offset() const override; + bool terrain_model_fitted() const override; // false, with the reason, when one of the paths can't be changed, its file not taking the changes back static bool tracks_editable(std::vector const &Tracks, std::string &Reason); void trim_history(); @@ -902,55 +1076,6 @@ class editor_mode : public application_mode, private editor_track::observer std::string m_track_events_missing; void render_path_events(TTrack &Track); std::string m_track_mode_notice; - // structure gauge along the line of the selected path, shown in the 3d view - struct gauge_state - { - bool open{false}; // window - bool enabled{true}; // tunnel along the selected path - std::vector profiles; - int profile{0}; // railway - int road{-1}; - TTrack const *track{nullptr}; // scanned - std::size_t history{0}; - bool pending{false}; // rescan once the edit ends - struct - { - std::vector surface; // triangles of the tunnel - std::vector edges; // lines - std::vector hits; - } line; // along the selected path - struct - { - bool scanned{false}; - std::size_t history{0}; - std::string profile; - std::vector hits; - } map; // whole scenery - struct - { - std::vector surface; - std::vector edges; - } spot; // tunnel around the hit the camera went to - int current{-1}; // hit the camera went to - bool published{false}; // overlay up to date - std::shared_ptr scan; // of the whole scenery, done in slices over the frames - } m_gauge; - void gauge_load(); - // the chosen railway and road outlines - gauge::choice gauge_choice() const; - std::vector const &gauge_hits() const; - void update_gauge(); - void scan_gauge(TTrack &Track); - void scan_gauge_map(); - void step_gauge_map(); - void finish_gauge_map(); - void gauge_publish(); - void gauge_focus(int Index); - void gauge_spot(gauge_hit const &Hit); - void render_gauge_window(); - void render_gauge_hits(); - bool render_gauge_choice(char const *Label, int &Index, bool Road); - bool render_gauge_outline(int Index); struct route_design { TTrack *from{nullptr}; @@ -983,12 +1108,52 @@ class editor_mode : public application_mode, private editor_track::observer int hot{-1}; double at{-1.0}; int vertex{-1}; - int part{0}; // 1 the radius, 2 the transition in, 3 the transition out + int part{0}; // 1 the radius, 2 the transition in, 3 the transition out, 4 the transition between two arcs of a compound curve + int arc{0}; // the arc of a compound curve the radius is of, 0 the first; of a transition between arcs the one after it double value{0.0}; ImVec2 press{0.0f, 0.0f}; float metres{1.0f}; // pixels of a metre and of a unit of the curvature, kept as the drag goes float curvature{1.0f}; } band; + // the part of a curve whose field the mouse is over in the panel, or which is being edited there, shown in the view + struct curve_focus + { + enum class part + { + whole, + entry, + exit, + arc, // of the radius `arc`, the first one 0 + between // the transition leading into the arc of the radius `arc` + }; + int vertex{-1}; + part what{part::whole}; + int arc{0}; + bool held{false}; // by a field being edited, the mouse over the others doesn't move it + } focus; + // the points clicked when the fragment was laid, kept with its design: the curves keep to them as their transitions and cants + // change, while hold is on + std::vector clicks; + bool hold{true}; + struct click_state + { + double offset{0.0}; // from the line, m + int vertex{-1}; // whose curve it's a point of + bool corner{false}; // a bend of straights clicked, the vertex itself rather than a point of its curve + }; + std::vector click_states; // as the line was at the revision measured + int revision{0}; // of the result, counted as it's computed + int measured{-1}; + // points clicked along the middle of the track seen on the orthophoto, the radii of the curves and their shares fitted to them after each click + struct trace_state + { + bool active{false}; + int radii{0}; // 0: as many as the points need, up to three + std::vector points; + alignment::design before; // taken back by Esc + double rms{0.0}; + double worst{0.0}; + } trace; }; void render_route_ui(); bool render_route_band(); @@ -996,10 +1161,29 @@ class editor_mode : public application_mode, private editor_track::observer bool render_route_vertices(); bool render_route_vertex(int const Index); void render_route_result(); + void render_route_trace(); + // the camera over the stretch of the line between the chainages, a curve seen whole + void route_show_curve(double const From, double const To); + // the departures of the points clicked from the line and the curves they belong to, measured again when the line changed + void route_measure_clicks(); + // the points clicked along the curve of the vertex, in the plan, the bends of straights left out + std::vector route_clicks_of(int const Vertex) const; + // the radii of the curve of the vertex fitted again to the points clicked along it, its transitions and cants as they are + bool route_hold_clicks(int const Vertex); + // the elements of the part of the curve in focus in the panel, in their order along the line + std::vector route_focused() const; + void route_trace_start(); + void route_trace_fit(); + void route_trace_end(bool const Keep); + int route_trace_hit() const; + glm::dvec3 route_trace_cursor() const; void route_reset(); 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); @@ -1014,14 +1198,29 @@ class editor_mode : public application_mode, private editor_track::observer TTrack *second{nullptr}; }; std::optional double_of(TTrack const &Track) const; + // a slip of the scenery the track is a part of: its switches, all its tracks, the switches among them, and its ends where it joins + // the tracks around, A and B of its first track through, C and D of the other one, with the way into the slip at each + struct slip_set + { + switch_store::record record; + std::vector switches; + std::vector parts; + std::array ends{}; + std::array into{}; + }; + std::optional slip_of(TTrack const &Track) const; + // the two tracks of the slip through it from end to end, as they cross where it's taken out + static std::array slip_throughs(slip_set const &Slip); // the paths of the set in the order editor_track::place_switch gives them static std::vector double_paths(double_switch const &Set); // the switch of the template, or the two switches of a double one with the plain track between them, committed and recorded std::vector create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name); + // with no Style, as the lay tool lays its tracks + std::vector create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const *Style, std::string const &Name); void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle = 0.0); void render_switch_record(TTrack const &Track) const; void route_store(); - bool digitize_drawn(std::vector const &Created, bool const Alone = true); + bool digitize_drawn(std::vector const &Created, bool const Alone = true) override; bool digitize_chain(editor_track::chain const &Chain, std::string *Reason = nullptr, TTrack **Culprit = nullptr); int digitize_parts(editor_track::chain const &Chain, std::string *Reason, int &Skipped); // the lines kept, the paths left out @@ -1058,10 +1257,12 @@ class editor_mode : public application_mode, private editor_track::observer void render_route_stored(); void route_update() { - m_route.design.norms.checked = false == m_norms_free; + m_route.design.norms.checked = true; m_route.result = alignment::compute(m_route.design); + ++m_route.revision; } void route_apply(); + void route_lay(); bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created, TTrack *&Last); void route_recommend(alignment::vertex &Vertex) const; void draw_route_overlay() const; @@ -1092,6 +1293,7 @@ class editor_mode : public application_mode, private editor_track::observer bool fit_has_before{false}; bool fit_has_after{false}; std::vector> drag_states; + std::size_t drag_step{0}; glm::dvec3 preview_start{0.0}; glm::dvec3 preview_end{0.0}; glm::dvec3 edit_start{0.0}; @@ -1127,7 +1329,7 @@ class editor_mode : public application_mode, private editor_track::observer void apply_straight_tool(); double straight_tool_radius(editor_track::straight const &Line) const; void straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius); - void straight_refresh(); + void straight_refresh() override; bool start_curve_fit(editor_track::straight const &Line, int const Handle); void update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &End); void add_detour_point(); @@ -1178,6 +1380,14 @@ class editor_mode : public application_mode, private editor_track::observer { std::vector keep_names; // the names the switches laid next take, before the ones of the tool bool curved{false}; + // a switch placed where there's no track: started on the ground, or at the free end it goes on from in its direction; the direction + // of its main track taken from the drag once it's long enough, then the drag sideways gives the side of the diverging one + 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; @@ -1185,7 +1395,10 @@ class editor_mode : public application_mode, private editor_track::observer int armed{-1}; int last{0}; // template armed the last time bool placing{false}; + // where it was pressed on the screen: let go no farther from there, the press places nothing, a click doesn't lay a switch by chance + glm::vec2 pressed_at{0.f}; double along{0.0}; + double pressed{0.0}; // along a curve: where the press was, the start of the switch drawn from it to the ends of the cant ramps glm::dvec3 point{0.0}; glm::dvec3 mouse{0.0}; editor_track::straight line; @@ -1221,6 +1434,10 @@ class editor_mode : public application_mode, private editor_track::observer 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}; @@ -1274,6 +1491,27 @@ class editor_mode : public application_mode, private editor_track::observer bool move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status); bool slide_switch(TTrack &Track, double const Distance); bool switch_slides(TTrack const &Track) const; + // a slip slid along its first track: the slip and the tracks its crossing track goes on into with their ends free move whole, the + // straights of its first track before it and past it give and take the length. Low and High: how far back and on it may go + struct slip_slide + { + glm::dvec3 shift{0.0}; // along its first track, a metre of it + std::vector movers; + // where it joins the tracks of its first track before it and past it: at its ends A and B, or at the joints of its template + // with the straights out to them going along; those tracks, the end of each there + std::array joints{}; + std::array, 2> ends{}; + double low{0.0}; + double high{0.0}; + }; + 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(); @@ -1370,11 +1608,45 @@ class editor_mode : public application_mode, private editor_track::observer std::vector line; }; 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 curve ending at the last of these + enum class lay_mode + { + straights, + curve + }; + // a curve traced on the photo as it's laid, how far the points clicked along it are from it + struct lay_trace + { + std::vector points; + std::vector offsets; + double rms{0.0}; + double worst{0.0}; + std::size_t curves{0}; + }; + // the line the points clicked make: its design, the bends among its vertices, the curves traced + struct lay_line_plan + { + alignment::design design; + alignment::result shape; + std::vector corners; + std::vector traces; + // the straights of a fixed direction the next bend slides along: from a free end, out of a curve traced + std::vector> guides; + // where the grade breaks from point to point: the bends kept as such, at the heights clicked; a curve traced, or bends made + // one curve, goes at one grade from end to end + std::vector knots; + }; // lays new track through the clicked points, with no path needed to start from struct lay_tool { bool active{false}; std::vector points; + std::vector modes; // of the points, as each was clicked + lay_mode mode{lay_mode::straights}; + int radii{0}; // of a curve traced: 0 as many as the points need, up to three + bool straights_between{true}; // a curve traced may be two with a straight between them + lay_line_plan plan; // of the line shown editor_track::snap_target start; // free end the track starts from, if any glm::dvec3 mouse{0.0}; editor_track::snap_target mouse_snap; @@ -1382,52 +1654,136 @@ class editor_mode : public application_mode, private editor_track::observer std::string preview_error; double preview_length{0.0}; double radius{500.0}; - bool transitions{false}; double piece_length{100.0}; bool copy_selected{false}; editor_track::path_style style; + bool own_speed{false}; // the speed of the style also where the track goes on from one with its own, the new tracks given it std::string status; double status_time{-100.0}; std::string skew; // what Alt turned the straight to - struct step - { - editor_track::snap_target start; - glm::dvec3 point{0.0}; - }; - std::vector chain; + double escape_time{-100.0}; // when Esc was pressed over points not laid: the second Esc drops them + std::size_t history{0}; // the step of the history the track laid last was begun at }; lay_tool m_lay; - bool m_norms_free{true}; alignment::limits laying_norms() const; - bool render_norms_choice(); glm::dvec3 lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const; // the straight track Alt turns the laid straight to: the nearest one the straight from From to To crosses, or one along From TTrack const *skew_reference(glm::dvec3 const &From, glm::dvec3 const &To, double const Reach) const; void lay_after_history(bool const Undo); void lay_back(); + void lay_escape(); + std::size_t lay_pending() const; // the points clicked and not laid yet void render_lay_ui(); void render_lay_status(); void lay_click(); void lay_finish(editor_track::snap_target const &End); void lay_cancel(); void lay_enter(); - std::vector lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const; - alignment::result lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const; + // Tab: the bends of straights or the curve traced, for the points clicked from now on + void lay_switch_mode(); + // the points the same in number as their modes, a point with none a bend of straights + void lay_trim(); + // the track through the points clicked, each the way its mode says; End: the free end it finishes at, the last point then + std::vector lay_pieces(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, double &Length, std::string &Error, + lay_line_plan *Plan = nullptr) const; + // Adjoining: the curve next to a free end on a curve goes on from its curvature, where it turns its way; else from a straight there + bool lay_line(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error, bool const Adjoining = true) const; + // whether the points next to the start of the line, or to its end, AtEnd, a free end joined there, are clicked along a curve + static bool traced_from(std::vector const &Points, std::vector const &Modes, bool const AtEnd); + // 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 + // 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 + { + return [this](alignment::design const &Base, std::vector const &Points) { return traced_stretch(Base, Points, 0, false, false); }; + } + mutable std::vector, alignment::trace_stretch>> m_stretch_cache; + // the stretch the cursor moves along is fitted on a thread, the frames drawn meanwhile with the fit done last + mutable alignment::stretch_fitter m_stretch_fitter; void render_switch_ui(); + void render_switch_timbers(); void render_rail_profile_hint(TTrack &Track); bool start_switch_placement(); void finish_switch_placement(); + // a switch with no track there: where a press on the ground starts it, at the free end of a track within reach or right there + editor_track::snap_target free_switch_end(glm::dvec3 const &Ground) const; + bool free_switch_offered() const; + 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); static curve_sample sample_at(std::vector const &Frame, double const Station); + // the curve a switch goes into as the norms look at it: its headings and its cant along it + std::vector frame_points(std::vector const &Frame) const; + // the way the switch being placed into a curve goes along it and the side its diverging track leaves to, from the drag + void curved_switch_pose(int &Direction, int &Side) const; + // the start of the switch being placed into a curve: where it was pressed, drawn to where the switch meets an end of a cant ramp or + // 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, 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; 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; - static bool curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks); + // Beyond: the straights past the ends of the curve go with it up to that far, and a straight track that near the curve is taken + // as its part + static bool curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks, double const Beyond = 0.0); static void cut_curve(std::vector &Candidates, std::vector const &Frame, glm::dvec3 const &First, glm::dvec3 const &Second, std::vector> &States, std::vector &Created, std::vector &Removed); void insert_curved_connection(); struct curved_crossover @@ -1481,31 +1837,79 @@ class editor_mode : public application_mode, private editor_track::observer 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); - 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"); + // 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", + std::function const &Bend = {}); + // a slip switch bent into a curve of a side track (ST-T1-A6 11.4 pkt 4b): laid out on the tangent to the curve at its crossing, where + // it was pressed, and bent onto the curve as a switch laid into it is, its crossing track coming along with its ends free. Direction + // and Side as of a switch dragged into a curve: the crossing track goes ahead to that side, through the quarter of the cursor, and the + // slip curve of a single slip lies on that side + struct curved_slip + { + std::string problem; + std::string snapped; // what the ends of the slip were drawn to + double middle{0.0}; // the station of the crossing along the curve + double half{0.0}; // from the crossing to the ends of the slip + double reach{0.0}; // from the crossing to the joints of the template, the straights of both tracks laid out that far + int direction{1}; + double angle{0.0}; + editor_track::slip_curve curve; + // as the slip is laid out on the tangent, before it's bent: its crossing, its tracks going Direction along the curve, its ends + glm::dvec2 crossing{0.0}; + glm::dvec2 first{0.0, 1.0}; + glm::dvec2 second{0.0, 1.0}; + std::array points{}; + }; + bool plan_curved_slip(editor_track::switch_template const &Shape, int const Direction, int const Side, curved_slip &Plan) const; + // the path of the slip laid out on the tangent, bent onto the curve + segment_data bent_slip_path(curved_slip const &Plan, segment_data const &Path) const; + // the two tracks through the slip and its slip curves, each from end to end, bent: the first track, the slip curve leaving it at its + // start, the second track, the other slip curve of a double slip + std::vector curved_slip_lines(curved_slip const &Plan, std::string const &Type) const; + // the straights of both tracks from the ends of the slip out to the joints of the template, bent + std::vector curved_slip_reaches(curved_slip const &Plan) const; + switch_curve::slip_report curved_slip_report(curved_slip const &Plan, std::string const &Type) const; + // the radii of the slip and its cant, as the view and the status tell them + static std::vector slip_report_lines(switch_curve::slip_report const &Report); + void insert_curved_slip(int const Direction, int const Side); bool start_extend(editor_track::snap_target const &End); void finish_extend(); std::vector extend_pieces() const; + // the same for the track shown while it's dragged: its grade line fitted on the thread of the fitter, the frame not waiting for it + std::vector extend_preview() const; // the free end of the path as the start of a new track; false if it isn't free or has no direction static bool free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool); // the free end new pieces are added at: the selected point if it's one, else the end, else the start @@ -1514,12 +1918,18 @@ class editor_mode : public application_mode, private editor_track::observer void extend_add(int const Side); void render_extend_ui(); using track_layout = editor_track::layout; - track_layout m_track_layout{track_layout::arc_straight}; + // the shape dragged from a free end, as the mode of laying has it: a straight in the one of the straights, an arc and a straight in + // the one of a curve + track_layout m_track_layout{track_layout::straight}; double m_join_radius{0.0}; int m_join_variant{0}; TTrack const *m_join_track{nullptr}; editor_track::point_ref m_join_point; mutable alignment::join_result m_join_last; + // the layout laid on from the end of From with the transition curves of the norms; none where it doesn't fit + std::optional eased_layout(TTrack const *From, glm::dvec3 const &Point, glm::dvec2 const &Direction, glm::dvec3 const &Target, double const Radius) const; + mutable std::optional m_eased_last; + mutable std::optional>> m_eased_cache; bool track_wheel(double const Offset); // a touchpad sends fractions of a notch: a choice moves a step for each whole notch gathered, and only one in a gesture when // Gesture; a pause, another key or the other way starts over @@ -1533,8 +1943,16 @@ class editor_mode : public application_mode, private editor_track::observer wheel_state m_wheel; int wheel_step(double const Offset, bool const Gesture); std::string track_shaping(bool const Joined) const; - void render_layout_combo(); double design_speed(TTrack const *From, TTrack const *To) const; + // the track whose speed the design takes, none where it's the one set for the laying + TTrack const *speed_track(TTrack const *From, TTrack const *To) const; + // the tracks the laying under way goes on from and to + std::pair laying_ends() const; + std::string design_speed_of(TTrack const *From, TTrack const *To) const; + // what ST-T1-A6 says of the sudden changes of the cant deficiency right behind a switch the pieces laid come out of at Start (From), + // or go into at End (To): of the end of its curve and the first sudden change of the pieces (9.11, tables 9.15, 9.16) + std::vector switch_exit_warnings(TTrack const *From, glm::dvec3 const &Start, TTrack const *To, glm::dvec3 const &End, std::vector const &Pieces) const; + void give_own_speed(std::vector const &Created) const; std::vector join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, std::string &Error) const; double m_extend_length{25.0}; @@ -1542,8 +1960,10 @@ class editor_mode : public application_mode, private editor_track::observer bool extend_snap(editor_track::snap_target &Target) const; void update_build_tools(); void draw_build_overlay() const; + // the joint before the blades of the switch being placed or the one selected and the tips of its blades + void draw_blade_tips() const; // modes of the track editor, one at a time: path is the plain selection - enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel, catenary }; + using track_tab = editor_track_tab; struct track_mode { char const *label; @@ -1553,7 +1973,7 @@ class editor_mode : public application_mode, private editor_track::observer editor_icons::icon picture; int family; // 0 builds the track, 1 equips it, 2 checks it }; - static std::array const &track_modes(); + static std::array const &track_modes(); bool track_plan_tab() const { return m_track_tab == track_tab::straights || m_track_tab == track_tab::route; } track_tab plan_tab_for(TTrack const *Track) const; // the objects along the track: a tab for each tool @@ -1589,7 +2009,7 @@ class editor_mode : public application_mode, private editor_track::observer // the track tools in the tool options window, and the selected path in the inspector void render_track_tool_options(); void render_track_selection(); - void show_track_tab(track_tab const Wanted); + void show_track_tab(track_tab const Wanted) override; bool track_analysis_tab() const { return m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_track_tab == track_tab::infra; } bool track_shortcut(int const Key); bool track_busy() const; // something going on in the mode, which Esc drops before it leaves the mode @@ -1608,6 +2028,12 @@ class editor_mode : public application_mode, private editor_track::observer 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(); void toggle_straight_set(TTrack &Track); @@ -1624,22 +2050,23 @@ class editor_mode : public application_mode, private editor_track::observer // a scenery with no tracks yet: in the middle of the view, how the first one is laid void draw_empty_scenery_hint(); // path under the cursor, outlined before a click so it's known which one the click takes - struct track_hover - { - TTrack *track{nullptr}; - int path{0}; - glm::dvec3 point{0.0}; - double since{0.0}; - }; + using track_hover = ::track_hover; track_hover m_hover; track_hover track_under_cursor(TTrack const *Only = nullptr, float const Pixels = 0.0f) const; track_hover split_spot() const; void update_track_hover(); void draw_track_lines() const; + // what the colours of the tracks in the view tell + enum class track_colouring { plain, radius, speed, grade }; + track_colouring m_track_colouring{track_colouring::plain}; + void draw_track_legend() const; + // the radii and lengths of the curves and the names and types of the switches written by them, from above, as on a plan + bool m_track_labels{true}; + void draw_track_labels() const; void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, catenary }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, switch_free, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace, kilometrage, srk_boundary, catenary }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -1651,6 +2078,17 @@ class editor_mode : public application_mode, private editor_track::observer int detail{0}; // what the tool of the intent means by it (catenary: what was hit) }; track_intent m_intent; + // the measure between two points (M): on the ground, at a free end or on the axis of the track under the cursor + struct tape_state + { + bool active{false}; + std::vector points; + glm::dvec3 mouse{0.0}; + }; + tape_state m_tape; + void tape_update(); + std::string tape_readout() const; + void draw_tape() const; track_intent track_intent_at(int const Mods); bool track_gesture_on() const; void update_track_intent(); @@ -1660,12 +2098,23 @@ class editor_mode : public application_mode, private editor_track::observer void draw_track_intent() const; editor_track::snap_target snap_free_end(glm::dvec3 const &Near, glm::vec2 const &Screen, TTrack const *Self, int const Category, std::vector const &Exclude) const; void draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const; + // the free ends of the rail tracks around the camera, looked for again after an edit or once the camera leaves the place + struct free_end_cache + { + int edits{-1}; + glm::dvec3 centre{0.0}; + double reach{0.0}; + std::vector points; + }; + mutable free_end_cache m_free_end_cache; + std::vector const &free_end_points(glm::dvec3 const &Centre, double const Reach) const; struct point_drag { bool active{false}; bool moved{false}; glm::vec2 from{0.f}; double height{0.0}; + std::size_t step{0}; }; point_drag m_point_drag; struct handle_drag @@ -1679,7 +2128,8 @@ class editor_mode : public application_mode, private editor_track::observer }; handle_drag m_handle_drag; void handle_drag_start(glm::dvec3 const &Anchor); - bool handle_drag_step(glm::dvec3 &Position); + // Ground: the handle goes onto the ground, at the height of the ballast of the track, unless Shift is held + bool handle_drag_step(glm::dvec3 &Position, TTrack const *Ground = nullptr); void point_drag_start(editor_track::point_ref const &Point); void point_drag_update(); void point_drag_finish(); @@ -1694,6 +2144,10 @@ class editor_mode : public application_mode, private editor_track::observer static std::pair trackbed(TTrack const *Style); std::vector bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const; void follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const; + ground_follow::grading laying_grading(TTrack const *Style) const; + // the grade lines of the tracks shown while they're laid are fitted on a thread of their own, not to hold up the frames + mutable ground_follow::fitter m_fitter; + mutable bool m_follow_preview{false}; glm::dvec3 lay_bed(glm::dvec3 const &Point); std::optional m_cursor_override; bool sweep_grabs(glm::dvec3 const &Point); @@ -1712,9 +2166,9 @@ class editor_mode : public application_mode, private editor_track::observer void track_context_release(); void render_track_context(); void split_track_at(TTrack &Track, glm::dvec3 const &Point); - void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point); + void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) override; // the camera flies over Duration seconds to the place Distance back from the target along Look, looking along it - void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6); + void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) override; // paths found by their names struct track_search { @@ -1784,6 +2238,9 @@ class editor_mode : public application_mode, private editor_track::observer void select_tracks(std::vector const &Tracks); // changes each track which can be changed, in one step of the undo; false from Change: the track stays as it was std::size_t tracks_apply(std::vector const &Tracks, std::function const &Change, bool const Rebuild, char const *What); + // the model of the timbers, with the fastenings where it has them, set on all the switches and crossings of the scenery, in one + // step of the undo; empty: the one of eu07.ini. returns: the tracks changed + std::size_t set_switch_timbers(std::string const &Model); int track_property_index(std::string const &Key) const; std::size_t track_property_apply(int const Property, std::vector const &Tracks, std::string const &Text); void track_field_settle(); @@ -1870,62 +2327,10 @@ class editor_mode : public application_mode, private editor_track::observer std::vector> m_track_similar; TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks int m_track_list_hover_frame{-1}; - // second track alongside the selected path or the line through it, at a spacing, curves included - struct parallel_tool - { - bool open{false}; - bool expand{false}; - double spacing{4.0}; - int side{1}; // 1: to the right, -1: to the left, of the direction of the line - int scope{1}; // 0: the selected path, 1: the line through it, up to the switches - TTrack *built_for{nullptr}; - double built_spacing{0.0}; - int built_side{0}; - int built_scope{-1}; - std::size_t built_history{0}; - TTrack *pair_src{nullptr}; - TTrack *pair_dst{nullptr}; - double measured{0.0}; - std::vector pieces; - std::vector styles; - double length{0.0}; - std::string error; - std::string status; - }; - parallel_tool m_parallel; - TTrack *parallel_source() const; - TTrack *parallel_target() const; - void parallel_update(); - void render_parallel_controls(bool const Scope = true); - void render_parallel_ui(); - void render_parallel_set_ui(); - void parallel_build(); - void parallel_apply(); - bool parallel_preview_visible() const; - void draw_parallel_preview() const; - - // scenery templates standing by the track, placed and checked by the editor - struct template_item - { - std::string file; - glm::dvec3 location{0.0}; // world space - double yaw{0.0}; - std::vector rest; // parameters after the rotation - bool described{false}; - std::string track; - glm::dvec2 tilt{0.0}; // rot.x and rot.z of a described template, degrees - std::map values; - }; - struct standing_template - { - scene::instance_handle instance{0}; - std::string file; // lower case, forward slashes - glm::dvec3 location{0.0}; - double yaw{0.0}; - std::vector values; - }; + using template_item = ::template_item; + using standing_template = ::standing_template; // includes of the templates whose file names the test accepts, with the location in p2..p4 and the rotation in p5 - std::vector standing_templates(std::function const &Accept) const; + std::vector standing_templates(std::function const &Accept) const override; struct switch_drive { standing_template standing; @@ -1938,160 +2343,25 @@ class editor_mode : public application_mode, private editor_track::observer void apply_switch_drive_choice(TTrack &Switch); void switch_drive_captured(TTrack const &Track); void switch_drives_committed(std::vector const &Tracks); - // places them in the active layer, as one step of the undo. returns: how many were placed - std::size_t place_templates(std::vector const &Items, std::string &Error); + // the drives of the switches deleted go with them, in the step of the undo history the deletion made last + void drives_deleted(std::vector const &Switches); + // the drives of all the switches of the scenery set at the tips of their blades, the ones the earlier versions of the editor set at the + // joint before them too: once, as the scenery is opened, with no step of the undo history; how many moved + std::size_t drives_to_blade_tips(); + bool m_drives_checked{false}; + scene::layer_handle output_layer(std::string const &Part) override; + // places them in the active layer or the given one, as one step of the undo. returns: how many were placed + std::size_t place_templates(std::vector const &Items, std::string &Error, scene::layer_handle Layer = scene::layer_handle{}) override; // moves them, as one step of the undo - std::size_t move_templates(std::vector> const &Moves); + std::size_t move_templates(std::vector> const &Moves) override; // takes them out, as one step of the undo - std::size_t remove_templates(std::vector const &Instances); - - // hectometre posts along the line, at every 100 m of the kilometrage - struct hekto_post - { - int hectometres{0}; - double chainage{0.0}; - glm::dvec3 position{0.0}; - double yaw{0.0}; - int standing{-1}; // post already there with this number - }; - struct hekto_found - { - standing_template post; - int hectometres{0}; - double chainage{0.0}; // where it stands along the line - double expected{0.0}; // where its number belongs - double error{0.0}; // m along the line - double distance{0.0}; // m from where it belongs, in the plan - bool wrong{false}; - bool duplicate{false}; // another post with the same number stands nearer to its place - bool outside{false}; // its number belongs past the ends of the line - }; - struct hekto_tool - { - bool open{false}; - bool expand{false}; - TTrack *track{nullptr}; // the line runs through it - glm::dvec3 point{0.0}; // where the kilometrage is given - double km{0.0}; - bool km_read{false}; // the kilometrage was read from the posts already standing - int growth{1}; // 1: grows along the line, -1: against - int side{1}; // 1: to the right of the growing kilometrage, -1: to the left, 0: alternately - double offset{3.0}; // m from the axis - int style{0}; // nowy, stary, zgnity, mixed - int turn{0}; // degrees added to the rotation - double reach{10000.0}; // m each way - double tolerance{5.0}; // m along the line, for the posts standing - bool dirty{true}; - std::size_t history{0}; - editor_track::route route; - std::vector samples; - double origin{0.0}; // chainage of the point - std::vector posts; - std::vector found; - std::string status; - std::string error; - }; - hekto_tool m_hekto; - void hekto_start(TTrack &Track, glm::dvec3 const &Point); - void hekto_update(); - void hekto_read_kilometrage(); - void hekto_place(); - void hekto_fix(); - void hekto_remove_duplicates(); - void render_hekto_ui(); - void draw_hekto_overlay() const; - - // fouling point markers (W17) where the tracks meeting at a switch are the spacing apart - struct fouling_point - { - TTrack *track{nullptr}; // the switch - glm::dvec3 position{0.0}; - double yaw{0.0}; - int standing{-1}; // marker already there - }; - struct fouling_found - { - standing_template marker; - int point{-1}; // the fouling point it belongs to - double error{0.0}; // m from where it belongs - double height_error{0.0}; // m over where it belongs - bool wrong{false}; - }; - struct fouling_tool - { - bool open{false}; - bool expand{false}; - int scope{1}; // 0: the selected switch, 1: around the camera, 2: the whole scenery - double reach{1500.0}; - double spacing{3.5}; // m between the axes - double tolerance{2.0}; // m, a marker farther than this from its point is wrong - int height_mode{1}; // 0: on the ground, 1: at the height given over the rail heads - double height{0.0}; // m over the rail heads - double height_tolerance{0.03}; // m, a marker standing higher or lower than this is wrong - int model{0}; // w17, w17_new, w17_old, w17_dwuteo - int turn{0}; - bool dirty{true}; - std::size_t history{0}; - TTrack *selected{nullptr}; - std::vector points; - std::vector found; - std::string status; - std::string error; - }; - fouling_tool m_fouling; - void fouling_update(); - // the markers put at the fouling points without one, or at the one only - void fouling_place(int const Only = -1); - // the markers standing wrong moved to their places, or the one found only - void fouling_fix(int const Only = -1); - // the fouling point without a marker or the marker standing wrong under the cursor: the index of the point or of the - // marker found, and whether it's a marker - std::optional> fouling_hit() const; - void render_fouling_ui(); - void draw_fouling_overlay() const; - - // a trainset put on the selected path, written to the scenery on save, or only to be driven - struct vehicle_tool - { - bool open{false}; - bool expand{false}; - std::shared_ptr bank; - char search[64]{}; - bool controllable_only{true}; - std::shared_ptr vehicle; - std::shared_ptr skin; - int driver{0}; // 0: a driver, to be driven; 1: nobody, parked - TTrack *track{nullptr}; - glm::dvec3 point{0.0}; - std::string placed; - std::string status; - bool leave{false}; // the editor gives way to the driving at the start of the next frame - struct consist_vehicle - { - std::shared_ptr vehicle; - std::shared_ptr skin; - bool turned{false}; - }; - std::vector consist; - int picked{-1}; // vehicle of the train whose buttons are shown - std::set minis; // skins whose side view was asked for - int facing{1}; - bool write{true}; - char timetable[64]{"none"}; - double velocity{0.0}; - struct trainset - { - std::string head; - std::string track; - std::vector lines; - }; - std::vector trainsets; - }; - vehicle_tool m_vehicle; - void vehicle_start(TTrack &Track, glm::dvec3 const &Point); - bool vehicle_place(); - void render_vehicle_ui(); - void draw_vehicle_marker() const; + std::size_t remove_templates(std::vector const &Instances) override; + bool standing_signals(std::vector &Signals, bool const Wait) override; + std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) override; + bool signal_heads(std::vector &Heads, bool const Wait) override; + std::size_t signal_reattach(std::vector const &Instances) override; + void select_signal(scene::instance_handle const Instance) override; + void render_signal_templates() override; struct signal_template { @@ -2106,6 +2376,8 @@ class editor_mode : public application_mode, private editor_track::observer int parameters{0}; bool plate{false}; bool linked{false}; + std::vector speeds; + int aspects{0}; // of a block signal, 3 or 4 }; struct signal_spot { @@ -2127,8 +2399,6 @@ class editor_mode : public application_mode, private editor_track::observer std::string read; TTrack *track{nullptr}; int list{0}; - signal_spot spot; - bool on_track{false}; }; struct signal_tool { @@ -2136,6 +2406,7 @@ class editor_mode : public application_mode, private editor_track::observer int revision{-1}; int kind{0}; std::array chosen{-1, -1, -1, -1, -1, -1, -1}; + bool armed{false}; // a click by a track places the chosen template char filter[64]{}; char name[64]{"A"}; char plate[64]{}; @@ -2152,22 +2423,34 @@ class editor_mode : public application_mode, private editor_track::observer int hover{-1}; scene::instance_handle selected{0}; bool moving{false}; + glm::dvec2 grab{0.0}; + signal_spot home; + bool home_valid{false}; signal_spot moved; bool moved_valid{false}; std::string status; }; signal_tool m_signal; bool signal_scan(bool Wait = true); + signal_template const *signal_current() const; signal_template const *signal_armed() const; signal_template const *signal_template_of(std::string const &File) const; bool signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const; void signal_refresh(bool const Force = false); + // the signals standing, read by the tracks as they are now, the templates as found so far + std::vector signal_standing_now() const; int signal_hit() const; void signal_press(track_intent const &Intent); void signal_release(); bool signal_place(signal_spot const &Spot, signal_template const &Template); void signal_move_update(); - void signal_move_finish(); + bool signal_standing_spot(signal_standing const &Standing, signal_spot &Spot) const; + bool signal_drop_at(signal_standing const &Standing, TTrack &Track, int const Path, glm::dvec3 const &Point, int const Side, signal_spot &Spot) const; + bool signal_move_to(signal_standing const &Standing, signal_spot const &Spot, char const *Read, char const *Unread); + bool signal_flip(scene::instance_handle const Instance); + bool signal_turn(scene::instance_handle const Instance); + bool signal_swap(scene::instance_handle const Instance, std::string const &File); + signal_standing const *signal_standing_of(scene::instance_handle const Instance) const; bool signal_delete(scene::instance_handle const Instance); void signal_detach_events(std::string const &Read, std::vector> &States); void signal_renamed(std::string const &Before, std::string const &After, EditorSnapshot &Snapshot); @@ -2179,6 +2462,22 @@ class editor_mode : public application_mode, private editor_track::observer void draw_signal_overlay() const; std::string signal_intent_label(signal_template const &Template, signal_spot const &Spot) const; std::string signal_variant(signal_template const &Template, signal_spot const &Spot) const; + // what a deleted track carried and no other one took, its read events and track circuits, for a track laid in its place + struct orphan_record + { + TTrack *track{nullptr}; // retired; the record waits while it stays so + std::vector> events; // the name, the list it was on + std::vector circuits; + // the tracks given the circuits: while one stays the circuits don't wait, once a step other than an undo removes one they + // are its own + std::vector given; + bool fresh{false}; // made by the step being taken + }; + std::vector m_orphans; + std::string m_orphan_note; // shown by the cursor after the step which made it + void orphans_register(std::vector const &Removed, std::vector const &Kept); + // Created: made by the step, their circuits the ones of the tracks they were made from; Changed: the others it changed + void orphans_take_over(std::vector const &Created, std::vector const &Changed); // catenary (sieć trakcyjna): the whole tool, its state and its workings are in application/editormodecatenary.h/.cpp; // here only the fixed hooks the rest of the editor calls, so changes of the tool rebuild only its own file @@ -2355,14 +2654,11 @@ class editor_mode : public application_mode, private editor_track::observer int wait{0}; bool quitting{false}; scripted_input input; + std::string badge; // the mouse or the keys drawn over the view, for the clips of the tutorial + glm::vec2 badge_at{0.0f}; // its bottom left corner, in the pixels of the window }; selftest m_selftest; - enum class selftest_result - { - unhandled, - handled, - pause - }; + using selftest_result = editor_selftest_result; selftest_result selftest_scene(std::string const &command, std::istringstream &words); selftest_result selftest_sleepers(std::string const &command, std::istringstream &words); selftest_result selftest_sweep(std::string const &command, std::istringstream &words); @@ -2436,11 +2732,6 @@ class editor_mode : public application_mode, private editor_track::observer 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}; @@ -2478,6 +2769,23 @@ class editor_mode : public application_mode, private editor_track::observer bool m_profiles_loaded{false}; void profile_library_load(); void profile_library_mark(std::vector Layers); + // the last step takes the kept grade lines back to the library given, as they were before it changed them + void profiles_to_history(std::vector Library); + // the kept grade lines back to the library given, the step changing them given up before it went to the history + void profiles_restore(std::vector Library); + // the tracks whose grade line was fitted, by name, with the shape they had then, kept as "//$f" comment lines: a track + // changed since, by hand or taken back, isn't fitted any more + struct fitted_mark + { + scene::layer_handle layer{null_handle}; + std::uint64_t shape{0}; + }; + std::map m_fitted; + bool m_fitted_loaded{false}; + void fitted_load(); + void fitted_set(std::vector const &Tracks); + bool grade_fitted(TTrack const &Track); + static std::uint64_t fitted_shape(TTrack const &Track); int profile_find(bool &Reversed) const; bool profile_restore(); bool profile_restore_parts(); @@ -2487,6 +2795,8 @@ class editor_mode : public application_mode, private editor_track::observer void profile_forget(); void profile_open_stored(std::size_t const Index); void profile_edited(); + // the radii of its neighbours were for the breaks of the grade it made, they follow the limits after it + void profile_remove_point(int const Index); void render_profile_body(); void render_profile_strip(); void render_profile_header(); @@ -2515,14 +2825,15 @@ class editor_mode : public application_mode, private editor_track::observer void profile_restore_ground(); void render_profile_earthworks(); void profile_check(); - // Terrain: the heightmap terrain follows the tracks changed - void profile_apply(bool const Terrain = false); + // Terrain: the heightmap terrain follows the tracks changed. Bridges: stretches of the route over a bridge or a viaduct, the + // paths over them cut off at their ends and set to the bridge environment + void profile_apply(bool const Terrain = false, std::vector const &Bridges = {}, double const Fewest = 0.0); // 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; @@ -2579,8 +2890,26 @@ class editor_mode : public application_mode, private editor_track::observer std::vector m_infra_buffer; // states of the objects before the change in progress std::vector> m_drive_buffer; bool m_infra_suspended{false}; + // the point objects which go with a path: the signals and the warning discs it reads, the SHP resonators, and the controls of the + // block sections, the signs and the posts by the path nearest to them + struct point_object + { + scene::instance_handle include{0}; + glm::dvec3 location{0.0}; + TTrack *reader{nullptr}; + TTrack *nearest{nullptr}; // of one no path reads + infra::category group{infra::category::other}; + }; + std::vector m_point_objects; + std::size_t m_point_objects_revision{static_cast(-1)}; + std::size_t m_point_objects_count{0}; + void infra_adopt(TTrack &Track); + int infra_bound_count() const; // by the infra tool, not the ones a change of their track took along + std::set> m_infra_carried; // the objects their tracks moved since the tool cleared it + std::string infra_carried_note(); // a line of the status telling how many, empty for none; clears them void infra_load(); void infra_store(); + void infra_forget(scene::basic_node const *Node); std::vector infra_scope() const; void infra_recognize(); void infra_bind_chosen(); @@ -2598,72 +2927,17 @@ class editor_mode : public application_mode, private editor_track::observer void infra_attach(EditorSnapshot &Snapshot); void render_infra_body(); - // speed limits of the paths checked against what their geometry allows - struct speed_state - { - bool open{false}; - int scope{0}; // 0: the line through the selected path, 1: the whole scenery - speed_check::options options; - std::vector> queue; - std::size_t next{0}; - std::vector results; - bool show_all{false}; - int limits{0}; // 0: all the paths, 1: only those with a speed limit set, 2: only those without - int selected{-1}; - std::size_t history{0}; // size of the undo history at the check - std::string status; - }; - speed_state m_speed; - void render_speed_body(); - void speed_start(); - void speed_step(); - void speed_focus(int const Index); - // with Branches the limits of the switch branches count too, otherwise they're only warnings - void speed_apply(std::vector const &Indices, bool const Branches); - std::vector speed_listed() const; - void draw_speed_overlay() const; + gauge_tool m_gauge{*this, *this, *this, *this}; + speed_tool m_speed{*this, *this, *this, *this}; + joints_tool m_joints{*this, *this, *this, *this}; + vehicle_tool m_vehicle{*this, *this}; + hekto_tool m_hekto{*this, *this, *this, *this, *this, *this}; + fouling_tool m_fouling{*this, *this, *this, *this, *this}; + parallel_tool m_parallel{*this, *this, *this, *this}; + srk_tool m_srk{*this, *this, *this, *this, *this}; + km_tool m_km{*this, *this, *this, *this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk, &m_km}; - // joints of the paths: ends which almost meet, steps, kinks, jumps of the cant and of the grade - struct joints_state - { - bool open{false}; - int scope{0}; // 0: around the camera, 1: the whole scenery - double reach{2000.0}; - double gap{1.0}; // m, free ends closer than this to another end - double step{0.005}; // m - double angle{0.2}; // deg - double cant{5.0}; // mm - double grade{10.0}; // per mille - double model{0.5}; // m, the track over or under the terrain model (NMT) - bool free_ends{false}; - bool model_check{true}; - std::vector queue; - std::size_t next{0}; - // the places of the tracks compared with the terrain model, in runs of one path each, once the model under them is in - std::vector places; - std::vector> runs; - std::size_t run_next{0}; - bool model_waiting{false}; - bool model_asked{false}; - double model_polled{0.0}; - bool model_unplaced{false}; - std::vector issues; - int filter{-1}; - int selected{-1}; - std::size_t history{0}; - std::string status; - }; - joints_state m_joints; - void joints_start(); - void joints_step(); - void joints_check(TTrack &Track); - // the paths lying off the terrain model by more than the threshold. returns: false while the model under them is coming - bool joints_model(); - void joints_focus(int const Index); - bool joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed); - void joints_fix(std::vector const &Indices); - void render_joints_body(); - void draw_joints_overlay() const; void infra_rebase(); void render_infra_search(); void render_infra_candidates(); @@ -2671,6 +2945,7 @@ class editor_mode : public application_mode, private editor_track::observer void draw_infra_overlay() const; bool m_route_dragging{false}; + alignment::design m_route_drag_from; float m_track_snap_radius{1.0f}; bool m_track_align_tangent{true}; bool m_track_drag_connected{true}; diff --git a/application/editormodeconnectioncurve.cpp b/application/editormodeconnectioncurve.cpp index 20e81fdb..0aae4951 100644 --- a/application/editormodeconnectioncurve.cpp +++ b/application/editormodeconnectioncurve.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editormodeshared.h" +#include "application/editormodetrackshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorSwitchStore.hpp" @@ -36,6 +37,37 @@ namespace // vertical curve: the ramps break the grade line without one double constexpr kRampShare{0.9}; double constexpr kOtherReach{10.0}; // m, from the release to the other track +double constexpr kStraightReach{track_mode::kCrossoverStraights}; + +// the first curved track a straight track leads to along its straight, either way, within the reach +TTrack *curve_ahead(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Reach) +{ + TTrack *found{nullptr}; + auto nearest{Reach}; + for (auto const throughend : {true, false}) + { + TTrack const *previous{&Track}; + TTrack *current{throughend ? Track.trNext : Track.trPrev}; + double distance{0.0}; + for (int count = 0; current != nullptr && count < 1000 && distance < nearest; ++count) + { + if (current->eType != tt_Normal || current->m_editorremoved || current->m_paths.empty()) + break; + if (false == editor_track::is_straight(*current, Tolerance)) + { + found = current; + nearest = distance; + break; + } + auto const &path{current->m_paths.front()}; + distance += plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]); + auto *next{current->trPrev == previous ? current->trNext : current->trPrev}; + previous = current; + current = next; + } + } + return found; +} } // namespace @@ -111,19 +143,55 @@ struct editor_mode::curve_mapping }; // the curve the track is a part of, as a frame of samples, and its tracks -bool editor_mode::curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks) +bool editor_mode::curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks, double const Beyond) { + auto *base{&Track}; + if (Beyond > 0.0 && Track.eType == tt_Normal && editor_track::is_straight(Track, Tolerance)) + base = curve_ahead(Track, Tolerance, Beyond); + if (base == nullptr) + return false; editor_track::curve curve; editor_track::chain chain; std::string error; - if (false == editor_track::find_curve(Track, Tolerance, Gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + if (false == editor_track::find_curve(*base, Tolerance, Gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) return false; for (auto const &part : editor_track::chain_parts(chain)) - if (std::find(part.tracks.begin(), part.tracks.end(), &Track) != part.tracks.end()) + if (std::find(part.tracks.begin(), part.tracks.end(), base) != part.tracks.end()) { chain = part; break; } + if (Beyond > 0.0) + { + // the straights past both ends, as far as the reach, taken along the curve: the one before it from its far end + auto const taken = [&](editor_track::straight_run const &Run) { + std::size_t count{0}; + for (double reached{0.0}; count < Run.tracks.size() && reached < Beyond; ++count) + { + auto const &path{Run.tracks[count]->m_paths.front()}; + reached += plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]); + } + return count; + }; + auto const before{editor_track::straight_beyond(chain, false, Tolerance)}; + auto const after{editor_track::straight_beyond(chain, true, Tolerance)}; + editor_track::chain extended; + for (auto i{taken(before)}; i-- > 0;) + { + extended.tracks.push_back(before.tracks[i]); + extended.forward.push_back(false == before.outward[i]); + } + extended.tracks.insert(extended.tracks.end(), chain.tracks.begin(), chain.tracks.end()); + extended.forward.insert(extended.forward.end(), chain.forward.begin(), chain.forward.end()); + for (std::size_t i = 0, count = taken(after); i < count; ++i) + { + extended.tracks.push_back(after.tracks[i]); + extended.forward.push_back(after.outward[i]); + } + chain = std::move(extended); + } + if (std::find(chain.tracks.begin(), chain.tracks.end(), &Track) == chain.tracks.end()) + return false; Frame = sample_chain(chain); Tracks = chain.tracks; return Frame.size() >= 2; @@ -166,7 +234,12 @@ void editor_mode::cut_curve(std::vector &Candidates, std::vector From + 0.01 && where < To - 0.01) { editor_track::retire(*candidate); - Removed.push_back(candidate); + // a piece split off in this step goes as if it had never been made: kept among the ones made and the ones taken out + // too, an undo would retire it and then bring it back + if (auto const made{std::find(Created.begin(), Created.end(), candidate)}; made != Created.end()) + Created.erase(made); + else + Removed.push_back(candidate); } } } @@ -181,12 +254,33 @@ void editor_mode::insert_curved_connection() }; if (tool.connection != static_cast(connection::kind::crossover)) return fail(STR("Along a curve only a crossover can be laid")); + // the curve pressed on with the straights past its ends: from a transition the crossover goes on onto the straight + auto pressedframe{tool.frame}; + auto pressedtracks{tool.frame_tracks}; + auto *pressed{tool.frame_tracks.front()}; + { + auto nearest{std::numeric_limits::max()}; + for (auto *track : tool.frame_tracks) + if (false == track->m_editorremoved) + if (auto const distance{plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, tool.point)), tool.point)}; distance < nearest) + { + nearest = distance; + pressed = track; + } + std::vector frame; + std::vector tracks; + if (curve_of(*pressed, m_straights.tolerance, m_route.design.norms.gauge, frame, tracks, kStraightReach)) + { + pressedframe = std::move(frame); + pressedtracks = std::move(tracks); + } + } // the other track: one beside this curve near the release, running along with it TTrack *other{nullptr}; auto best{kOtherReach}; for (auto *track : simulation::Paths.sequence()) { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || std::find(tool.frame_tracks.begin(), tool.frame_tracks.end(), track) != tool.frame_tracks.end()) + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || std::find(pressedtracks.begin(), pressedtracks.end(), track) != pressedtracks.end()) continue; auto const distance{plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, tool.mouse)), tool.mouse)}; if (distance < best) @@ -199,12 +293,12 @@ void editor_mode::insert_curved_connection() return fail(STR("Drag onto the other track")); std::vector otherframe; std::vector othertracks; - if (false == curve_of(*other, m_straights.tolerance, m_route.design.norms.gauge, otherframe, othertracks)) + if (false == curve_of(*other, m_straights.tolerance, m_route.design.norms.gauge, otherframe, othertracks, kStraightReach)) return fail(STR("The other track isn't a curve running along this one")); // the inner track is the frame: the plane of its cant carries the crossover, the outer track goes up onto it - auto innerframe{tool.frame}; - auto innertracks{tool.frame_tracks}; - auto station{tool.along}; + auto innerframe{pressedframe}; + auto innertracks{pressedtracks}; + auto station{nearest_station(innerframe, tool.point)}; auto const at{sample_at(innerframe, station)}; auto const across{sample_at(otherframe, nearest_station(otherframe, at.position))}; glm::dvec2 const left{-at.tangent.y, at.tangent.x}; @@ -271,7 +365,9 @@ void editor_mode::insert_curved_connection() std::vector> states; std::vector created; std::vector removed; - auto *style{tool.frame_tracks.front()}; + auto *style{pressed}; + // the kept grade lines before the outer track went up, the design of which goes up with it + std::optional> library; auto const undo_all = [&]() { for (auto *track : created) editor_track::retire(*track); @@ -285,6 +381,8 @@ void editor_mode::insert_curved_connection() editor_track::revive(*track); editor_track::commit({track}); } + if (library.has_value()) + profiles_restore(std::move(*library)); straight_refresh(); }; @@ -298,7 +396,7 @@ void editor_mode::insert_curved_connection() low = std::min(low, path.points[end].z); high = std::max(high, path.points[end].z); } - auto *outertrack{swapped ? tool.frame_tracks.front() : other}; + auto *outertrack{swapped ? pressed : other}; for (auto const &entry : plan.switches) WriteLog(format("Editor: curved crossover - planned switch on %d: %.3f %.3f -> %.3f %.3f; extent %.3f..%.3f", entry.line, entry.paths.front().points[segment_data::point::start].x, entry.paths.front().points[segment_data::point::start].z, entry.paths.front().points[segment_data::point::end].x, entry.paths.front().points[segment_data::point::end].z, low, high), logtype::generic); std::size_t raised{0}; @@ -333,7 +431,7 @@ void editor_mode::insert_curved_connection() // the top of the ramps where the switch on the outer track ends: no short paths left between the two if (c1 > c2) std::swap(c1, c2); - auto const ramp{std::abs(lift) / (kRampShare * m_profile.line.norms.curve_threshold / 1000.0)}; + auto const ramp{std::abs(lift) / (std::max(0.5, kRampShare * m_profile.line.norms.curve_threshold) / 1000.0)}; flatfrom = editor_track::sampled_position(samples, c1); flatto = editor_track::sampled_position(samples, c2); ramplength = ramp; @@ -360,6 +458,8 @@ void editor_mode::insert_curved_connection() // the design of the outer track before, kept or read from the track, to go up with it auto *first{route.spans.front().track}; auto *last{route.spans.back().track}; + profile_library_load(); + library = m_profiles; profile_take(route, first, last); auto const before{m_profile.line}; std::vector> raisestates; @@ -431,7 +531,7 @@ void editor_mode::insert_curved_connection() closest = track; } } - if (closest == nullptr || false == curve_of(*closest, m_straights.tolerance, m_route.design.norms.gauge, outerframe, outertracks)) + if (closest == nullptr || false == curve_of(*closest, m_straights.tolerance, m_route.design.norms.gauge, outerframe, outertracks, kStraightReach)) { undo_all(); return fail(STR("The outer track can't be followed after it went up")); @@ -439,7 +539,7 @@ void editor_mode::insert_curved_connection() } // the inner curve again: the outer track may be split where the ramps end if (false == swapped) - innertracks = tool.frame_tracks; + innertracks = pressedtracks; std::vector switches; for (auto const &entry : plan.switches) @@ -532,6 +632,9 @@ void editor_mode::insert_curved_connection() switch_store::put(entry, entry.layer); } push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + // the design of the outer track goes back down with it + if (library.has_value()) + profiles_to_history(std::move(*library)); straight_refresh(); first_step_done(first_step::turnout); m_node = switches.front(); @@ -545,7 +648,7 @@ void editor_mode::insert_curved_connection() } tool.status = format(STR_C("Crossover laid along the curve, switches %s, the tracks %.2f m apart"), names.c_str(), std::abs(offset)); if (std::abs(lift) > 0.002) - tool.status += format(STR_C("\nThe outer track went up %.3f m onto the plane of the cant of the inner one, with ramps of %.1f per mille"), std::abs(lift), kRampShare * m_profile.line.norms.curve_threshold); + tool.status += format(STR_C("\nThe outer track went up %.3f m onto the plane of the cant of the inner one, with ramps of %.1f per mille"), std::abs(lift), std::max(0.5, kRampShare * m_profile.line.norms.curve_threshold)); tool.status += "\n" + connection_readout(plan); ui()->set_status(tool.status, false); } @@ -723,13 +826,16 @@ bool editor_mode::slide_curved_connection(TTrack &Switch, double const Distance) std::vector removed; TTrack *first{nullptr}; TTrack *second{nullptr}; + // the design of the outer track comes down with the lift taken back, an undo puts both back + profile_library_load(); + auto library{m_profiles}; if (false == take_out_crossover(*crossover, states, created, removed, first, second)) return false; // the new places of the press and of the release, along the two curves, or the two straights std::vector firstframe, secondframe; std::vector firsttracks, secondtracks; glm::dvec3 press, release; - if (curve_of(*first, m_straights.tolerance, m_route.design.norms.gauge, firstframe, firsttracks) && curve_of(*second, m_straights.tolerance, m_route.design.norms.gauge, secondframe, secondtracks)) + if (curve_of(*first, m_straights.tolerance, m_route.design.norms.gauge, firstframe, firsttracks, kStraightReach) && curve_of(*second, m_straights.tolerance, m_route.design.norms.gauge, secondframe, secondtracks, kStraightReach)) { auto const towards{nearest_station(firstframe, firstend) >= nearest_station(firstframe, firststart) ? 1.0 : -1.0}; press = sample_at(firstframe, nearest_station(firstframe, firststart) + towards * Distance).position; @@ -747,6 +853,8 @@ bool editor_mode::slide_curved_connection(TTrack &Switch, double const Distance) switch_store::follow(); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); records_to_history(); + if (std::abs(crossover->record.lift) > 0.002) + profiles_to_history(std::move(library)); // laid again if (false == lay_crossover(shape, press, release, 3.0, names)) { diff --git a/application/editormodedigitize.cpp b/application/editormodedigitize.cpp index 9eb5f83e..75e76f81 100644 --- a/application/editormodedigitize.cpp +++ b/application/editormodedigitize.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "editor/editorAlignmentStore.hpp" #include "editor/editorConnections.hpp" @@ -328,7 +329,7 @@ void editor_mode::digitize_crossovers(digitize_report &Report, bool const Write) if (spacing < kSpacingLeast || spacing > kSpacingMost) continue; ++Report.crossovers; - auto const name = [](TTrack const *Track) { return Track->name().empty() ? STR("(no name)") : Track->name(); }; + auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const note = [&](std::string const &Why) { Report.notes.push_back(name(first) + " + " + name(second) + ": " + Why); }; if (false == switch_store::connections(*first).empty()) { @@ -388,9 +389,9 @@ std::string editor_mode::digitize_scenery(bool const Write) m_infra.scope = 3; infra_recognize(); report.found = static_cast(m_infra.candidates.size()); - auto const before{m_bindings.size()}; + auto const before{infra_bound_count()}; infra_bind_chosen(); - report.bound = static_cast(m_bindings.size() - before); + report.bound = infra_bound_count() - before; m_infra.scope = scope; } auto text{format(STR_C("Switches: %d, kept already %d, of the catalogue %d, measured %d, parts of slips %d, odd %d"), report.switches, report.known, report.designed, report.measured, report.slips, report.odd)}; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 84af9df3..42ecf58c 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -8,7 +8,8 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" +#include "application/editormodegauge.h" +#include "editor/editorTrack.hpp" #include "application/editoruilayer.h" #include "editor/editorFormat.hpp" #include "editor/editorGauge.hpp" @@ -326,7 +327,7 @@ template void visit_triangles(TSubModel *Submodel, glm::dmat4 } // the triangle enters the gauge when any point of a grid spread over it does; the deepest such point goes to the hit -void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, editor_mode::gauge_hit &Hit) +void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, gauge_tool::gauge_hit &Hit) { auto const middle{(A + B + C) / 3.0}; if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)}))) @@ -353,24 +354,24 @@ void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const } // the deepest place of the model in the gauge, added to the hits when it enters it -void scan_model(corridor const &Space, TAnimModel &Instance, std::vector &Hits) +void scan_model(corridor const &Space, TAnimModel &Instance, std::vector &Hits) { if (false == Instance.visible() || Instance.Model() == nullptr || Instance.Model()->GetSMRoot() == nullptr) return; if (false == Space.nearby(Instance.location(), Instance.radius())) return; - editor_mode::gauge_hit hit{Instance.name(), Instance.location()}; + gauge_tool::gauge_hit hit{Instance.name(), Instance.location()}; visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { scan_triangle(Space, A, B, C, hit); }); if (hit.depth > 0.0) Hits.push_back(std::move(hit)); } // the same for the copies of a model laid along a curve -void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) +void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) { if (Sweep.m_editorremoved || false == Space.nearby(Sweep.location(), Sweep.radius())) return; - editor_mode::gauge_hit hit{Sweep.name(), Sweep.location()}; + gauge_tool::gauge_hit hit{Sweep.name(), Sweep.location()}; for (auto const &shape : Sweep.create_shapes()) { auto const &vertices{shape.data().vertices}; @@ -381,13 +382,13 @@ void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) +void sort_hits(std::vector &Hits) { std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; }); } // the models entering the gauge, the deepest first -void scan_models(corridor const &Space, std::vector &Hits) +void scan_models(corridor const &Space, std::vector &Hits) { for (auto *instance : simulation::Instances.sequence()) if (instance != nullptr) @@ -550,7 +551,7 @@ struct gauge_scan std::chrono::steady_clock::time_point started; }; -void editor_mode::gauge_load() +void gauge_tool::gauge_load() { if (m_gauge.profiles.empty()) m_gauge.profiles = gauge::load_profiles(gauge_file); @@ -568,18 +569,18 @@ void editor_mode::gauge_load() } } -gauge::choice editor_mode::gauge_choice() const +gauge::choice gauge_tool::gauge_choice() const { auto const pick{[&](int const Index) { return Index >= 0 ? &m_gauge.profiles[Index] : nullptr; }}; return {pick(m_gauge.profile), pick(m_gauge.road)}; } -std::vector const &editor_mode::gauge_hits() const +std::vector const &gauge_tool::gauge_hits() const { return m_gauge.map.scanned ? m_gauge.map.hits : m_gauge.line.hits; } -void editor_mode::update_gauge() +void gauge_tool::update() { if (m_gauge.scan) step_gauge_map(); @@ -590,9 +591,9 @@ void editor_mode::update_gauge() m_gauge.published = false; return; } - auto *track{m_gauge.enabled ? selected_track() : nullptr}; + auto *track{m_gauge.enabled ? m_selection.selected_track() : nullptr}; // the geometry is rescanned once the edit ends - if (ImGui::IsMouseDown(0) || m_track_gizmo_using || m_dragging) + if (ImGui::IsMouseDown(0) || m_design.track_editing()) m_gauge.pending = true; else if (track == nullptr) { @@ -603,19 +604,19 @@ void editor_mode::update_gauge() m_gauge.published = false; } } - else if (track != m_gauge.track || m_history.size() != m_gauge.history || m_gauge.pending) + else if (track != m_gauge.track || m_history.history_revision() != m_gauge.history || m_gauge.pending) { m_gauge.track = track; - m_gauge.history = m_history.size(); + m_gauge.history = m_history.history_revision(); m_gauge.pending = false; scan_gauge(*track); m_gauge.published = false; } - if (false == m_gauge.published || glm::distance(Camera.Pos, EditorOverlay.origin) > gauge_origin_range) + if (false == m_gauge.published || glm::distance(m_view.camera_position(), EditorOverlay.origin) > gauge_origin_range) gauge_publish(); } -void editor_mode::scan_gauge(TTrack &Track) +void gauge_tool::scan_gauge(TTrack &Track) { gauge_load(); m_gauge.line = {}; @@ -631,7 +632,7 @@ void editor_mode::scan_gauge(TTrack &Track) m_gauge.current = -1; } -void editor_mode::scan_gauge_map() +void gauge_tool::scan_gauge_map() { gauge_load(); auto scan{std::make_shared()}; @@ -644,7 +645,7 @@ void editor_mode::scan_gauge_map() scan->space.emplace(scan->lines); m_gauge.map = {}; m_gauge.map.scanned = true; - m_gauge.map.history = m_history.size(); + m_gauge.map.history = m_history.history_revision(); auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }}; m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road); m_gauge.current = -1; @@ -652,7 +653,7 @@ void editor_mode::scan_gauge_map() m_gauge.scan = std::move(scan); } -void editor_mode::step_gauge_map() +void gauge_tool::step_gauge_map() { auto &scan{*m_gauge.scan}; auto const &instances{simulation::Instances.sequence()}; @@ -675,7 +676,7 @@ void editor_mode::step_gauge_map() finish_gauge_map(); } -void editor_mode::finish_gauge_map() +void gauge_tool::finish_gauge_map() { auto const scan{std::move(m_gauge.scan)}; auto &hits{m_gauge.map.hits}; @@ -690,11 +691,11 @@ void editor_mode::finish_gauge_map() gauge_focus(0); } -void editor_mode::gauge_publish() +void gauge_tool::gauge_publish() { m_gauge.published = true; EditorOverlay.clear(); - EditorOverlay.origin = Camera.Pos; + EditorOverlay.origin = m_view.camera_position(); auto const batch{[](glm::vec4 const &Color, unsigned int const Type, std::vector const &Points) { gfx::editor_overlay::batch result{Color, Type}; result.points.reserve(Points.size()); @@ -717,7 +718,7 @@ void editor_mode::gauge_publish() batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.spot.edges)}; } -void editor_mode::gauge_focus(int const Index) +void gauge_tool::gauge_focus(int const Index) { auto const &hits{gauge_hits()}; if (hits.empty()) @@ -729,12 +730,12 @@ void editor_mode::gauge_focus(int const Index) // along the track, tangent to the gauge at the height of the place, from the end nearer to the camera glm::dvec3 look{hit.direction.x, 0.0, hit.direction.y}; look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); - if (glm::dot(Camera.Pos - hit.point, look) > 0.0) + if (glm::dot(m_view.camera_position() - hit.point, look) > 0.0) look = -look; - fly_to(hit.point, look, gauge_view_distance); + m_view.fly_to(hit.point, look, gauge_view_distance); } -void editor_mode::gauge_spot(gauge_hit const &Hit) +void gauge_tool::gauge_spot(gauge_hit const &Hit) { m_gauge.spot = {}; auto const &paths{simulation::Paths.sequence()}; @@ -751,7 +752,7 @@ void editor_mode::gauge_spot(gauge_hit const &Hit) build_tunnel(*line, m_gauge.spot.surface, m_gauge.spot.edges); } -void editor_mode::render_gauge_window() +void gauge_tool::render_windows() { if (false == m_gauge.open) return; @@ -798,7 +799,7 @@ void editor_mode::render_gauge_window() m_gauge.current = -1; m_gauge.published = false; } - else if (m_gauge.map.history != m_history.size()) + else if (m_gauge.map.history != m_history.history_revision()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the scan (%s)"), m_gauge.map.profile.c_str()); } render_gauge_hits(); @@ -809,7 +810,7 @@ void editor_mode::render_gauge_window() ImGui::End(); } -void editor_mode::render_gauge_hits() +void gauge_tool::render_gauge_hits() { auto const &hits{gauge_hits()}; if (false == m_gauge.map.scanned && (false == m_gauge.enabled || m_gauge.track == nullptr)) @@ -841,7 +842,7 @@ void editor_mode::render_gauge_hits() ImGui::EndChild(); } -bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const Road) +bool gauge_tool::render_gauge_choice(char const *Label, int &Index, bool const Road) { bool changed{false}; ImGui::PushID(Label); @@ -863,7 +864,7 @@ bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const return changed; } -bool editor_mode::render_gauge_outline(int const Index) +bool gauge_tool::render_gauge_outline(int const Index) { if (Index < 0) return false; @@ -884,3 +885,23 @@ bool editor_mode::render_gauge_outline(int const Index) ImGui::TreePop(); return edited; } + +editor_selftest_result gauge_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "gauge") + { + std::string name; + Words >> name; + if (auto *track{simulation::Paths.find(name)}) + { + scan_gauge(*track); + WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast(m_gauge.line.hits.size()))); + for (auto const &hit : m_gauge.line.hits) + WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z)); + } + else + WriteLog("SELFTEST gauge " + name + ": no such track"); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodegauge.h b/application/editormodegauge.h new file mode 100644 index 00000000..0f0507bb --- /dev/null +++ b/application/editormodegauge.h @@ -0,0 +1,91 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editortool.h" +#include "editor/editorGauge.hpp" + +#include +#include +#include + +class gauge_tool final : public editor_tool +{ + public: + // model entering the structure gauge, at its deepest place + struct gauge_hit + { + std::string model; + glm::dvec3 point{0.0}; + double depth{0.0}; // m + TTrack const *track{nullptr}; // nearest one, may be gone by now + int path{0}; + glm::dvec2 direction{0.0, 1.0}; // of the track there, in the plan + }; + gauge_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_track_design &Design) : m_selection{Selection}, m_view{View}, m_history{History}, m_design{Design} {} + void update() override; + void render_windows() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + bool is_open() const { return m_gauge.open; } + void toggle() { m_gauge.open = !m_gauge.open; } + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_track_design &m_design; + // structure gauge along the line of the selected path, shown in the 3d view + struct gauge_state + { + bool open{false}; // window + bool enabled{true}; // tunnel along the selected path + std::vector profiles; + int profile{0}; // railway + int road{-1}; + TTrack const *track{nullptr}; // scanned + std::size_t history{0}; + bool pending{false}; // rescan once the edit ends + struct + { + std::vector surface; // triangles of the tunnel + std::vector edges; // lines + std::vector hits; + } line; // along the selected path + struct + { + bool scanned{false}; + std::size_t history{0}; + std::string profile; + std::vector hits; + } map; // whole scenery + struct + { + std::vector surface; + std::vector edges; + } spot; // tunnel around the hit the camera went to + int current{-1}; // hit the camera went to + bool published{false}; // overlay up to date + std::shared_ptr scan; // of the whole scenery, done in slices over the frames + } m_gauge; + void gauge_load(); + // the chosen railway and road outlines + gauge::choice gauge_choice() const; + std::vector const &gauge_hits() const; + void scan_gauge(TTrack &Track); + void scan_gauge_map(); + void step_gauge_map(); + void finish_gauge_map(); + void gauge_publish(); + void gauge_focus(int Index); + void gauge_spot(gauge_hit const &Hit); + void render_gauge_hits(); + bool render_gauge_choice(char const *Label, int &Index, bool Road); + bool render_gauge_outline(int Index); +}; diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 1840ffe7..abc9f30a 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -49,6 +49,11 @@ using geometry::plan_distance; using geometry::plan_of; std::string const kBindingMark{"//$b"}; +// the templates of the signalling which go with a path without a signal template of their own: the SHP resonator, read by its +// event, and the control of a block section, by the path nearest to it +std::string const kResonatorFile{"rezonator.inc"}; +std::string const kControlFile{"sbl_odstep.inc"}; +double constexpr kAdoptReach{12.0}; // m, of a point object from the axis of its path template bool contains(std::vector const &Items, Type_ const &Item) @@ -97,6 +102,11 @@ class live_nodes { return Node != nullptr && m_nodes.count(Node) > 0; } + void forget(scene::basic_node const *Node) + { + m_nodes.erase(Node); + m_valid = false; + } private: std::unordered_set m_nodes; @@ -129,13 +139,6 @@ struct include_place std::string category; // of the description of the template }; -bool numeric(std::string const &Text, double &Value) -{ - char *end{nullptr}; - Value = std::strtod(Text.c_str(), &end); - return false == Text.empty() && *end == '\0'; -} - bool include_place_of(scene::instance_handle const Instance, include_place &Place) { if (false == scene::Layers.tracked(Instance)) @@ -292,6 +295,7 @@ void move_traction(TTraction *Traction, std::array const &Points) Traction->location(glm::mix(Points[1], Points[0], 0.5)); Traction->m_area.radius = -1.f; simulation::Region->insert_and_register(Traction); + GfxRenderer->Erase(Traction->m_geometry); Traction->m_geometry = gfx::geometry_handle{}; auto §ion{simulation::Region->section(Traction->location())}; if (section.m_geometrycreated) @@ -777,6 +781,17 @@ void editor_mode::infra_load() logtype::generic); } +void editor_mode::infra_forget(scene::basic_node const *Node) +{ + Live.forget(Node); + auto const held = [Node](infra::binding const &Binding) { return Binding.type != infra::kind::include && Binding.node == Node; }; + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), held), m_bindings.end()); + m_infra_buffer.erase(std::remove_if(m_infra_buffer.begin(), m_infra_buffer.end(), [&](infra::object_state const &State) { return held(State.saved); }), m_infra_buffer.end()); + for (auto &candidate : m_infra.candidates) + if (held(candidate.binding)) + candidate.binding.node = nullptr; +} + void editor_mode::infra_store() { if (false == m_bindings_loaded || scene::Layers.empty()) @@ -793,7 +808,7 @@ void editor_mode::infra_store() { std::vector points; std::optional yaw; - if (binding.anchors.empty() || false == object_points(binding, points, yaw)) + if (binding.adopted || binding.anchors.empty() || false == object_points(binding, points, yaw)) continue; auto layer{scene::Layers.resolve(binding.anchors.front().at.track->layer())}; if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) @@ -860,6 +875,18 @@ infra::binding *editor_mode::infra_find(infra::binding const &Binding) return nullptr; } +std::string editor_mode::infra_carried_note() +{ + auto const count{m_infra_carried.size()}; + m_infra_carried.clear(); + return count > 0 ? format(STR_P("\n%zu object by the tracks moved sideways with them", "\n%zu objects by the tracks moved sideways with them", count), count) : std::string{}; +} + +int editor_mode::infra_bound_count() const +{ + return static_cast(std::count_if(m_bindings.begin(), m_bindings.end(), [](infra::binding const &Binding) { return false == Binding.adopted; })); +} + void editor_mode::infra_recognize() { auto &state{m_infra}; @@ -967,7 +994,8 @@ void editor_mode::infra_recognize() Binding.label = object_label(Binding); candidate.offset = Binding.anchors.front().offset; candidate.reason = Reason; - candidate.bound = (infra_find(Binding) != nullptr); + auto const *existing{infra_find(Binding)}; + candidate.bound = (existing != nullptr && false == existing->adopted); candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other && Binding.group != infra::category::environment); candidate.binding = std::move(Binding); state.read += candidate.reader != nullptr ? 1 : 0; @@ -1143,6 +1171,133 @@ void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh m_infra_buffer.push_back(infra_state_of(Binding)); } +void editor_mode::infra_adopt(TTrack &Track) +{ + auto const revision{history_revision()}; + if (revision != m_point_objects_revision || scene::Layers.instance_count() != m_point_objects_count) + { + m_point_objects_revision = revision; + m_point_objects_count = scene::Layers.instance_count(); + m_point_objects.clear(); + std::unordered_map readers; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + if (false == event.first.empty()) + readers.emplace(ToLower(event.first), track); + static auto const rules{infra::default_rules()}; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (false == include_place_of(handle, place) || place.values.empty() || false == scene::Layers.removable(handle)) + continue; + point_object object; + object.include = handle; + object.location = include_location(place); + auto const file{ToLower(place.file)}; + auto const *signal{signal_template_of(place.file)}; + std::string read; + if (signal != nullptr) + { + object.group = infra::category::signal; + read = signal->read.empty() ? std::string{} : editor_includes::substitute(signal->read, place.values); + } + else if (file == kResonatorFile) + { + object.group = infra::category::equipment; + read = place.values.front() + "_shp"; + } + else if (file == kControlFile) + object.group = infra::category::logic; + else + { + auto const named{infra::category_named(place.category)}; + object.group = named.has_value() ? *named : infra::classify(place.file + ' ' + place.description, rules); + if (object.group != infra::category::signal && object.group != infra::category::sign) + continue; + } + if (false == read.empty()) + if (auto const reader{readers.find(ToLower(read))}; reader != readers.end()) + object.reader = reader->second; + m_point_objects.push_back(std::move(object)); + } + // the path nearest to each one no path reads, the objects by cells of the plan + auto constexpr cell{200.0}; + auto const key = [&](double const Value) { return static_cast(std::floor(Value / cell)); }; + std::map, std::vector> cells; + for (std::size_t i = 0; i < m_point_objects.size(); ++i) + if (m_point_objects[i].reader == nullptr) + cells[{key(m_point_objects[i].location.x), key(m_point_objects[i].location.z)}].push_back(i); + std::vector nearest(m_point_objects.size(), kAdoptReach); + for (auto *track : simulation::Paths.sequence()) + { + if (cells.empty() || track == nullptr || track->m_editorremoved || false == editor_track::is_supported(*track)) + continue; + auto const area{area_of(*track)}; + auto const reach{area.radius + kAdoptReach}; + for (auto x = key(area.centre.x - reach); x <= key(area.centre.x + reach); ++x) + for (auto z = key(area.centre.z - reach); z <= key(area.centre.z + reach); ++z) + { + auto const found{cells.find({x, z})}; + if (found == cells.end()) + continue; + for (auto const i : found->second) + { + auto &object{m_point_objects[i]}; + if (plan_distance(object.location, area.centre) > area.radius + nearest[i]) + continue; + infra::station station; + bool interior; + auto const distance{infra::project(*track, object.location, station, interior)}; + if (distance < nearest[i] && beside(station, object.location, interior)) + { + nearest[i] = distance; + object.nearest = track; + } + } + } + } + } + auto const area{area_of(Track)}; + for (auto const &object : m_point_objects) + { + if (plan_distance(object.location, area.centre) > area.radius + kAdoptReach) + continue; + infra::binding binding; + binding.type = infra::kind::include; + binding.include = object.include; + if (infra_find(binding) != nullptr) + continue; + // of the line of the track: read by it or by a path of its run, else nearest to it; not one of the track alongside + if (object.reader != nullptr && object.reader != &Track) + { + auto const spans{editor_track::run_route(*object.reader, 250.0).spans}; + if (std::none_of(spans.begin(), spans.end(), [&](auto const &Span) { return Span.track == &Track; })) + continue; + } + if (object.reader == nullptr && object.nearest != &Track) + continue; + infra::station station; + bool interior; + if (infra::project(Track, object.location, station, interior) > kAdoptReach || false == beside(station, object.location, interior)) + continue; + std::vector points; + std::optional yaw; + if (false == object_points(binding, points, yaw) || points.empty()) + continue; + binding.group = object.group; + binding.anchors.push_back(infra::make_anchor(station, points.front())); + binding.turns = yaw.has_value() && object.group != infra::category::logic; + if (binding.turns) + binding.yaw = turn_of(binding, *yaw); + binding.label = object_label(binding); + binding.adopted = true; + binding.measured = points.front(); + m_bindings.push_back(std::move(binding)); + } +} + void editor_mode::track_captured(TTrack const &Track) { sweeps_captured(Track); @@ -1150,6 +1305,8 @@ void editor_mode::track_captured(TTrack const &Track) if (m_infra_suspended || false == m_infra.follow) return; infra_load(); + // the signals and the like of the track follow it, staying where they stand along it + infra_adopt(const_cast(Track)); if (m_bindings.empty()) return; Live.refresh(); @@ -1178,12 +1335,16 @@ void editor_mode::track_captured(TTrack const &Track) if (binding.turns && yaw.has_value() && binding.anchors.front().at.track == &Track) binding.yaw = turn_of(binding, *yaw); } + if (binding.adopted && false == points.empty()) + binding.measured = points.front(); infra_buffer(binding, true); } } void editor_mode::tracks_committed(std::vector const &Tracks) { + nmt_track_changed(Tracks); + m_joints.tracks_committed(Tracks); sweeps_committed(Tracks); switch_drives_committed(Tracks); if (m_infra_suspended || false == m_infra.follow) @@ -1211,6 +1372,7 @@ void editor_mode::tracks_committed(std::vector const &Tracks) // only a path running where the old one did, not the one alongside return nearest_beside(*everywhere, Foot, 1.0, Station); }; + std::vector dropped; for (auto &binding : m_bindings) { auto const affected{std::any_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { @@ -1218,6 +1380,14 @@ void editor_mode::tracks_committed(std::vector const &Tracks) })}; if (false == affected || false == alive(binding)) continue; + std::vector standing; + std::optional facing; + if (binding.adopted && (false == object_points(binding, standing, facing) || standing.empty() || glm::distance(standing.front(), binding.measured) > 0.01)) + { + // moved by another tool while its track was being changed: it stays where that put it, bound again with the next change + dropped.push_back(binding.include); + continue; + } auto moved{binding}; auto lost{false}; for (auto &anchor : moved.anchors) @@ -1244,27 +1414,58 @@ void editor_mode::tracks_committed(std::vector const &Tracks) } auto const offset{anchor.offset}; auto const height{anchor.height}; + // a path taking the object over may run the other way: the object stays on its side, turned as it was + auto const before{plan_of(anchor.end - anchor.start)}; anchor = infra::make_anchor(station, anchor.foot); - anchor.offset = offset; + auto const reversed{glm::length(before) > 1e-6 && glm::dot(before, infra::frame_at(anchor.at).forward) < 0.0}; + anchor.offset = reversed ? -offset : offset; anchor.height = height; + if (reversed && &anchor == &moved.anchors.front()) + moved.yaw = clamp_circular(moved.yaw + 180.0); } if (lost) { - binding.lost = true; + if (binding.adopted) + dropped.push_back(binding.include); + else + binding.lost = true; continue; } + if (binding.adopted) + { + // where nothing moved, the directive stays as it is written + auto const there{infra::place(moved.anchors.front())}; + auto const turned{facing.has_value() && moved.turns ? std::abs(clamp_circular(infra::heading(infra::frame_at(moved.anchors.front().at).forward) + moved.yaw - *facing + 180.0) - 180.0) : 0.0}; + if (glm::distance(there, standing.front()) < 0.005 && turned < 0.01) + { + moved.lost = false; + binding = std::move(moved); + continue; + } + } infra_buffer(binding, false); moved.lost = false; binding = std::move(moved); infra_move(binding); + m_infra_carried.emplace(binding.node, binding.include); + if (binding.adopted) + { + std::vector points; + std::optional yaw; + if (object_points(binding, points, yaw) && false == points.empty()) + binding.measured = points.front(); + } for (auto &entry : m_infra_buffer) if (entry.saved.same(binding)) entry.moved = true; } + if (false == dropped.empty()) + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), [&](infra::binding const &Binding) { return Binding.adopted && contains(dropped, Binding.include); }), m_bindings.end()); } -void editor_mode::track_retired(TTrack &) +void editor_mode::track_retired(TTrack &Track) { + nmt_track_gone(Track); tracks_committed({}); } @@ -1347,6 +1548,9 @@ void editor_mode::infra_attach(EditorSnapshot &Snapshot) auto const involved = [&](infra::object_state const &State) { if (State.moved) return true; + // one the editor bound for the change which didn't move: its own changes, if any, are taken back by the tool which made them + if (State.saved.adopted) + return false; for (auto const &anchor : State.saved.anchors) { if (contains(Snapshot.created, anchor.at.track) || contains(Snapshot.removed, anchor.at.track)) @@ -1381,7 +1585,7 @@ void editor_mode::render_infra_body() "models and includes turn with the path and stay upright. The move is a part of the undo step of the change.")); ImGui::SameLine(); ImGui::Checkbox(STR_C("Show in 3D"), &state.show); - ImGui::Text(STR_C("Objects bound in the scenery: %d"), static_cast(m_bindings.size())); + ImGui::Text(STR_C("Objects bound in the scenery: %d"), infra_bound_count()); if (false == m_bindings_unresolved.empty()) { ImGui::SameLine(); @@ -1599,9 +1803,9 @@ void editor_mode::render_infra_bound() std::unordered_set const tracks(scope.begin(), scope.end()); std::vector along; for (std::size_t i = 0; i < m_bindings.size(); ++i) - if (std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) + if (false == m_bindings[i].adopted && std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) along.push_back(i); - ImGui::Text(STR_C("%d of %d"), static_cast(along.size()), static_cast(m_bindings.size())); + ImGui::Text(STR_C("%d of %d"), static_cast(along.size()), infra_bound_count()); if (false == along.empty()) { ImGui::SameLine(); @@ -1665,7 +1869,8 @@ void editor_mode::draw_infra_overlay() const drawlist->AddCircleFilled(screen, Size, Colour); }; for (auto const &binding : m_bindings) - draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); + if (false == binding.adopted) + draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); auto const object = [&](infra_candidate const &Candidate) { std::vector points; std::optional yaw; diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index 856623cc..1d61aa66 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -8,12 +8,13 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" +#include "application/editormodejoints.h" +#include "editor/editorNmt.hpp" #include "application/editormodeshared.h" -#include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorProfile.hpp" #include "scene/scene.h" #include "scene/scenelayers.h" @@ -45,8 +46,14 @@ double const kModelStep{5.0}; // m between the places of the tracks compared wit std::size_t const kModelRun{200}; // places of a path whose model is asked for at once double const kModelPoll{0.25}; // s between the looks whether the model is in double const kUnfitted{30.0}; // m; a scenery whose tracks lie this far from the model as a whole isn't fitted to it +// a track over the model by more than the depth goes on a bridge or a viaduct the model leaves out where the model falls away by +// the abutments, steeper than the banks, within the reach along the track both ways; as the grade lines fitted to the model +// tell them. By the abutments the track is over the model by less +double const kBridgeDepth{1.0}; // m +double const kAbutment{100.0}; // per mille +double const kBridgeReach{200.0}; // m -using kind = editor_mode::joint_issue::kind; +using kind = joints_tool::joint_issue::kind; bool rail(TTrack const &Track) { @@ -85,16 +92,11 @@ double plan_angle(glm::dvec3 const &A, glm::dvec3 const &B) return glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(a), glm::normalize(b)), -1.0, 1.0))); } -using end_key = std::tuple; - -end_key key_of(TTrack const *Track, editor_track::point_ref const &Point) +std::tuple key_of(TTrack const *Track, editor_track::point_ref const &Point) { return {Track, Point.path, static_cast(Point.kind)}; } -// pairs of the ends already looked at in the check under way -std::set> reported; - char const *kind_name(kind const Kind) { switch (Kind) @@ -109,7 +111,7 @@ char const *kind_name(kind const Kind) } } -std::string value_text(editor_mode::joint_issue const &Issue) +std::string value_text(joints_tool::joint_issue const &Issue) { switch (Issue.type) { @@ -137,16 +139,14 @@ ImU32 kind_colour(kind const Kind) } } -std::string name_of(TTrack const *Track) +std::string label_of(TTrack const *Track) { - if (Track == nullptr) - return "-"; - return Track->name().empty() ? STR("(no name)") : Track->name(); + return Track == nullptr ? std::string{"-"} : ::name_of(*Track); } } // namespace -void editor_mode::joints_start() +void joints_tool::joints_start() { auto &state{m_joints}; state.queue.clear(); @@ -154,7 +154,7 @@ void editor_mode::joints_start() state.issues.clear(); state.selected = -1; state.status.clear(); - reported.clear(); + m_reported.clear(); glm::dvec3 const camera{Global.pCamera.Pos}; for (auto *track : simulation::Paths.sequence()) { @@ -164,7 +164,7 @@ void editor_mode::joints_start() continue; state.queue.push_back(track); } - state.history = m_history.size(); + state.history = m_history.history_revision(); state.places.clear(); state.runs.clear(); state.run_next = 0; @@ -174,14 +174,14 @@ void editor_mode::joints_start() state.model_unplaced = false; if (false == state.model_check || state.queue.empty()) return; - nmt_locate(); - if (false == m_nmt.placed()) + m_ground.nmt_locate(); + if (false == m_ground.terrain_model().placed()) { state.model_unplaced = true; return; } std::set const queued(state.queue.begin(), state.queue.end()); - for (auto const &place : nmt_track_places(kModelStep)) + for (auto const &place : m_ground.nmt_track_places(kModelStep)) if (place.shapes && queued.count(place.track) > 0) { if (state.places.empty() || state.places.back().track != place.track || state.places.back().path != place.path || state.places.size() - state.runs.back().first >= kModelRun) @@ -192,7 +192,7 @@ void editor_mode::joints_start() state.model_waiting = false == state.places.empty(); } -bool editor_mode::joints_model() +bool joints_tool::joints_model() { auto &state{m_joints}; auto const now{ImGui::GetTime()}; @@ -205,8 +205,8 @@ bool editor_mode::joints_model() for (auto i = Run.first; i < Run.second; ++i) { glm::dvec2 const at{state.places[i].position.x, state.places[i].position.z}; - Min = glm::min(Min, at - 2.0); - Max = glm::max(Max, at + 2.0); + Min = glm::min(Min, at - kBridgeReach); + Max = glm::max(Max, at + kBridgeReach); } }; glm::dvec2 low, high; @@ -216,32 +216,63 @@ bool editor_mode::joints_model() for (auto const &run : state.runs) { box(run, low, high); - m_nmt.request(low, high); + m_ground.terrain_model().request(low, high); } state.model_asked = true; } while (state.run_next < state.runs.size()) { box(state.runs[state.run_next], low, high); - if (false == m_nmt.request(low, high)) + if (false == m_ground.terrain_model().request(low, high)) return false; ++state.run_next; } std::vector differences(state.places.size(), std::numeric_limits::quiet_NaN()); std::vector known; for (std::size_t i = 0; i < state.places.size(); ++i) - if (double height; m_nmt.height(state.places[i].position.x, state.places[i].position.z, height)) + if (double height; m_ground.terrain_model().height(state.places[i].position.x, state.places[i].position.z, height)) { - differences[i] = state.places[i].crown - (height + m_nmt_state.offset); + differences[i] = state.places[i].crown - (height + m_ground.terrain_model_offset()); known.push_back(differences[i]); } auto shift{0.0}; - if (false == m_nmt_state.fitted && false == known.empty()) + if (false == m_ground.terrain_model_fitted() && false == known.empty()) { std::nth_element(known.begin(), known.begin() + known.size() / 2, known.end()); if (std::abs(known[known.size() / 2]) > kUnfitted) shift = known[known.size() / 2]; } + // a place on a bridge or a viaduct the model leaves out isn't off it: the route through its track sampled both ways, the + // stretches over the model as on a bridge found along it + std::map, std::vector>> routes; + auto const on_bridge = [&](nmt_place const &Place) { + auto found{routes.find(Place.track)}; + if (found == routes.end()) + { + auto route{editor_track::run_route(*Place.track, kBridgeReach)}; + auto samples{editor_track::sample_route(route, 1.0)}; + std::vector chainages, heights, ground; + for (auto const &sample : samples) + { + chainages.push_back(sample.chainage); + heights.push_back(sample.position.y - route.spans[sample.span].track->rail_height()); + double height; + ground.push_back(m_ground.terrain_model().height(sample.position.x, sample.position.z, height) ? height + m_ground.terrain_model_offset() + shift : std::numeric_limits::quiet_NaN()); + } + auto spans{profile::bridged(chainages, heights, ground, kBridgeDepth, kAbutment)}; + found = routes.emplace(Place.track, std::make_pair(std::move(samples), std::move(spans))).first; + } + auto const &[samples, spans]{found->second}; + auto nearest{std::numeric_limits::max()}; + double at{0.0}; + for (auto const &sample : samples) + if (auto const distance{geometry::plan_distance(sample.position, Place.position)}; distance < nearest) + { + nearest = distance; + at = sample.chainage; + } + return nearest <= 1.0 && std::any_of(spans.begin(), spans.end(), [&](profile::departure const &Span) { return at >= Span.from - kModelStep && at <= Span.to + kModelStep; }); + }; // the place of each path the farthest from the model std::map, std::size_t> worst; for (std::size_t i = 0; i < state.places.size(); ++i) @@ -249,7 +280,7 @@ bool editor_mode::joints_model() if (false == std::isfinite(differences[i])) continue; differences[i] -= shift; - if (std::abs(differences[i]) <= state.model) + if (std::abs(differences[i]) <= state.model || (differences[i] > 0.0 && on_bridge(state.places[i]))) continue; auto const key{std::make_pair(state.places[i].track, state.places[i].path)}; auto const found{worst.find(key)}; @@ -275,10 +306,10 @@ bool editor_mode::joints_model() return true; } -void editor_mode::joints_check(TTrack &Track) +void joints_tool::joints_check(TTrack &Track, std::vector &Issues, std::set> &Seen, bool const Open) const { - auto &state{m_joints}; - auto const once = [&](end_key const &A, end_key const &B) { return reported.insert(A < B ? std::make_pair(A, B) : std::make_pair(B, A)).second; }; + auto const &state{m_joints}; + auto const once = [&](end_key const &A, end_key const &B) { return Seen.insert(A < B ? std::make_pair(A, B) : std::make_pair(B, A)).second; }; for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) { for (auto const kind_of_end : {editor_track::point_kind::start, editor_track::point_kind::end}) @@ -316,17 +347,17 @@ void editor_mode::joints_check(TTrack &Track) { nearest.type = kind::gap; nearest.position = position; - state.issues.push_back(nearest); + Issues.push_back(nearest); } } - else if (state.free_ends) + else if (Open) { joint_issue open; open.type = kind::open; open.track = &Track; open.point = point; open.position = position; - state.issues.push_back(open); + Issues.push_back(open); } continue; } @@ -351,7 +382,7 @@ void editor_mode::joints_check(TTrack &Track) auto const add = [&](kind const Kind, double const Value) { issue.type = Kind; issue.value = Value; - state.issues.push_back(issue); + Issues.push_back(issue); }; if (step > state.step) add(kind::step, step); @@ -366,9 +397,44 @@ void editor_mode::joints_check(TTrack &Track) } } -void editor_mode::joints_step() +void joints_tool::tracks_committed(std::vector const &Tracks) +{ + for (auto *track : Tracks) + if (track != nullptr) + m_live.pending.insert(track); +} + +void joints_tool::live_update() +{ + auto &live{m_live}; + auto const gone = [](TTrack const *Track) { return Track != nullptr && Track->m_editorremoved; }; + std::erase_if(live.issues, [&](joint_issue const &Issue) { return gone(Issue.track) || gone(Issue.other); }); + if (live.pending.empty() || ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right)) + return; + // the joints of a track changed are looked at again, from its side and from the side of the tracks it joins + std::set checked; + for (auto *track : live.pending) + { + if (track->m_editorremoved || false == rail(*track)) + continue; + checked.insert(track); + for (auto const &path : track->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + for (auto const &connection : editor_track::connected_points(*track, path.points[index])) + if (rail(*connection.first)) + checked.insert(connection.first); + } + live.pending.clear(); + std::erase_if(live.issues, [&](joint_issue const &Issue) { return checked.contains(Issue.track) || checked.contains(Issue.other); }); + std::set> seen; + for (auto *track : checked) + joints_check(*track, live.issues, seen, false); +} + +void joints_tool::update() { auto &state{m_joints}; + live_update(); if (state.next >= state.queue.size() && false == state.model_waiting) return; auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)}; @@ -377,7 +443,7 @@ void editor_mode::joints_step() auto *track{state.queue[state.next++]}; if (track->m_editorremoved) continue; - joints_check(*track); + joints_check(*track, state.issues, m_reported, state.free_ends); } if (state.next < state.queue.size()) return; @@ -401,7 +467,7 @@ void editor_mode::joints_step() WriteLog("Editor: joints check - " + state.status, logtype::generic); } -void editor_mode::joints_focus(int const Index) +void joints_tool::joints_focus(int const Index) { auto &state{m_joints}; if (Index < 0 || Index >= static_cast(state.issues.size())) @@ -411,13 +477,12 @@ void editor_mode::joints_focus(int const Index) auto const &paths{simulation::Paths.sequence()}; if (std::find(paths.begin(), paths.end(), issue.track) == paths.end() || issue.track->m_editorremoved) return; - m_node = issue.track; - m_track_point = {}; - ui()->set_node(m_node); - focus_track(*issue.track, issue.point.path, issue.position); + m_selection.set_current_node(issue.track); + m_selection.clear_track_point(); + m_view.focus_track(*issue.track, issue.point.path, issue.position); } -bool editor_mode::joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed) +bool joints_tool::joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed) { if (Issue.fixed || Issue.type == kind::open || Issue.type == kind::model || Issue.track == nullptr || Issue.other == nullptr || Issue.track->m_editorremoved || Issue.other->m_editorremoved) return false; @@ -481,7 +546,7 @@ bool editor_mode::joint_fix(joint_issue &Issue, std::vector const &Indices) +void joints_tool::joints_fix(std::vector const &Indices) { auto &state{m_joints}; std::vector> states; @@ -499,16 +564,16 @@ void editor_mode::joints_fix(std::vector const &Indices) state.status = STR_C("Nothing could be fixed: the paths are switches or in the files which can't be changed"); return; } - push_track_snapshot(std::move(states)); + m_history.push_track_snapshot(std::move(states)); editor_track::commit(changed); for (auto *track : changed) track->mark_dirty(); - state.history = m_history.size(); + state.history = m_history.history_revision(); state.status = format(STR_C("%zu joints fixed, Ctrl+Z takes them back"), count); WriteLog("Editor: joints check - " + state.status, logtype::generic); } -void editor_mode::render_joints_body() +void joints_tool::render_options() { auto &state{m_joints}; ImGui::PushTextWrapPos(0.0f); @@ -539,7 +604,7 @@ void editor_mode::render_joints_body() { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.55f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.75f))); - if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, tall_button_height()))) joints_start(); ImGui::PopStyleColor(2); item_tooltip("Finds the gaps between the ends of the tracks, the steps, the kinks and the jumps of the cant and of the grade"); @@ -581,7 +646,7 @@ void editor_mode::render_joints_body() ImGui::TextWrapped("%s", state.status.c_str()); if (state.issues.empty()) return; - if (state.history != m_history.size()) + if (state.history != m_history.history_revision()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("The scenery changed since the check")); int filter{state.filter + 1}; ImGui::SetNextItemWidth(150.0f); @@ -633,9 +698,9 @@ void editor_mode::render_joints_body() ImGui::NextColumn(); ImGui::TextUnformatted(issue.fixed ? STR_C("fixed") : value_text(issue).c_str()); ImGui::NextColumn(); - ImGui::TextUnformatted(name_of(issue.track).c_str()); + ImGui::TextUnformatted(label_of(issue.track).c_str()); ImGui::NextColumn(); - ImGui::TextUnformatted(name_of(issue.other).c_str()); + ImGui::TextUnformatted(label_of(issue.other).c_str()); ImGui::NextColumn(); } ImGui::Columns(1); @@ -643,20 +708,35 @@ void editor_mode::render_joints_body() if (chosen) { auto &issue{state.issues[state.selected]}; - ImGui::Text("%s %s: %s / %s", STR_C(kind_name(issue.type)), value_text(issue).c_str(), name_of(issue.track).c_str(), name_of(issue.other).c_str()); + ImGui::Text("%s %s: %s / %s", STR_C(kind_name(issue.type)), value_text(issue).c_str(), label_of(issue.track).c_str(), label_of(issue.other).c_str()); if (false == issue.fixed && issue.type != kind::open && issue.type != kind::model && ImGui::Button(STR_C("Fix this one"))) joints_fix({state.selected}); } } -void editor_mode::draw_joints_overlay() const +void joints_tool::draw_overlay() const { auto const &state{m_joints}; - if (false == state.open || state.issues.empty()) - return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; glm::dvec3 const camera{Global.pCamera.Pos}; + if (false == state.open) + { + for (auto const &issue : m_live.issues) + { + ImVec2 screen; + if ((false == Global.EditorOrtho && glm::distance(issue.position, camera) > kOverlayRange) || false == projection.project(issue.position, screen)) + continue; + auto const colour{kind_colour(issue.type)}; + drawlist->AddCircle(screen, 9.0f, colour, 16, 2.0f); + auto const text{std::string{STR_C(kind_name(issue.type))} + " " + value_text(issue)}; + drawlist->AddRectFilled(ImVec2(screen.x + 11.0f, screen.y - 9.0f), ImVec2(screen.x + 17.0f + ImGui::CalcTextSize(text.c_str()).x, screen.y + 9.0f), IM_COL32(0, 0, 0, 150), 3.0f); + drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 7.0f), colour, text.c_str()); + } + return; + } + if (state.issues.empty()) + return; for (int i = 0; i < static_cast(state.issues.size()); ++i) { auto const &issue{state.issues[i]}; @@ -671,3 +751,72 @@ void editor_mode::draw_joints_overlay() const drawlist->AddText(ImVec2(screen.x + 13.0f, screen.y - 7.0f), colour, text.c_str()); } } + +editor_selftest_result joints_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "jointsmodel") + { + // the whole scenery against the terrain model, as the check of the panel does it, waiting for the model + static bool started{false}; + if (false == started) + { + double threshold{0.5}; + Words >> threshold; + m_joints.scope = 1; + m_joints.model_check = true; + m_joints.model = threshold; + joints_start(); + started = true; + } + update(); + if (m_joints.next < m_joints.queue.size() || m_joints.model_waiting) + return editor_selftest_result::again; + started = false; + std::size_t count{0}; + for (auto const &issue : m_joints.issues) + if (issue.type == joint_issue::kind::model && count++ < 40) + WriteLog(format("SELFTEST jointsmodel: %+.2f m at %.1f %.1f, %s path %d", issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", issue.point.path)); + WriteLog("SELFTEST jointsmodel: " + m_joints.status); + return editor_selftest_result::handled; + } + if (Command == "jointslive") + { + live_update(); + std::string text; + for (auto const &issue : m_live.issues) + text += format(" [%s %s at %.1f %.1f]", kind_name(issue.type), value_text(issue).c_str(), issue.position.x, issue.position.z); + WriteLog(format("SELFTEST jointslive: %zu pending, %zu marked", m_live.pending.size(), m_live.issues.size()) + text); + return editor_selftest_result::handled; + } + if (Command == "joints") + { + std::vector points; + for (glm::dvec3 point{0.0}; Words >> point.x >> point.z;) + points.push_back(point); + auto const scope{m_joints.scope}; + auto const model{m_joints.model_check}; + m_joints.scope = 1; + m_joints.model_check = false; + joints_start(); + while (m_joints.next < m_joints.queue.size()) + update(); + m_joints.scope = scope; + m_joints.model_check = model; + char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "off the terrain model", "free end"}; + int around{0}; + for (auto const &issue : m_joints.issues) + { + if (false == points.empty() && issue.type == joint_issue::kind::open) + continue; + if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) + continue; + ++around; + auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name() + (Track->eType == tt_Switch ? "(switch)" : ""); }; + auto const rise{issue.other != nullptr ? editor_track::point_position(*issue.other, issue.theirs).y - issue.position.y : 0.0}; + WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s, rise %.3f", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, name(issue.track).c_str(), name(issue.other).c_str(), rise)); + } + WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodejoints.h b/application/editormodejoints.h new file mode 100644 index 00000000..01617d88 --- /dev/null +++ b/application/editormodejoints.h @@ -0,0 +1,100 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editortool.h" + +#include +#include +#include +#include +#include + +class joints_tool final : public editor_tool +{ + public: + // joint of the paths found wrong by the joints check + struct joint_issue + { + enum class kind { gap, step, kink, cant, grade, model, open }; + kind type{kind::gap}; + TTrack *track{nullptr}; + editor_track::point_ref point; + TTrack *other{nullptr}; + editor_track::point_ref theirs; + glm::dvec3 position{0.0}; + double value{0.0}; // m, deg, mm, per mille; m of the track over the terrain model + bool fixed{false}; + }; + joints_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_ground &Ground) : m_selection{Selection}, m_view{View}, m_history{History}, m_ground{Ground} {} + void update() override; + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + void set_open(bool const Open) { m_joints.open = Open; } + // the tracks just changed, checked at their joints once the change is over + void tracks_committed(std::vector const &Tracks); + double kink_angle() const { return m_joints.angle; } + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_ground &m_ground; + // joints of the paths: ends which almost meet, steps, kinks, jumps of the cant and of the grade + struct joints_state + { + bool open{false}; + int scope{0}; // 0: around the camera, 1: the whole scenery + double reach{2000.0}; + double gap{1.0}; // m, free ends closer than this to another end + double step{0.005}; // m + double angle{0.2}; // deg + double cant{5.0}; // mm + double grade{10.0}; // per mille + double model{0.5}; // m, the track over or under the terrain model (NMT) + bool free_ends{false}; + bool model_check{true}; + std::vector queue; + std::size_t next{0}; + // the places of the tracks compared with the terrain model, in runs of one path each, once the model under them is in + std::vector places; + std::vector> runs; + std::size_t run_next{0}; + bool model_waiting{false}; + bool model_asked{false}; + double model_polled{0.0}; + bool model_unplaced{false}; + std::vector issues; + int filter{-1}; + int selected{-1}; + std::size_t history{0}; + std::string status; + }; + joints_state m_joints; + using end_key = std::tuple; + // pairs of the ends already looked at in the check under way + std::set> m_reported; + // the joints of the tracks changed last, marked while the panel is closed + struct live_state + { + std::set pending; + std::vector issues; + }; + live_state m_live; + void live_update(); + void joints_start(); + void joints_check(TTrack &Track, std::vector &Issues, std::set> &Seen, bool const Open) const; + // the paths lying off the terrain model by more than the threshold. returns: false while the model under them is coming + bool joints_model(); + void joints_focus(int const Index); + bool joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed); + void joints_fix(std::vector const &Indices); +}; diff --git a/application/editormodekm.cpp b/application/editormodekm.cpp new file mode 100644 index 00000000..d1fe8362 --- /dev/null +++ b/application/editormodekm.cpp @@ -0,0 +1,1025 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormodekm.h" +#include "application/editorgestures.h" +#include "application/editormodeshared.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorTrack.hpp" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::plan_distance; +using geometry::plan_of; + +double constexpr kOverlayRange{3000.0}; +double constexpr kTickRange{1500.0}; // m, of the hectometres shown in the scenery +double constexpr kLabelRange{700.0}; // m, of the km written there, farther ones crowd the horizon +double constexpr kDigitRange{250.0}; // m, of the hectometres written there +double constexpr kRailHead{0.18}; // m, above the axis of a path +double constexpr kPickReach{15.0}; // m from a point of the km a click on a track takes it +double constexpr kReadoutReach{30.0}; // m from the axis of a line its km is shown by the mouse +double constexpr kPostReach{60000.0}; // m along the line each way, of the posts read +double constexpr kPostCorridor{15.0}; // m from the track, posts farther away belong to another line +double constexpr kPostTolerance{10.0}; // m, of the distance between two posts against their numbers +double constexpr kCurve{10000.0}; // m, of the radius a jump of the km is on a curve under +double constexpr kStation{300.0}; // m from a switch a jump of the km is at a station + +ImU32 constexpr kAxis{IM_COL32(110, 200, 255, 220)}; +ImU32 constexpr kGuessed{IM_COL32(110, 200, 255, 90)}; +ImU32 constexpr kTick{IM_COL32(235, 240, 250, 230)}; +ImU32 constexpr kKm{IM_COL32(255, 215, 90, 255)}; +ImU32 constexpr kJump{IM_COL32(255, 95, 215, 255)}; +ImU32 constexpr kMark{IM_COL32(95, 220, 140, 255)}; +ImU32 constexpr kAstray{IM_COL32(240, 90, 80, 255)}; + +// the digits after the rotation make the number of a hectometre post, the last one the hectometre +bool hectometres_of(std::vector const &Values, int &Hectometres) +{ + if (Values.size() < 6) + return false; + Hectometres = 0; + for (std::size_t i = 5; i < Values.size(); ++i) + { + double digit; + if (false == numeric(Values[i], digit) || digit < 0.0 || digit > 9.0) + return false; + Hectometres = Hectometres * 10 + static_cast(digit); + } + return true; +} + +// chainage of the point of the samples nearest to the point, and how far from them it is +double project(std::vector const &Samples, glm::dvec3 const &Point, double &Distance) +{ + Distance = std::numeric_limits::max(); + double chainage{0.0}; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < Samples.size(); ++i) + { + auto const a{plan_of(Samples[i - 1].position)}; + auto const ab{plan_of(Samples[i].position) - a}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; + if (auto const distance{glm::length(point - (a + ab * t))}; distance < Distance) + { + Distance = distance; + chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t; + } + } + return chainage; +} + +std::size_t sample_at(std::vector const &Samples, double const Chainage) +{ + auto const found{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + return std::min(static_cast(found - Samples.begin()), Samples.size() - 1); +} + +// the number of a line: blanks taken out +std::string line_of(std::string const &Text) +{ + std::string result; + for (auto const c : Text) + if (false == std::isspace(static_cast(c))) + result += c; + return result; +} + +// a km typed as PKP PLK write it, 100,234, or with a point +bool km_of(char const *Text, double &Km) +{ + std::string text{Text}; + std::replace(text.begin(), text.end(), ',', '.'); + char *end{nullptr}; + auto const value{std::strtod(text.c_str(), &end)}; + if (end == text.c_str() || false == std::isfinite(value)) + return false; + Km = value; + return true; +} + +std::string mark_text(kilometrage::mark const &Mark) +{ + return Mark.beyond.has_value() ? kilometrage::text(Mark.km) + " = " + kilometrage::text(*Mark.beyond) : kilometrage::text(Mark.km); +} + +void draw_label(ImDrawList *List, ImVec2 const At, ImU32 const Ground, ImU32 const Colour, std::string const &Text) +{ + auto const size{ImGui::CalcTextSize(Text.c_str())}; + List->AddRectFilled(ImVec2(At.x - size.x * 0.5f - 4.0f, At.y - size.y - 4.0f), ImVec2(At.x + size.x * 0.5f + 4.0f, At.y), Ground, 3.0f); + List->AddText(ImVec2(At.x - size.x * 0.5f, At.y - size.y - 2.0f), Colour, Text.c_str()); +} + +void draw_arrow(screen_projection const &Projection, ImDrawList *List, glm::dvec3 const &From, glm::dvec2 const &Way, double const Length, ImU32 const Colour) +{ + glm::dvec3 const along{Way.x, 0.0, Way.y}; + glm::dvec3 const across{-Way.y, 0.0, Way.x}; + auto const tip{From + along * Length}; + Projection.line(List, From, tip, Colour, 3.0f); + Projection.line(List, tip, tip - along * (Length * 0.3) + across * (Length * 0.15), Colour, 3.0f); + Projection.line(List, tip, tip - along * (Length * 0.3) - across * (Length * 0.15), Colour, 3.0f); +} + +} // namespace + +void km_tool::refresh() +{ + m_drawn.clear(); + for (auto const &line : kilometrage::lines()) + { + auto const *axis{kilometrage::axis_of(line)}; + if (axis == nullptr || axis->points.size() < 2) + continue; + drawn entry; + entry.line = line; + entry.points = axis->points; + auto const low{axis->anchors.front().chainage - 1.0}; + auto const high{axis->anchors.back().chainage + 1.0}; + for (auto const chainage : axis->chainage) + entry.guessed.push_back(chainage < low || chainage > high ? 1 : 0); + for (auto const &anchor : axis->anchors) + if (std::abs(anchor.after - anchor.before) >= 1e-6) + { + auto const way{kilometrage::direction_on(*axis, anchor.chainage)}; + entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true}); + } + for (auto const &hectometre : kilometrage::hectometres(*axis)) + entry.ticks.push_back({hectometre.at, {-hectometre.rising.y, 0.0, hectometre.rising.x}, hectometre.number, false}); + m_drawn.push_back(std::move(entry)); + // the shorter track where the tracks run apart + for (auto const &side : axis->sides) + { + drawn apart; + apart.line = line; + apart.points = side.points; + apart.guessed.assign(side.points.size(), 0); + m_drawn.push_back(std::move(apart)); + } + } + m_revision = kilometrage::revision(); + m_drawn_once = true; +} + +void km_tool::update() +{ + if (false == m_open) + return; + auto const &marks{kilometrage::marks()}; + // after an undo the point edited may be gone + if (m_edited >= 0 && (m_edited >= static_cast(marks.size()) || false == m_pick.has_value() || plan_distance(marks[m_edited].point, m_pick->point) > 0.5)) + m_edited = -1; + if (false == m_drawn_once || kilometrage::revision() != m_revision) + refresh(); +} + +void km_tool::forget(scene::basic_node const *Node) +{ + if (m_pick.has_value() && m_pick->track == Node) + m_pick->track = nullptr; +} + +glm::dvec2 km_tool::rising() const +{ + if (false == m_pick.has_value()) + return {0.0, 1.0}; + return m_pick->along * (m_against ? -1.0 : 1.0); +} + +std::string km_tool::jump_warning(glm::dvec3 const &Point) const +{ + auto *track{kilometrage::track_near(Point, 6.0)}; + if (track == nullptr) + return {}; + auto route{editor_track::run_route(*track, kStation + 50.0)}; + auto const samples{editor_track::sample_route(route, 2.0)}; + if (samples.size() < 2) + return {}; + double gap{0.0}; + auto const at{project(samples, Point, gap)}; + bool curve{false}; + bool station{false}; + for (auto const &sample : samples) + { + auto const apart{std::abs(sample.chainage - at)}; + if (apart <= 30.0 && std::abs(sample.curvature) > 1.0 / kCurve) + curve = true; + if (apart <= kStation && sample.span < route.spans.size() && route.spans[sample.span].track->eType != tt_Normal) + station = true; + } + if (curve && station) + return STR("Id-1 § 19: the km shouldn't jump on a curve nor at a station, move the jump to a straight on the open line"); + if (curve) + return STR("Id-1 § 19: the km shouldn't jump on a curve, move the jump to a straight"); + if (station) + return STR("Id-1 § 19: the km shouldn't jump at a station, move the jump out to the open line"); + return {}; +} + +void km_tool::pick(TTrack &Track, glm::dvec3 const &Point) +{ + auto const t{editor_track::nearest_parameter(Track, Point)}; + picked entry; + entry.track = &Track; + entry.point = editor_track::point_at(Track, t); + auto const ahead{plan_of(editor_track::point_at(Track, std::min(1.0, t + 0.01)) - editor_track::point_at(Track, std::max(0.0, t - 0.01)))}; + entry.along = glm::length(ahead) > 1e-9 ? glm::normalize(ahead) : glm::dvec2{0.0, 1.0}; + entry.path = kilometrage::path_near(Track, Point, entry.on); + m_status.clear(); + m_route_status.clear(); + m_edited = -1; + auto const &marks{kilometrage::marks()}; + auto nearest{kPickReach}; + for (std::size_t index = 0; index < marks.size(); ++index) + if (auto const distance{plan_distance(marks[index].point, entry.point)}; distance < nearest) + { + nearest = distance; + m_edited = static_cast(index); + } + if (m_edited >= 0) + { + auto const &mark{marks[m_edited]}; + entry.point = mark.point; + m_line = mark.line; + m_km = mark.km; + m_jump = mark.beyond.has_value(); + m_beyond = mark.beyond.value_or(mark.km); + m_against = glm::dot(mark.way, entry.along) < 0.0; + } + else + { + // the line the point lies on, and its km there + if (auto const line{kilometrage::line_at(entry.point, kReadoutReach)}; false == line.empty()) + m_line = line; + double km{0.0}; + if (false == m_line.empty() && kilometrage::km_at(m_line, entry.point, km, kReadoutReach)) + { + m_km = std::round(km * 1000.0) / 1000.0; + auto const *axis{kilometrage::axis_of(m_line)}; + double gap{0.0}; + auto const along{kilometrage::chainage_at(*axis, entry.point, gap)}; + m_against = glm::dot(kilometrage::direction_on(*axis, along) * static_cast(axis->growth), entry.along) < 0.0; + } + m_jump = false; + m_beyond = m_km; + } + m_pick = entry; +} + +std::string km_tool::readout(glm::dvec3 const &Point) const +{ + std::string result; + for (auto const &line : kilometrage::lines()) + { + double km{0.0}; + if (false == kilometrage::km_at(line, Point, km, kReadoutReach)) + continue; + if (false == result.empty()) + result += '\n'; + result += format(STR_C("line %s: km %s"), line.c_str(), kilometrage::text(km).c_str()); + } + return result; +} + +void km_tool::put() +{ + m_status.clear(); + if (false == m_pick.has_value()) + return; + auto const line{line_of(m_line)}; + if (line.empty()) + { + m_status = STR("Give the number of the line first"); + return; + } + if (m_jump && std::abs(m_beyond - m_km) < 0.0005) + { + m_status = STR("A jump needs another km on from the point"); + return; + } + kilometrage::mark entry; + entry.line = line; + entry.point = m_pick->point; + entry.way = rising(); + entry.km = m_km; + if (m_jump) + entry.beyond = m_beyond; + if (m_edited >= 0) + kilometrage::change(static_cast(m_edited), entry); + else + { + kilometrage::put(entry); + m_edited = static_cast(kilometrage::marks().size()) - 1; + } + m_line = line; + m_history.push_kilometrage_snapshot(); + m_status = format(STR_C("Line %s: km %s at the point"), line.c_str(), mark_text(entry).c_str()); +} + +void km_tool::remove() +{ + m_status.clear(); + if (m_edited < 0) + return; + kilometrage::remove(static_cast(m_edited)); + m_edited = -1; + m_history.push_kilometrage_snapshot(); + m_status = STR("The point of the km removed"); +} + +void km_tool::show(std::size_t const Index) +{ + auto const &marks{kilometrage::marks()}; + if (Index >= marks.size()) + return; + auto const point{marks[Index].point}; + if (auto *track{kilometrage::track_near(point, 6.0)}; track != nullptr) + pick(*track, point); + m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); +} + +void km_tool::show_via(std::size_t const Index) +{ + auto const &vias{kilometrage::vias()}; + if (Index >= vias.size()) + return; + auto const point{vias[Index].point}; + auto const line{vias[Index].line}; + int path{0}; + if (auto *track{kilometrage::track_near(point, 6.0, path)}; track != nullptr) + { + pick(*track, point); + m_line = line; + } + m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); +} + +int km_tool::via_here() const +{ + if (false == m_pick.has_value() || m_pick->track == nullptr) + return -1; + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + if (vias[index].line != m_line) + continue; + int path{0}; + if (kilometrage::track_near(vias[index].point, 6.0, path) == m_pick->track && path == m_pick->path) + return static_cast(index); + } + return -1; +} + +void km_tool::lead_along() +{ + m_route_status.clear(); + if (false == m_pick.has_value() || m_pick->track == nullptr) + return; + auto const line{line_of(m_line)}; + if (line.empty()) + { + m_route_status = STR("Give the number of the line first"); + return; + } + auto const astray_of = [&]() { + auto const *axis{kilometrage::axis_of(line)}; + return axis != nullptr ? axis->astray.size() : kilometrage::marks().size(); + }; + auto const before{kilometrage::current()}; + auto const astray{astray_of()}; + kilometrage::via entry; + entry.line = line; + entry.point = m_pick->on; + kilometrage::put(entry); + auto const *axis{kilometrage::axis_of(line)}; + if (axis == nullptr || false == axis->runs_along(m_pick->track, m_pick->path)) + { + kilometrage::restore(before); + m_route_status = STR("The line can't run along this track: no way leads from it onto the line"); + return; + } + m_line = line; + m_history.push_kilometrage_snapshot(); + m_route_status = format(STR_C("Line %s runs along this track now"), line.c_str()); + if (auto const lost{astray_of()}; lost > astray) + m_route_status += "\n" + format(STR_P("%zu point of the km is off the line now: the track leads it away from it", "%zu points of the km are off the line now: the track leads it away from them", lost - astray), lost - astray); +} + +void km_tool::drop_via() +{ + m_route_status.clear(); + auto const index{via_here()}; + if (index < 0) + return; + kilometrage::remove_via(static_cast(index)); + m_history.push_kilometrage_snapshot(); + m_route_status = format(STR_C("Line %s runs along the straight paths of the switches here again"), m_line.c_str()); +} + +void km_tool::read_posts() +{ + m_status.clear(); + auto const line{line_of(m_line)}; + if (false == m_pick.has_value() || m_pick->track == nullptr) + { + m_status = STR("Click a track of the line first"); + return; + } + if (line.empty()) + { + m_status = STR("Give the number of the line first"); + return; + } + auto route{editor_track::run_route(*m_pick->track, kPostReach)}; + auto const samples{editor_track::sample_route(route, 2.0)}; + if (samples.size() < 2) + { + m_status = STR("No line through this track"); + return; + } + auto const length{samples.back().chainage}; + struct post + { + double chainage{0.0}; + int hectometres{0}; + }; + std::vector posts; + for (auto const &standing : m_templates.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) + { + post entry; + if (false == hectometres_of(standing.values, entry.hectometres)) + continue; + double distance{0.0}; + entry.chainage = project(samples, standing.location, distance); + if (distance > kPostCorridor || entry.chainage <= 0.0 || entry.chainage >= length) + continue; + posts.push_back(entry); + } + if (posts.size() < 2) + { + m_status = STR("Fewer than two hectometre posts stand along the line, nothing to read"); + return; + } + std::sort(posts.begin(), posts.end(), [](post const &A, post const &B) { return A.chainage < B.chainage; }); + // runs of posts numbered by the distance between them, a post of the same number by the other track taken once + auto const fits = [](post const &From, post const &To, int const Sign) { + auto const counted{(To.hectometres - From.hectometres) * 100.0}; + auto const apart{To.chainage - From.chainage}; + return counted != 0.0 && (Sign == 0 || (counted > 0.0 ? 1 : -1) == Sign) && std::abs(std::abs(counted) - apart) <= std::max(kPostTolerance, apart * 0.02); + }; + auto const sign_of = [](std::vector const &Run) { return Run.size() < 2 ? 0 : Run.back().hectometres > Run.front().hectometres ? 1 : -1; }; + std::vector> runs; + std::size_t twins{0}; + for (auto const &entry : posts) + { + if (false == runs.empty()) + { + auto &run{runs.back()}; + if (entry.hectometres == run.back().hectometres && entry.chainage - run.back().chainage < kPostTolerance * 3.0) + { + ++twins; + continue; + } + if (fits(run.back(), entry, sign_of(run))) + { + run.push_back(entry); + continue; + } + } + runs.push_back({entry}); + } + // a post alone between the runs is one out of place + std::size_t stray{0}; + std::vector> kept; + for (auto &run : runs) + { + if (run.size() < 2) + { + ++stray; + continue; + } + if (false == kept.empty() && fits(kept.back().back(), run.front(), sign_of(kept.back())) && sign_of(kept.back()) == sign_of(run)) + kept.back().insert(kept.back().end(), run.begin(), run.end()); + else + kept.push_back(std::move(run)); + } + std::size_t rising{0}; + std::size_t falling{0}; + for (auto const &run : kept) + (sign_of(run) > 0 ? rising : falling) += run.size(); + auto const growth{rising >= falling ? 1 : -1}; + kept.erase(std::remove_if(kept.begin(), kept.end(), [&](std::vector const &Run) { + if (sign_of(Run) == growth) + return false; + stray += Run.size(); + return true; + }), kept.end()); + if (kept.empty()) + { + m_status = STR("The numbers of the posts don't go by 100 m anywhere along the line, nothing read"); + return; + } + auto const mark_at = [&](double const Chainage, double const Km, std::optional const Beyond) { + kilometrage::mark entry; + entry.line = line; + entry.point = editor_track::sampled_position(samples, Chainage); + entry.way = samples[sample_at(samples, Chainage)].direction * static_cast(growth); + entry.km = Km; + entry.beyond = Beyond; + return entry; + }; + std::vector made; + std::size_t jumps{0}; + std::size_t misplaced{0}; + for (std::size_t r = 0; r < kept.size(); ++r) + { + auto const &run{kept[r]}; + auto const &first{run.front()}; + auto const &last{run.back()}; + // the jump to the next run at the post it starts the irregular hectometre at, along the rising km + auto const jump_before{r > 0 && growth > 0}; + auto const jump_after{r + 1 < kept.size() && growth < 0}; + if (jump_before) + { + auto const &previous{kept[r - 1].back()}; + made.push_back(mark_at(first.chainage, previous.hectometres / 10.0 + (first.chainage - previous.chainage) / 1000.0, first.hectometres / 10.0)); + } + else + made.push_back(mark_at(first.chainage, first.hectometres / 10.0, std::nullopt)); + if (jump_after) + { + auto const &next{kept[r + 1].front()}; + made.push_back(mark_at(last.chainage, next.hectometres / 10.0 + (next.chainage - last.chainage) / 1000.0, last.hectometres / 10.0)); + } + else + made.push_back(mark_at(last.chainage, last.hectometres / 10.0, std::nullopt)); + if (jump_before || jump_after) + { + ++jumps; + if (false == jump_warning((jump_before ? made[made.size() - 2] : made.back()).point).empty()) + ++misplaced; + } + } + // the points of the line along the stretch read give way to the ones read + auto const &standing{kilometrage::marks()}; + for (auto index{static_cast(standing.size()) - 1}; index >= 0; --index) + { + if (standing[index].line != line) + continue; + double gap{0.0}; + project(samples, standing[index].point, gap); + if (gap <= kPostCorridor) + kilometrage::remove(static_cast(index)); + } + for (auto &entry : made) + kilometrage::put(std::move(entry)); + m_line = line; + m_edited = -1; + m_history.push_kilometrage_snapshot(); + m_status = format(STR_C("%zu posts read: %zu points of the km, %zu jumps"), posts.size() - twins, made.size(), jumps); + if (stray > 0) + m_status += "\n" + format(STR_P("%zu post doesn't fit its neighbours and was left out", "%zu posts don't fit their neighbours and were left out", stray), stray); + if (misplaced > 0) + m_status += "\n" + format(STR_P("%zu jump lies on a curve or at a station, Id-1 § 19 wants none there", "%zu jumps lie on a curve or at a station, Id-1 § 19 wants none there", misplaced), misplaced); +} + +void km_tool::render_options() +{ + ImGui::TextDisabled("%s", STR_C("The km of a railway line: the points where the km is known, from a post or a document, and where it jumps. The km of the rest go from them along the axis of the line: between the tracks of a double-track one, along the longer track where the tracks run on formations of their own, the shorter one getting an irregular hectometre")); + ImGui::Spacing(); + gesture_rows({{"LMB on a track", "a point of the km there, or the one standing by it"}}); + auto const &marks{kilometrage::marks()}; + auto const lines{kilometrage::lines()}; + std::size_t jumps{0}; + for (auto const &mark : marks) + if (mark.beyond.has_value()) + ++jumps; + metric_tiles({{format("%zu", lines.size()), STR_C("lines"), kAxis}, {format("%zu", marks.size()), STR_C("points"), kMark}, {format("%zu", jumps), STR_C("jumps"), kJump}}); + + step_heading(1, STR_C("A point of the km")); + if (false == m_pick.has_value()) + ImGui::TextDisabled("%s", STR_C("Click a track where the km is known: by a post, a station or a bridge")); + else + { + if (begin_fields("##kmpoint")) + { + field_row("Line"); + std::array buffer{}; + m_line.copy(buffer.data(), buffer.size() - 1); + if (ImGui::InputTextWithHint("##kmline", "91", buffer.data(), buffer.size())) + m_line = line_of(buffer.data()); + item_tooltip("The number of the railway line, as 91: a scenery can hold several lines, each with its own km"); + auto const km_field = [&](char const *Label, char const *Id, double &Value) { + field_row(Label); + std::array text{}; + kilometrage::text(Value).copy(text.data(), text.size() - 1); + if (ImGui::InputText(Id, text.data(), text.size())) + km_of(text.data(), Value); + }; + km_field("Km", "##kmkm", m_km); + item_tooltip("The km of the point, as PKP PLK write it: 100,234 is km 100 and 234 m. At a jump: the km the point is reached at, coming along the rising km"); + field_row("Jump"); + ImGui::Checkbox("##kmjump", &m_jump); + item_tooltip("The km jumps at the point: a line built anew, shortened or lengthened keeps the km of the rest. The irregular hectometre is " + "marked by a black stripe under the digit of the post (Id-1 § 19)"); + if (m_jump) + { + km_field("Km on", "##kmbeyond", m_beyond); + item_tooltip("The km on from the point, along the rising km"); + } + ImGui::EndTable(); + } + auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + auto const fixed{axis != nullptr && axis->anchors.size() > 1 && std::abs(axis->anchors.back().before - axis->anchors.front().before) > 1e-6}; + if (false == fixed) + { + int way{m_against ? 1 : 0}; + if (segmented("##kmway", way, {{0, STR_C("Rise this way"), "The km rise the way the arrow at the point shows"}, {1, STR_C("Rise the other way"), "The km rise against the arrow"}})) + m_against = way == 1; + } + if (m_jump) + if (auto const warning{jump_warning(m_pick->point)}; false == warning.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", warning.c_str()); + if (main_button(m_edited >= 0 ? STR_C("Change the point") : STR_C("Put the point of the km"), ImVec2(-1.0f, tall_button_height()))) + put(); + item_tooltip("The point goes to the file of the scenery with the track, the simulation skips it. One step of undo"); + if (m_edited >= 0) + { + if (ImGui::Button(STR_C("Remove the point"), ImVec2(-1.0f, 0.0f))) + remove(); + item_tooltip("One step of undo"); + } + } + + step_heading(2, STR_C("The way through a station")); + auto const *line_axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + if (false == m_pick.has_value() || m_pick->track == nullptr) + ImGui::TextDisabled("%s", STR_C("Through a station the km run along the straight paths of the switches. Click a track of the station to lead them along it instead")); + else if (line_axis == nullptr) + ImGui::TextDisabled("%s", STR_C("Put a point of the km of the line first")); + else if (auto const via{via_here()}; via >= 0) + { + if (std::find(line_axis->blocked.begin(), line_axis->blocked.end(), static_cast(via)) != line_axis->blocked.end()) + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("The line doesn't run along this track: another track chosen leads it elsewhere")); + else + ImGui::TextUnformatted(format(STR_C("Line %s runs along this track, as chosen"), m_line.c_str()).c_str()); + if (ImGui::Button(STR_C("Back onto the straight paths"), ImVec2(-1.0f, 0.0f))) + drop_via(); + item_tooltip("The track chosen is forgotten, the line runs along the straight paths of the switches here again. One step of undo"); + } + else if (line_axis->runs_along(m_pick->track, m_pick->path)) + ImGui::TextDisabled("%s", format(STR_C("Line %s runs along this track"), m_line.c_str()).c_str()); + else + { + ImGui::TextDisabled("%s", format(STR_C("Line %s doesn't run along this track"), m_line.c_str()).c_str()); + if (ImGui::Button(STR_C("Lead the km along this track"), ImVec2(-1.0f, 0.0f))) + lead_along(); + item_tooltip("The line runs along the track clicked, along the path of the switch clicked, and on along the straight paths of the switches " + "until it meets its tracks again; where it doesn't, as at a junction, it goes on that way. One step of undo"); + } + if (false == m_route_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_route_status.c_str()); + + step_heading(3, STR_C("From the posts")); + if (ImGui::Button(STR_C("Read the km from the posts"), ImVec2(-1.0f, 0.0f))) + read_posts(); + item_tooltip("Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last " + "post of each run numbered by 100 m, a jump where two runs meet. The points of the line along it before give way. One step of undo"); + if (false == m_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + + step_heading(4, STR_C("The points of the lines")); + if (lines.empty()) + { + ImGui::TextDisabled("%s", STR_C("None yet")); + return; + } + for (auto const &line : lines) + { + auto const *axis{kilometrage::axis_of(line)}; + auto const count{static_cast(std::count_if(marks.begin(), marks.end(), [&](kilometrage::mark const &Mark) { return Mark.line == line; }))}; + if (false == ImGui::CollapsingHeader(format("%s %s (%zu)###kmline%s", STR_C("Line"), line.c_str(), count, line.c_str()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + continue; + if (axis == nullptr) + { + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("No track by the points of the line")); + continue; + } + ImGui::TextDisabled("%s", format(STR_C("%.1f km of the axis, %s"), axis->length() / 1000.0, axis->between ? STR_C("between the tracks") : STR_C("along the track")).c_str()); + if (false == axis->sides.empty()) + { + ImGui::TextDisabled("%s", format(STR_P("%zu stretch with the tracks on formations of their own", "%zu stretches with the tracks on formations of their own", axis->sides.size()), axis->sides.size()).c_str()); + item_tooltip("Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by " + "its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)"); + } + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + if (vias[index].line != line) + continue; + auto const blocked{std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end()}; + double km{0.0}; + auto const label{kilometrage::km_at(line, vias[index].point, km, kReadoutReach) ? format(STR_C("Along the track chosen by km %s"), kilometrage::text(km).c_str()) : std::string{STR_C("Along the track chosen")}}; + if (blocked) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f)); + if (ImGui::Selectable((label + "##kmvia" + std::to_string(index)).c_str(), false)) + show_via(index); + if (blocked) + ImGui::PopStyleColor(); + item_tooltip(blocked ? "The line doesn't run along this track: another track chosen leads it elsewhere" : "Through the station the line runs along this track, not along the straight paths of the switches"); + } + auto const rows{axis->anchors.size() + axis->astray.size()}; + if (false == ImGui::BeginTable(("##kmmarks" + line).c_str(), 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(260.0f, ImGui::GetTextLineHeightWithSpacing() * (rows + 1.5f))))) + continue; + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Km"), ImGuiTableColumnFlags_WidthStretch, 0.4f); + ImGui::TableSetupColumn(STR_C("In the scenery"), ImGuiTableColumnFlags_WidthStretch, 0.22f); + ImGui::TableSetupColumn(STR_C("By the km"), ImGuiTableColumnFlags_WidthStretch, 0.22f); + ImGui::TableSetupColumn(STR_C("Off by"), ImGuiTableColumnFlags_WidthStretch, 0.16f); + ImGui::TableHeadersRow(); + auto const growth{axis->growth}; + for (std::size_t i = 0; i < axis->anchors.size(); ++i) + { + auto const &anchor{axis->anchors[i]}; + auto const &mark{marks[anchor.mark]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((mark_text(mark) + "##kmmark" + std::to_string(anchor.mark)).c_str(), static_cast(anchor.mark) == m_edited, ImGuiSelectableFlags_SpanAllColumns)) + show(anchor.mark); + if (i == 0) + continue; + auto const &previous{axis->anchors[i - 1]}; + auto const scenery{anchor.chainage - previous.chainage}; + auto const counted{std::abs((growth > 0 ? anchor.before - previous.after : previous.before - anchor.after) * 1000.0)}; + ImGui::TableNextColumn(); + ImGui::Text("%.0f m", scenery); + ImGui::TableNextColumn(); + ImGui::Text("%.0f m", counted); + ImGui::TableNextColumn(); + auto off{scenery > 1.0 ? (scenery - counted) / scenery * 100.0 : 0.0}; + if (std::abs(off) < 0.05) + off = 0.0; + if (std::abs(off) > 2.0) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%+.1f%%", off); + else + ImGui::Text("%+.1f%%", off); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How much longer the stretch is in the scenery than by the km: the km between the points are stretched to it evenly")); + } + for (auto const index : axis->astray) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f)); + if (ImGui::Selectable((mark_text(marks[index]) + "##kmmark" + std::to_string(index)).c_str(), static_cast(index) == m_edited, ImGuiSelectableFlags_SpanAllColumns)) + show(index); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The point is away from the axis of the line the other points make: the main paths of the switches don't lead to it")); + } + ImGui::EndTable(); + } +} + +void km_tool::draw_overlay() const +{ + if (false == m_open) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + auto const near_enough = [&](glm::dvec3 const &Point, double const Range) { return Global.EditorOrtho || glm::distance(Point, camera) < Range; }; + glm::dvec3 const lift{0.0, kRailHead, 0.0}; + for (auto const &entry : m_drawn) + { + // the axis, faint where the km run on past the last point + for (std::size_t i = 1; i < entry.points.size(); i += 5) + { + auto const next{std::min(i + 4, entry.points.size() - 1)}; + if (near_enough(entry.points[next], kOverlayRange)) + projection.line(drawlist, entry.points[i - 1] + lift, entry.points[next] + lift, entry.guessed[next] != 0 ? kGuessed : kAxis, Global.EditorOrtho ? 4.0f : 2.5f); + } + for (auto const &tick : entry.ticks) + { + if (false == near_enough(tick.at, kTickRange)) + continue; + auto const centre{tick.at + lift}; + auto const whole{tick.hectometres % 10 == 0}; + auto const reach{tick.jump ? 3.5 : whole ? 2.6 : 1.4}; + projection.line(drawlist, centre - tick.across * reach, centre + tick.across * reach, tick.jump ? kJump : whole ? kKm : kTick, tick.jump || whole ? 3.5f : 2.0f); + if (tick.jump) + continue; + ImVec2 screen; + if (whole && near_enough(tick.at, kLabelRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.6, 0.0}, screen)) + draw_label(drawlist, screen, IM_COL32(0, 0, 0, 170), kKm, format("km %d", tick.hectometres / 10)); + else if (false == whole && near_enough(tick.at, kDigitRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.4, 0.0}, screen)) + draw_label(drawlist, screen, IM_COL32(0, 0, 0, 140), kTick, std::to_string(std::abs(tick.hectometres) % 10)); + } + } + // the points of the km: a pole with the km over it, the arrow the way the km rise + auto const &marks{kilometrage::marks()}; + for (std::size_t index = 0; index < marks.size(); ++index) + { + auto const &mark{marks[index]}; + if (false == near_enough(mark.point, kLabelRange * 2.0)) + continue; + auto astray{true}; + for (auto const &entry : m_drawn) + if (entry.line == mark.line) + if (auto const *axis{kilometrage::axis_of(mark.line)}; axis != nullptr) + astray = std::find(axis->astray.begin(), axis->astray.end(), index) != axis->astray.end(); + auto const colour{astray ? kAstray : mark.beyond.has_value() ? kJump : kMark}; + auto const foot{mark.point + lift}; + auto const top{foot + glm::dvec3{0.0, 3.0, 0.0}}; + projection.line(drawlist, foot, top, colour, static_cast(index) == m_edited ? 4.0f : 2.5f); + draw_arrow(projection, drawlist, foot, mark.way, 6.0, colour); + ImVec2 screen; + if (projection.project(top, screen)) + draw_label(drawlist, screen, colour, IM_COL32(16, 18, 22, 255), mark.line + ": " + mark_text(mark)); + } + // the tracks chosen for the way through a station: a rhombus on the track + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + auto const &via{vias[index]}; + ImVec2 screen; + if (false == near_enough(via.point, kLabelRange * 2.0) || false == projection.project(via.point + lift, screen)) + continue; + auto blocked{false}; + if (auto const *axis{kilometrage::axis_of(via.line)}; axis != nullptr) + blocked = std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end(); + auto const colour{blocked ? kAstray : kAxis}; + auto const r{8.0f}; + drawlist->AddQuadFilled(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), colour); + drawlist->AddQuad(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), IM_COL32(16, 18, 22, 255), 1.5f); + draw_label(drawlist, ImVec2(screen.x, screen.y - r - 2.0f), colour, IM_COL32(16, 18, 22, 255), format(STR_C("%s: this way"), via.line.c_str())); + } + if (m_pick.has_value() && m_edited < 0) + { + auto const foot{m_pick->point + lift}; + ImVec2 screen; + if (projection.project(foot, screen)) + drawlist->AddCircle(screen, 9.0f, kKm, 20, 2.5f); + draw_arrow(projection, drawlist, foot, rising(), 8.0, kKm); + } +} + +editor_selftest_result km_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "kmpick") + { + // kmpick track= [at=<0..1>]: a click on the track + auto const values{selftest_values(Words)}; + auto const found{values.find("track")}; + auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; + if (track == nullptr) + { + WriteLog("SELFTEST kmpick: no such track"); + return editor_selftest_result::handled; + } + auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; + auto const path{values.count("path") ? std::atoi(values.at("path").c_str()) : 0}; + pick(*track, path > 0 && path < static_cast(track->m_paths.size()) ? geometry::bezier{track->m_paths[path]}.point(at) : editor_track::point_at(*track, at)); + auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f path %d along %d via %d", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z, + m_pick->path, axis != nullptr && axis->runs_along(m_pick->track, m_pick->path) ? 1 : 0, via_here())); + return editor_selftest_result::handled; + } + if (Command == "kmset") + { + // kmset line= km= [beyond=] [against=1]: the fields set and the point put + auto const values{selftest_values(Words)}; + if (auto const line{values.find("line")}; line != values.end()) + m_line = line->second; + if (auto const km{values.find("km")}; km != values.end()) + km_of(km->second.c_str(), m_km); + m_jump = values.count("beyond") > 0; + if (m_jump) + km_of(values.at("beyond").c_str(), m_beyond); + if (auto const against{values.find("against")}; against != values.end()) + m_against = against->second == "1"; + put(); + WriteLog("SELFTEST kmset: " + m_status); + return editor_selftest_result::handled; + } + if (Command == "kmvia") + { + // kmvia [line=] [remove=1]: the line led along the track picked, or back onto the straight paths there + auto const values{selftest_values(Words)}; + if (auto const line{values.find("line")}; line != values.end()) + m_line = line->second; + if (values.count("remove") > 0) + drop_via(); + else + lead_along(); + auto text{m_route_status}; + std::replace(text.begin(), text.end(), '\n', ';'); + WriteLog("SELFTEST kmvia: " + text); + return editor_selftest_result::handled; + } + if (Command == "kmremove") + { + remove(); + WriteLog("SELFTEST kmremove: " + m_status); + return editor_selftest_result::handled; + } + if (Command == "kmposts") + { + // kmposts line=: the km read from the posts along the line through the track picked + auto const values{selftest_values(Words)}; + if (auto const line{values.find("line")}; line != values.end()) + m_line = line->second; + read_posts(); + WriteLog("SELFTEST kmposts: " + m_status); + return editor_selftest_result::handled; + } + if (Command == "kmstate") + { + WriteLog(format("SELFTEST kmstate: picked %d line %s km %s jump %d edited %d", m_pick.has_value() ? 1 : 0, m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_edited)); + return editor_selftest_result::handled; + } + if (Command == "kmshow") + { + // kmshow mark=: the point of the km picked, the camera over it + auto const values{selftest_values(Words)}; + auto const index{values.count("mark") ? std::atoi(values.at("mark").c_str()) : 0}; + show(static_cast(std::max(index, 0))); + WriteLog(format("SELFTEST kmshow %d: line %s km %s edited %d", index, m_line.c_str(), kilometrage::text(m_km).c_str(), m_edited)); + return editor_selftest_result::handled; + } + if (Command == "kmlist") + { + refresh(); + for (auto const &entry : m_drawn) + { + std::string list; + for (std::size_t i = 0; i < entry.ticks.size() && entry.points.size() < 2000; ++i) + list += format(" %d@%.0f,%.0f", entry.ticks[i].hectometres, entry.ticks[i].at.x, entry.ticks[i].at.z); + WriteLog(format("SELFTEST kmlist ticks %s: %zu points, %zu ticks", entry.line.c_str(), entry.points.size(), entry.ticks.size()) + list); + } + auto const &marks{kilometrage::marks()}; + WriteLog(format("SELFTEST kmlist: %zu points", marks.size())); + for (auto const &line : kilometrage::lines()) + { + auto const *axis{kilometrage::axis_of(line)}; + if (axis == nullptr) + { + WriteLog("SELFTEST kmlist line " + line + ": no axis"); + continue; + } + std::string list; + for (auto const &anchor : axis->anchors) + list += format(" %s@%.0f/%.1f", mark_text(marks[anchor.mark]).c_str(), anchor.chainage, anchor.gap); + for (auto const &side : axis->sides) + WriteLog(format("SELFTEST kmlist apart: %.1f m of track by %.1f m of axis (%.1f..%.1f), km %s..%s", side.length, side.to - side.from, side.from, side.to, kilometrage::text(kilometrage::km_on(*axis, side, 0.0)).c_str(), + kilometrage::text(kilometrage::km_on(*axis, side, side.length)).c_str())); + WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, %zu blocked, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), axis->blocked.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(), + kilometrage::text(kilometrage::km_on(*axis, axis->length())).c_str()) + + list); + } + return editor_selftest_result::handled; + } + if (Command == "kmat") + { + // kmat track= [at=<0..1>]: the km of the lines there + auto const values{selftest_values(Words)}; + auto const found{values.find("track")}; + auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; + if (track == nullptr) + { + WriteLog("SELFTEST kmat: no such track"); + return editor_selftest_result::handled; + } + auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; + auto text{readout(editor_track::point_at(*track, at))}; + std::replace(text.begin(), text.end(), '\n', ';'); + WriteLog(format("SELFTEST kmat %s %.2f: ", track->name().c_str(), at) + (text.empty() ? std::string{"-"} : text)); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodekm.h b/application/editormodekm.h new file mode 100644 index 00000000..2e22bafb --- /dev/null +++ b/application/editormodekm.h @@ -0,0 +1,102 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editortool.h" +#include "editor/editorKilometrage.hpp" + +#include +#include +#include +#include + +// the kilometrage of the lines: the points of a line where its km is known and where it jumps, the km of the rest from them +class km_tool final : public editor_tool +{ + public: + km_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates) + : m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates} + { + } + void update() override; + void draw_overlay() const override; + void render_options() override; + void forget(scene::basic_node const *Node) override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + void set_open(bool const Open) { m_open = Open; } + // the point of the track clicked: a new point of the km there, or the one standing by it + void pick(TTrack &Track, glm::dvec3 const &Point); + // the km of the lines at the point, a line of text each, empty where none reaches + std::string readout(glm::dvec3 const &Point) const; + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_templates &m_templates; + bool m_open{false}; + struct picked + { + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; // on the axis of the track + glm::dvec2 along{0.0, 1.0}; // of the track there, in the plan + int path{0}; // of the switch, the one nearest to the click + glm::dvec3 on{0.0}; // the point of it nearest to the click + }; + std::optional m_pick; + int m_edited{-1}; // of the marks, the one standing by the point picked, -1 for a new one + std::string m_line; + double m_km{0.0}; + bool m_jump{false}; + double m_beyond{0.0}; + bool m_against{false}; // the km rise against the track picked + std::string m_status; + std::string m_route_status; // of the way through a station + // what the overlay draws, made again when the axes change + struct tick + { + glm::dvec3 at{0.0}; + glm::dvec3 across{0.0}; // in the plan, a unit + int hectometres{0}; + bool jump{false}; + }; + struct drawn + { + std::string line; + std::vector points; + std::vector guessed; // of the points, beyond the last point of the km: the km run on from it + std::vector ticks; + }; + std::vector m_drawn; + std::size_t m_revision{0}; + bool m_drawn_once{false}; + + void refresh(); + void put(); + void remove(); + // the point of the km picked and the camera over it + void show(std::size_t const Index); + // the line of the field led along the track picked, along the path of the switch picked, instead of the straight paths of the + // switches; one step of the history + void lead_along(); + // the line back on the straight paths of the switches by the track picked; one step of the history + void drop_via(); + // of the vias of the line of the field, the one on the track picked, -1 for none + int via_here() const; + // the track of the via picked and the camera over it + void show_via(std::size_t const Index); + // the hectometre posts standing along the line through the track picked read into points of the km: the first and the last + // post of each run of them numbered by 100 m, a jump where the runs meet; one step of the history + void read_posts(); + // the way the km rise at the point picked, in the plan + glm::dvec2 rising() const; + // warnings of a jump at the point: Id-1 § 19 wants none on a curve nor at a station + std::string jump_warning(glm::dvec3 const &Point) const; +}; diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index d66b7608..f66a831e 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -10,6 +10,7 @@ http: #include "stdafx.h" #include "application/editormode.h" +#include "editor/editorSettings.hpp" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" @@ -17,6 +18,7 @@ http: #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorGroundFollow.hpp" #include "editor/editorIncludeInfo.hpp" #include "utilities/Globals.h" @@ -44,6 +46,89 @@ http: using namespace track_mode; +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)); +} + +// the curve of the track of a free end the line laid goes on from at its start, or into at its end: its radius, the side it turns to +// as the line runs and its cant leaning into the turn; nothing on a straight +void adjoining_curve(editor_track::snap_target const &End, bool const Atend, bool const Traced, double const Gauge, alignment::design &Design) +{ + if (false == goes_on_curved(End, Traced)) + return; + auto const bend{editor_track::bend_at(*End.track, End.position)}; + if (false == bend.has_value() || std::abs(bend->curvature) < 1e-4) + return; + // the curvature as the track goes on beyond its end: the way the line runs at its start, against it at its end + auto const curvature{Atend ? -bend->curvature : bend->curvature}; + auto const cant{Gauge * std::sin(glm::radians(bend->roll)) * (bend->curvature > 0.0 ? 1.0 : -1.0)}; + (Atend ? Design.end_radius : Design.start_radius) = 1.0 / std::abs(curvature); + (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 + void editor_mode::update_build_tools() { glm::dvec3 const ground{cursor_ground()}; @@ -65,42 +150,88 @@ void editor_mode::update_build_tools() m_extend.mouse = cursor_terrain(m_extend.point.y); m_extend.snap = m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; if (m_extend.snap.track == nullptr) - m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); + m_extend.mouse.y = EditorSettings.heights() == editorSettings::lay_heights::level ? m_extend.point.y : bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); joining(m_extend.snap); } if (m_switch.placing) + { m_switch.mouse = cursor_level(m_switch.point.y); + // a switch with no track takes the direction of its main track once the drag is long enough, dragged sideways after that it + // takes the side; back by its start the direction is free again + if (auto &tool{m_switch}; tool.free && tool.free_end.track == nullptr) + { + auto const offset{plan_of(tool.mouse - tool.point)}; + if (glm::length(offset) < kFreeSwitchRelease) + tool.free_heading.reset(); + else if (false == tool.free_heading.has_value() && glm::length(offset) >= kFreeSwitchLock) + tool.free_heading = glm::normalize(offset); + } + } auto &lay{m_lay}; lay.preview.clear(); lay.preview_error.clear(); 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(); + lay.plan = {}; if (lay.points.empty()) return; + // the cursor is the next point, in the mode it would be clicked in auto points{lay.points}; + auto modes{lay.modes}; auto end{lay.mouse_snap}; if (end.track != nullptr && glm::distance(end.position, points.front()) > 1.0) + { points.push_back(end.position); + modes.push_back(lay.mode); + } else { end = {}; if (plan_distance(lay.mouse, points.back()) > 1.0) + { points.push_back(lay.mouse); + modes.push_back(lay.mode); + } + } + 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); } - lay.preview = lay_pieces(points, end, lay.preview_length, lay.preview_error); } bool editor_mode::free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool) @@ -219,6 +350,21 @@ editor_track::snap_target editor_mode::snap_free_end(glm::dvec3 const &Near, glm return result; } +std::vector const &editor_mode::free_end_points(glm::dvec3 const &Centre, double const Reach) const +{ + auto &cache{m_free_end_cache}; + if (cache.edits != m_track_edits || glm::distance(cache.centre, Centre) > cache.reach * 0.25 || Reach > cache.reach * 1.01 || Reach < cache.reach * 0.5) + { + cache.edits = m_track_edits; + cache.centre = Centre; + cache.reach = Reach * 1.25; + cache.points.clear(); + for (auto const &end : editor_track::free_ends(nullptr, kRailCategory, Centre, cache.reach, {})) + cache.points.push_back(end.position); + } + return cache.points; +} + void editor_mode::draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const { screen_projection const projection; @@ -265,7 +411,7 @@ void editor_mode::handle_drag_start(glm::dvec3 const &Anchor) m_handle_drag.offset = {Anchor.x - level.x, 0.0, Anchor.z - level.z}; } -bool editor_mode::handle_drag_step(glm::dvec3 &Position) +bool editor_mode::handle_drag_step(glm::dvec3 &Position, TTrack const *Ground) { auto &drag{m_handle_drag}; auto const &io{ImGui::GetIO()}; @@ -283,7 +429,13 @@ bool editor_mode::handle_drag_step(glm::dvec3 &Position) screen_projection const projection; glm::dvec3 level; if (projection.on_level(io.MousePos, drag.height, level) && (Global.editor_tracks || glm::distance(level, glm::dvec3{Global.pCamera.Pos}) <= static_cast(kMaxPlacementDistance))) + { drag.position = level + drag.offset; + if (Ground != nullptr && false == io.KeyShift) + drag.position.y = bed_height(drag.position, {0.0, 0.0}, Ground); + else + drag.position.y = drag.height; + } Position = drag.position; return true; } @@ -351,12 +503,20 @@ void editor_mode::point_drag_update() for (auto *dragged : m_track_drag) states.emplace_back(dragged, editor_track::capture(*dragged)); push_track_snapshot(std::move(states)); + drag.step = m_history.size(); records_watch_begin(); } screen_projection const projection; glm::dvec3 position; if (false == projection.on_level(io.MousePos, drag.height, position) || (false == Global.editor_tracks && glm::distance(position, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) return; + // an end goes onto the ground at the height of the ballast, as a point laid; with Shift it keeps its height + if (editor_track::is_end(m_track_point.kind) && false == io.KeyShift) + { + auto const &path{track->m_paths[m_track_point.path]}; + auto const tangent{geometry::bezier{path}.first(m_track_point.kind == editor_track::point_kind::end ? 1.0 : 0.0)}; + position.y = bed_height(position, plan_of(tangent), track); + } m_track_snap = {}; if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) { @@ -391,8 +551,47 @@ void editor_mode::point_drag_finish() m_track_joints.clear(); } +std::optional editor_mode::eased_layout(TTrack const *From, glm::dvec3 const &Point, glm::dvec2 const &Direction, glm::dvec3 const &Target, double const Radius) const +{ + m_eased_last.reset(); + // from the end of a curved track the line goes on from its curvature: a curve from it, a straight after a transition out of it, never + // with a jump of the curvature at the end; at the diverging track of a switch a curve from it, a straight right from its end, as the + // switch goes on with a straight + if (From == nullptr || (From->eType != tt_Normal && (From->eType != tt_Switch || m_track_layout == track_layout::straight))) + return std::nullopt; + auto const end{editor_track::bend_at(*From, Point)}; + if (false == end.has_value()) + return std::nullopt; + alignment::lay_request request; + request.start.position = Point; + request.start.direction = Direction; + request.start.roll = end->roll; + request.curvature = end->curvature; + request.target = Target; + switch (m_track_layout) + { + case track_layout::straight: request.kind = alignment::lay_kind::straight; break; + case track_layout::arc: request.kind = alignment::lay_kind::arc; break; + case track_layout::straight_arc: request.kind = alignment::lay_kind::straight_arc; break; + default: request.kind = alignment::lay_kind::arc_straight; break; + } + request.radius = Radius; + request.speed = design_speed(From, nullptr); + request.piece_length = m_lay.piece_length; + request.norms = laying_norms(); + // the diverging track of a switch has no cant: the line out of its curve is laid for the speed the curve lets with the unbalanced + // acceleration alone, as ease_out_of does + if (From->eType == tt_Switch && std::abs(end->curvature) > 1e-9) + request.speed = std::min(request.speed, std::sqrt(12.96 * request.norms.unbalanced / std::abs(end->curvature))); + if (false == m_eased_cache.has_value() || m_eased_cache->first != request) + m_eased_cache.emplace(request, alignment::lay_on(request)); + m_eased_last = m_eased_cache->second; + return m_eased_last; +} + std::vector editor_mode::extend_pieces() const { + m_eased_last.reset(); std::vector result; auto const &tool{m_extend}; editor_track::snap_target target; @@ -410,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]}; @@ -417,43 +619,137 @@ 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 pieces{editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; + 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); return pieces; } +std::vector editor_mode::extend_preview() const +{ + 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 +{ + if (m_lay.own_speed && m_lay.style.velocity > 0.0) + return nullptr; + TTrack const *result{nullptr}; + for (auto const *track : {From, To}) + if (track != nullptr && editor_track::velocity(*track) > 0.0 && (result == nullptr || editor_track::velocity(*track) < editor_track::velocity(*result))) + result = track; + return result; +} + double editor_mode::design_speed(TTrack const *From, TTrack const *To) const { - double speed{0.0}; - for (auto const *track : {From, To}) + if (auto const *track{speed_track(From, To)}; track != nullptr) + return editor_track::velocity(*track); + return m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; +} + +std::pair editor_mode::laying_ends() const +{ + if (m_extend.active) { - if (track == nullptr) - continue; - auto const velocity{editor_track::velocity(*track)}; - if (velocity > 0.0) - speed = speed > 0.0 ? std::min(speed, velocity) : velocity; + editor_track::snap_target target; + return {m_extend.track, extend_snap(target) ? target.track : nullptr}; } - if (speed <= 0.0) - speed = m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; - return speed; + if (m_lay.active && false == m_lay.points.empty()) + return {m_lay.start.track, m_lay.points.size() == 1 ? m_lay.mouse_snap.track : nullptr}; + return {nullptr, nullptr}; +} + +std::string editor_mode::design_speed_of(TTrack const *From, TTrack const *To) const +{ + auto const speed{design_speed(From, To)}; + if (auto const *track{speed_track(From, To)}; track != nullptr) + return format(STR_C("design speed %.0f km/h, that of the track %s"), speed, name_of(*track).c_str()); + return format(STR_C("design speed %.0f km/h, set for the laying"), speed); +} + +std::vector editor_mode::switch_exit_warnings(TTrack const *From, glm::dvec3 const &Start, TTrack const *To, glm::dvec3 const &End, std::vector const &Pieces) const +{ + auto const *switched{From != nullptr && From->eType == tt_Switch ? From : To != nullptr && To->eType == tt_Switch ? To : nullptr}; + if (switched == nullptr || Pieces.empty()) + 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) { 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; + for (auto const &path : switched->m_paths) + for (auto const atend : {false, true}) + { + if (false == back.empty() || glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], joint) > 0.05) + continue; + std::vector pieces{path.pieces.empty() ? std::vector{path} : path.pieces}; + if (atend) + std::reverse(pieces.begin(), pieces.end()); + for (auto const &piece : pieces) + back.push_back({bezier{piece}.plan_length(), atend ? curvature_at(piece, 1.0) : -curvature_at(piece, 0.0), atend ? curvature_at(piece, 0.0) : -curvature_at(piece, 1.0)}); + } + // the curve the switch ends with, and the straight of a straight frog after it + double straight{0.0}; + std::optional curvature; + for (auto const &[length, inner, outer] : back) + { + if (std::abs(inner) > 1e-4 || std::abs(outer) > 1e-4) + { + curvature = inner; + break; + } + straight += length; + } + if (false == curvature.has_value()) + return {}; + // the pieces laid as the line runs on from the switch + std::vector> line; + if (switched == From) + for (auto const &piece : Pieces) + line.push_back({bezier{piece}.plan_length(), curvature_at(piece, 0.0), curvature_at(piece, 1.0)}); + else + for (auto piece{Pieces.rbegin()}; piece != Pieces.rend(); ++piece) + line.push_back({bezier{*piece}.plan_length(), -curvature_at(*piece, 1.0), -curvature_at(*piece, 0.0)}); + // no faster than the diverging track of the switch lets + auto const speed{std::min(design_speed(From, To), editor_track::diverging_speed(1.0 / std::abs(*curvature)))}; + return switch_curve::check_exit(*curvature, straight, line, speed, laying_norms().gauge).warnings; +} + +void editor_mode::give_own_speed(std::vector const &Created) const +{ + if (false == m_lay.own_speed || m_lay.style.velocity <= 0.0) + return; + for (auto *track : Created) + editor_track::velocity(*track, m_lay.style.velocity); } std::vector editor_mode::join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, @@ -468,17 +764,17 @@ 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 = m_lay.transitions; request.speed = design_speed(From, End.track); - request.kink = m_joints.angle * 0.5; + request.kink = m_joints.kink_angle() * 0.5; request.shape = m_route.design.shape; request.norms = laying_norms(); auto result{alignment::join(request)}; - if (m_lay.transitions && result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) + if (result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) { auto const grade{(End.position.y - Start.y) / std::max(1.0, plan_distance(Start, End.position))}; - auto const in{From != nullptr && From->eType == tt_Normal ? editor_track::ease_out_of(*From, Start, grade, request.speed, request.norms) : editor_track::easing{}}; - auto const out{End.track != nullptr && End.track->eType == tt_Normal ? editor_track::ease_out_of(*End.track, End.position, -grade, request.speed, request.norms) : editor_track::easing{}}; + auto const curved = [](TTrack const *Track) { return Track != nullptr && (Track->eType == tt_Normal || Track->eType == tt_Switch); }; + auto const in{curved(From) ? editor_track::ease_out_of(*From, Start, grade, request.speed, request.norms) : editor_track::easing{}}; + auto const out{curved(End.track) ? editor_track::ease_out_of(*End.track, End.position, -grade, request.speed, request.norms) : editor_track::easing{}}; if (auto eased{eased_join(request, in, out)}) result = std::move(*eased); } @@ -548,17 +844,24 @@ void editor_mode::finish_extend() m_extend.active = false; if (pieces.empty() || style == nullptr) return; + // the track laid takes only its heights from the ground: the terrain is shaped under it by the buttons of the terrain model + m_nmt_state.laying = true; std::vector created; for (auto const &piece : pieces) created.push_back(editor_track::create_path(*style, piece)); + give_own_speed(created); editor_track::commit(created); - digitize_drawn(created); + auto const from{m_history.size()}; push_track_snapshot({}, created); first_step_done(first_step::laid); if (target.track != nullptr) first_step_done(first_step::join); + if (auto const warnings{switch_exit_warnings(style, pieces.front().points[segment_data::point::start], target.track, target.position, pieces)}; false == warnings.empty()) + ui()->set_status(lines_of(warnings), true); m_node = created.back(); ui()->set_node(m_node); + nmt_fit_laid(from, created.back()->m_paths.front().points[segment_data::point::end]); + m_nmt_state.laying = false; straight_refresh(); } @@ -585,7 +888,6 @@ void editor_mode::extend_add(int const Side) for (auto const &piece : pieces) created.push_back(editor_track::create_path(*tool.track, piece)); editor_track::commit(created); - digitize_drawn(created); push_track_snapshot({}, created); m_node = created.back(); ui()->set_node(m_node); @@ -636,66 +938,6 @@ void editor_mode::render_extend_ui() extend_add(-1); } -// the shapes laid from a free end as four tiles, each with a sketch: the end of the track, what's laid from it, the cursor -void editor_mode::render_layout_combo() -{ - char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; - auto const current{static_cast(m_track_layout)}; - ImGui::TextDisabled("%s", STR_C("Shape from a free end")); - auto const font{ImGui::GetFontSize()}; - auto const gap{std::round(font * 0.3f)}; - auto const width{std::floor((ImGui::GetContentRegionAvail().x - 3.0f * gap) * 0.25f)}; - auto const height{std::round(font * 2.4f)}; - auto *list{ImGui::GetWindowDrawList()}; - auto const accent{family_colour(0)}; - for (int i = 0; i < 4; ++i) - { - if (i > 0) - ImGui::SameLine(0.0f, gap); - ImGui::PushID(i); - auto const top{ImGui::GetCursorScreenPos()}; - if (ImGui::InvisibleButton("##shape", ImVec2(width, height))) - m_track_layout = static_cast(i); - auto const hovered{ImGui::IsItemHovered()}; - if (hovered) - ImGui::SetTooltip("%s\n%s", STR_C(names[i]), STR_C("While laying from a free end: Shift+wheel")); - ImGui::PopID(); - auto const chosen{i == current}; - ImVec2 const bottom{top.x + width, top.y + height}; - auto const rounding{std::round(font * 0.35f)}; - list->AddRectFilled(top, bottom, chosen ? (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 46) : ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding); - list->AddRect(top, bottom, chosen ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, chosen ? 2.0f : 1.0f); - auto const pad{std::round(font * 0.45f)}; - auto const at = [&](float const X, float const Y) { return ImVec2(top.x + pad + X * (width - 2.0f * pad), top.y + pad + Y * (height - 2.0f * pad)); }; - auto const laid{chosen ? accent : ImGui::GetColorU32(ImGuiCol_Text, 0.7f)}; - list->AddLine(at(0.0f, 0.85f), at(0.18f, 0.85f), ImGui::GetColorU32(ImGuiCol_TextDisabled), 3.0f); - switch (i) - { - case 0: - list->AddBezierQuadratic(at(0.18f, 0.85f), at(0.4f, 0.85f), at(0.61f, 0.6f), laid, 2.0f); - list->AddLine(at(0.61f, 0.6f), at(1.0f, 0.1f), laid, 2.0f); - break; - case 1: - list->AddLine(at(0.18f, 0.85f), at(0.45f, 0.85f), laid, 2.0f); - list->AddBezierQuadratic(at(0.45f, 0.85f), at(0.85f, 0.85f), at(1.0f, 0.1f), laid, 2.0f); - break; - case 2: list->AddBezierQuadratic(at(0.18f, 0.85f), at(0.7f, 0.85f), at(1.0f, 0.1f), laid, 2.0f); break; - default: - list->AddLine(at(0.18f, 0.85f), at(1.0f, 0.85f), laid, 2.0f); - for (float y{0.15f}; y < 0.8f; y += 0.2f) - list->AddLine(at(1.0f, y), at(1.0f, y + 0.1f), IM_COL32(255, 210, 60, 160), 1.0f); - break; - } - list->AddCircleFilled(at(0.18f, 0.85f), 2.5f, IM_COL32(230, 80, 230, 255)); - auto const cursor{at(1.0f, 0.1f)}; - list->AddLine(ImVec2(cursor.x - 4.0f, cursor.y), ImVec2(cursor.x + 4.0f, cursor.y), IM_COL32(255, 210, 60, 255), 1.5f); - list->AddLine(ImVec2(cursor.x, cursor.y - 4.0f), ImVec2(cursor.x, cursor.y + 4.0f), IM_COL32(255, 210, 60, 255), 1.5f); - } - ImGui::TextUnformatted(STR_C(names[current])); - ImGui::SameLine(); - ImGui::TextDisabled("%s", STR_C("(Shift+wheel)")); -} - int editor_mode::wheel_step(double const Offset, bool const Gesture) { auto const &io{ImGui::GetIO()}; @@ -767,6 +1009,21 @@ bool editor_mode::track_wheel(double const Offset) return radii[0]; }; first_step_done(io.KeyCtrl ? first_step::wheel : first_step::shape); + if (io.KeyCtrl && io.KeyShift) + { + static double const speeds[]{20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 120.0, 140.0, 160.0, 200.0, 250.0}; + auto const [from, to]{laying_ends()}; + auto const current{design_speed(from, to)}; + int index{0}; + for (int i = 0; i < static_cast(std::size(speeds)); ++i) + if (speeds[i] <= current + 1e-6) + index = i; + if (step < 0 && speeds[index] < current - 1e-6) + ++index; + m_lay.style.velocity = speeds[std::clamp(index + step, 0, static_cast(std::size(speeds)) - 1)]; + m_lay.own_speed = true; + return true; + } if (io.KeyCtrl) { if (false == joined) @@ -781,49 +1038,43 @@ bool editor_mode::track_wheel(double const Offset) } if (joined) m_join_variant += step; - else - m_track_layout = static_cast((static_cast(m_track_layout) + 4 - step) % 4); return true; } -std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const +std::vector editor_mode::lay_pieces(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, double &Length, std::string &Error, + lay_line_plan *Plan) const { auto const &tool{m_lay}; Length = 0.0; + m_eased_last.reset(); if (Points.size() < 2) return {}; - if (Points.size() == 2) + auto const unknown{std::numeric_limits::quiet_NaN()}; + if (Points.size() == 2 && End.track != nullptr) { - auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; - if (glm::length(leaving) < 1e-6) - { - Error = STR_C("Two points lie on one another"); - return {}; - } - if (End.track != nullptr && tool.start.track != nullptr) - return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); - m_join_last = {}; - std::vector pieces; + // two free ends: the curve between them is the one the join finds if (tool.start.track != nullptr) { - auto const grade{-grade_of(tool.start.direction)}; - pieces = editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); - Error = narrowed(pieces, m_track_layout, tool.radius); - follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN(), tool.start.track); + auto const leaving{-plan_of(tool.start.direction)}; + if (glm::length(leaving) < 1e-6) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); } - else if (End.track == nullptr) + // from a point to a free end: the shape of the layout laid back from the end + m_join_last = {}; + std::vector pieces; + if (glm::length(plan_of(End.direction)) > 1e-6) { - auto const length{glm::length(leaving)}; - pieces = straight_piece(Points[0], glm::normalize(leaving), (Points[1].y - Points[0].y) / length, length); - follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), Points[1].y, std::numeric_limits::quiet_NaN(), tool.copy_selected ? selected_track() : nullptr); - } - else if (glm::length(plan_of(End.direction)) > 1e-6) - { - auto const back{editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; + auto const eased{eased_layout(End.track, End.position, glm::normalize(-plan_of(End.direction)), Points[0], tool.radius)}; + auto const laid{eased.has_value() && false == eased->pieces.empty()}; + auto const back{laid ? eased->pieces : editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) pieces.push_back(reversed(*piece)); - Error = narrowed(pieces, m_track_layout, tool.radius); - follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); + Error = laid ? narrowed(m_track_layout, tool.radius, eased->radius) : narrowed(pieces, m_track_layout, tool.radius); + follow_ground(pieces, Points[0].y, unknown, End.position.y, grade_of(End.direction), End.track); } for (auto const &piece : pieces) Length += bezier{piece}.plan_length(); @@ -831,26 +1082,21 @@ std::vector editor_mode::lay_pieces(std::vector const Error = STR_C("The track can't be laid through these points"); return pieces; } - auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; - auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; - if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) - { - Error = STR_C("Two points lie on one another"); + m_join_last = {}; + lay_line_plan plan; + if (false == lay_line(Points, Modes, End, plan, Error)) return {}; - } - alignment::design design; - auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)}; - if (false == shape.valid) - { - Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front(); - return {}; - } + auto const &design{plan.design}; + auto const &shape{plan.shape}; Error = lines_of(shape.warnings); auto const count = [&](alignment::result const &Shape, alignment::design const &Design) { return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); }; - // 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 + std::vector pieces; + // 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 (Joint.track == nullptr || Joint.track->eType != tt_Normal) return editor_track::easing{}; @@ -860,98 +1106,765 @@ std::vector editor_mode::lay_pieces(std::vector const result.pieces.clear(); return result; }; - auto const in{tool.transitions ? eased(tool.start, 0.0, 1.0) : editor_track::easing{}}; - auto const out{tool.transitions ? eased(End, shape.length, -1.0) : editor_track::easing{}}; + auto const in{eased(tool.start, 0.0, 1.0)}; + auto const out{eased(End, shape.length, -1.0)}; if ((false == in.pieces.empty() || false == out.pieces.empty()) && editor_track::eases_room(in, out, Points.front(), design.start_direction, Points.back(), design.end_direction)) { - auto points{Points}; - auto starting{design.start_direction}; - auto ending{design.end_direction}; + auto easeddesign{design}; + // an end moved out of the curve of the adjoining track is on a straight if (false == in.pieces.empty()) { - points.front() = in.end; - starting = in.direction; + easeddesign.start = in.end; + easeddesign.start_direction = in.direction; + easeddesign.start_curved = false; + easeddesign.start_radius = 0.0; + easeddesign.start_turn = 0; } if (false == out.pieces.empty()) { - points.back() = out.end; - ending = -out.direction; + easeddesign.end = out.end; + easeddesign.end_direction = -out.direction; + easeddesign.end_curved = false; + easeddesign.end_radius = 0.0; + easeddesign.end_turn = 0; } - alignment::design easeddesign; - auto const easedshape{lay_shape(points, starting, ending, easeddesign)}; + // a straight with no bend, clicked on from the end the way it went: after the transition it runs on the way the transition leaves, + // as far as the point clicked + if (false == in.pieces.empty() && out.pieces.empty() && End.track == nullptr && easeddesign.vertices.empty()) + { + auto const along{glm::normalize(in.direction)}; + auto const distance{glm::dot(plan_of(easeddesign.end - in.end), along)}; + easeddesign.end = {in.end.x + along.x * distance, easeddesign.end.y, in.end.z + along.y * distance}; + easeddesign.end_direction = along; + } + // the bends stay where they are, measured from the ends moved + if (easeddesign.vertices.size() >= 2 && shape.vertices.size() == easeddesign.vertices.size()) + { + easeddesign.vertices.front().offset = std::max(0.1, glm::dot(shape.vertices.front() - plan_of(easeddesign.start), glm::normalize(easeddesign.start_direction))); + easeddesign.vertices.back().offset = std::max(0.1, glm::dot(plan_of(easeddesign.end) - shape.vertices.back(), glm::normalize(easeddesign.end_direction))); + } + auto const easedshape{alignment::ease_corners(easeddesign, plan.corners, {EditorSettings.corner_low(), EditorSettings.corner_high()}, bends_fit())}; if (easedshape.valid) { Length = editor_track::eased_length(in, easedshape.length, out); Error = lines_of(easedshape.warnings); - return editor_track::eased_pieces(in, alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign)), out); + pieces = editor_track::eased_pieces(in, alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign)), out); + plan.design = easeddesign; + plan.shape = easedshape; } - Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front()); + else + Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front()); } - Length = shape.length; - return alignment::pieces(shape, design, count(shape, design)); + if (pieces.empty()) + { + Length = shape.length; + pieces = alignment::pieces(shape, design, count(shape, design)); + // a line lay_line begins at the end of the transition out of the curve of the free end keeps the transition before it + 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{}}; + Length = editor_track::eased_length(before, Length, beyond); + pieces = editor_track::eased_pieces(before, pieces, beyond); + } + // the heights: fitted to the ground, or from point to point through the points clicked + TTrack const *style{tool.start.track != nullptr ? tool.start.track : End.track != nullptr ? End.track : tool.copy_selected ? selected_track() : nullptr}; + auto const grade0{tool.start.track != nullptr ? -grade_of(tool.start.direction) : unknown}; + auto const grade1{End.track != nullptr ? grade_of(End.direction) : unknown}; + if (EditorSettings.heights() == editorSettings::lay_heights::fitted) + follow_ground(pieces, Points.front().y, grade0, Points.back().y, grade1, style); + else + ground_follow::follow_points(pieces, plan.knots, {Points.front().y, grade0, Points.back().y, grade1}); + if (Plan != nullptr) + *Plan = std::move(plan); + return pieces; } -alignment::result editor_mode::lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &design) const +bool editor_mode::traced_from(std::vector const &Points, std::vector const &Modes, bool const AtEnd) +{ + if (Points.size() < (AtEnd ? 3u : 2u)) + return false; + auto const index{AtEnd ? Points.size() - 2 : std::size_t{1}}; + return index < Modes.size() && Modes[index] == lay_mode::curve; +} + +bool editor_mode::lay_line(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error, bool const Adjoining) const { auto const &tool{m_lay}; - design = {}; + Plan = {}; + auto &design{Plan.design}; design.norms = laying_norms(); design.shape = m_route.design.shape; - design.speed = design_speed(tool.start.track, nullptr); + design.speed = design_speed(tool.start.track, End.track); design.start = Points.front(); - design.end = Points.back(); - design.start_direction = Startdirection; - design.end_direction = Enddirection; - - alignment::vertex corner; - corner.radius = std::max(1.0, tool.radius); - if (Points.size() == 2) + auto const joined{End.track != nullptr && glm::length(plan_of(End.direction)) > 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. 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 (false == alignment::collinear(design)) + if (tool.start.track != nullptr && glm::length(plan_of(tool.start.direction)) > 1e-6) + adjoining_curve(tool.start, false, traced_from(Points, Modes, false), design.norms.gauge, design); + if (joined) + adjoining_curve(End, true, traced_from(Points, Modes, true), design.norms.gauge, design); + } + auto const curved{design.start_curved || design.end_curved}; + auto const from_straight = [&]() { return curved && lay_line(Points, Modes, End, Plan, Error, false); }; + // the end of the transition out of the curve of the free end, the start of the line where a curve traced next to it is laid from a + // straight there; the same at the end + std::optional> easedstart; + std::optional> easedend; + // the points between the start and the end: a free end reached is the last point + auto const inner{Points.size() - (joined ? 1 : 0)}; + auto const mode_of = [&](std::size_t const Index) { return Index < Modes.size() ? Modes[Index] : lay_mode::straights; }; + auto const lifted = [](glm::dvec2 const &Point, double const Height) { return glm::dvec3{Point.x, Height, Point.y}; }; + auto const apart = [&](glm::dvec2 const &Direction) { + if (glm::length(Direction) > 1e-6) + return true; + Error = STR_C("Two points lie on one another"); + return false; + }; + + // the straight the line goes on along: a point of it and, where it's fixed, its direction + auto anchor{Points.front()}; + std::optional along; + if (tool.start.track != nullptr && glm::length(plan_of(tool.start.direction)) > 1e-6) + along = glm::normalize(-plan_of(tool.start.direction)); + auto const startdirection{along}; + struct bend + { + glm::dvec3 position{0.0}; + alignment::vertex vertex; + bool corner{true}; + }; + std::vector bends; + // a bend clicked after a straight of a fixed direction goes onto it, as far along it as it was clicked + auto const onto = [&](glm::dvec3 const &Point) { + auto const distance{std::max(1.0, glm::dot(plan_of(Point - anchor), *along))}; + Plan.guides.emplace_back(anchor, lifted(plan_of(anchor) + *along * (distance + std::max(30.0, 0.2 * distance)), anchor.y)); + return lifted(plan_of(anchor) + *along * distance, Point.y); + }; + // the straights meet where the two lines cross, ahead of both points + auto const meeting = [](glm::dvec2 const &From, glm::dvec2 const &Along, glm::dvec2 const &To, glm::dvec2 const &Arriving, glm::dvec2 &Point) { + double t, u; + if (false == geometry::lines_cross(From, Along, To, Arriving, t, u) || t <= 0.0 || u <= 0.0) + return false; + Point = From + Along * t; + return true; + }; + auto const straight_bend = [&](glm::dvec3 Point) { + if (along.has_value()) { - glm::dvec2 intersection; - if (alignment::tangent_intersection(design, intersection)) + Point = onto(Point); + along.reset(); + } + bends.push_back({Point, {}, true}); + anchor = Point; + }; + + std::size_t i{1}; + while (i < inner) + { + auto last{i}; + while (last < inner && mode_of(last) == lay_mode::curve) + ++last; + auto const reaching{last == inner && joined}; + // 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}; + auto const from{plan_of(anchor)}; + glm::dvec2 const inward{-along->y * side, along->x * side}; + auto const centre{from + inward * radius}; + auto const outward{plan_of(Points[1]) - centre}; + if (glm::length(outward) > 0.5) { - design.vertices.push_back(corner); - } - else - { - corner.offset = plan_distance(design.start, design.end) / 3.0; - design.vertices = {corner, corner}; + // how far round the curve turns to there + auto sweep{std::fmod(side * (std::atan2(outward.y, outward.x) - std::atan2(from.y - centre.y, from.x - centre.x)), 2.0 * glm::pi())}; + if (sweep < 0.0) + sweep += 2.0 * glm::pi(); + if (sweep > 1e-3 && sweep < glm::pi()) + { + alignment::vertex curve; + curve.radius = radius; + curve.cant = design.start_cant; + bends.push_back({lifted(from + *along * (radius * std::tan(0.5 * sweep)), anchor.y), curve, false}); + anchor = lifted(centre + glm::normalize(outward) * radius, Points[1].y); + along = turned(*along, side * sweep); + i = last; + continue; + } } } + // a bend of straights, or a point of a curve too alone to make one + if (last == i || (last - i < 2 && false == reaching)) + { + straight_from_end(); + straight_bend(Points[i]); + ++i; + continue; + } + lay_trace trace; + std::vector traced; + for (auto k{i}; k < last; ++k) + { + trace.points.push_back(Points[k]); + traced.push_back(plan_of(Points[k])); + } + i = last; + // the straight the curve comes from: the one of the free end or of the curve before, else the one through the last two + // points clicked before the curve, the last of them then no bend + auto entry{anchor}; + glm::dvec2 entering; + // how far back the curve may begin before the point it comes from: up to the bend or the start before it, half the way back + // to a curve traced before, nothing before a free end + double behind{0.0}; + // the first point clicked, with no heading of its own: the curve fitted may start out of it any way + auto loose{false}; + if (along.has_value()) + { + entering = *along; + if (false == bends.empty()) + behind = 0.5 * plan_distance(entry, bends.back().position); + } + else if (false == bends.empty() && bends.back().corner) + { + entry = bends.back().position; + bends.pop_back(); + auto const before{bends.empty() ? Points.front() : bends.back().position}; + entering = plan_of(entry - before); + behind = plan_distance(entry, before) * (bends.empty() || bends.back().corner ? 1.0 : 0.5); + } + else + { + entering = traced.front() - plan_of(anchor); + loose = bends.empty(); + } + if (false == apart(entering)) + return false; + entering = glm::normalize(entering); + // where the curve ends: at the free end the line finishes at, else at the last point clicked, heading as the circle through the last + // three points does there; a point clicked after it in the mode of the straights, the curve heads for it there, the straight + // to it as clicked + auto exit{trace.points.back()}; + glm::dvec2 leaving; + auto const headed{false == reaching && last < inner}; + if (reaching) + { + exit = End.position; + leaving = glm::normalize(plan_of(End.direction)); + } + else if (headed) + { + leaving = plan_of(Points[last]) - plan_of(exit); + if (false == apart(leaving)) + return false; + leaving = glm::normalize(leaving); + } + else + { + 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; + } + auto const &fit{*fitted}; + if (false == fit.valid) + { + Error = STR_C("No curve follows the points clicked along it"); + return from_straight(); + } + for (auto const &vertex : fit.vertices) + bends.push_back({lifted(vertex.position, entry.y), vertex, false}); + if (fit.vertices.empty()) + { + // a straight: the two lines are one + exit = lifted(plan_of(entry) + entering * glm::dot(plan_of(exit - entry), entering), exit.y); + leaving = entering; + } + trace.offsets = fit.offsets; + trace.rms = fit.rms; + trace.worst = fit.worst; + trace.curves = fit.vertices.size(); + } + Plan.traces.push_back(std::move(trace)); + anchor = exit; + along = leaving; + } + + // the end: the free end reached, the end of a curve traced, or the last point clicked + glm::dvec3 end; + glm::dvec2 ending; + if (joined) + { + end = End.position; + ending = glm::normalize(plan_of(End.direction)); + if (along.has_value() && (false == bends.empty() || false == Plan.traces.empty())) + { + // from the straight out of a curve traced to the one of the free end: a bend where they meet + glm::dvec2 corner; + if (meeting(plan_of(anchor), *along, plan_of(end), -ending, corner)) + bends.push_back({lifted(corner, anchor.y), {}, true}); + } + } + else if (along.has_value()) + { + end = anchor; + ending = *along; } else { - for (std::size_t i = 1; i + 1 < Points.size(); ++i) + if (bends.empty()) { - corner.position = plan_of(Points[i]); - design.vertices.push_back(corner); + Error = STR_C("Click at least two points"); + return false; } - if (design.vertices.size() >= 2) + end = bends.back().position; + bends.pop_back(); + ending = plan_of(end) - plan_of(bends.empty() ? Points.front() : bends.back().position); + if (false == apart(ending)) + return false; + ending = glm::normalize(ending); + } + if (plan_distance(end, Points.front()) < 1.0) + { + Error = STR_C("Click at least two points"); + return false; + } + design.end = end; + design.end_direction = ending; + if (startdirection.has_value()) + design.start_direction = *startdirection; + else + { + auto const first{plan_of(bends.empty() ? end : bends.front().position) - plan_of(Points.front())}; + if (false == apart(first)) + return false; + design.start_direction = glm::normalize(first); + } + if (easedstart.has_value()) + std::tie(design.start, design.start_direction) = *easedstart; + if (easedend.has_value()) + { + std::tie(design.end, design.end_direction) = *easedend; + end = design.end; + } + for (auto const &bend : bends) + { + auto vertex{bend.corner ? alignment::vertex{} : bend.vertex}; + vertex.position = plan_of(bend.position); + design.vertices.push_back(vertex); + Plan.corners.push_back(bend.corner ? 1 : 0); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = glm::dot(plan_of(bends.front().position - design.start), design.start_direction); + design.vertices.back().offset = glm::dot(plan_of(end - bends.back().position), design.end_direction); + } + Plan.shape = alignment::ease_corners(design, Plan.corners, {EditorSettings.corner_low(), EditorSettings.corner_high()}, bends_fit()); + if (false == Plan.shape.valid) + { + Error = Plan.shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : Plan.shape.errors.front(); + return from_straight(); + } + // the grade breaks at the bends which stayed bends; along a curve, traced or made of bends, it goes on even from end to end + for (std::size_t k = 0; k < design.vertices.size() && k < Plan.corners.size(); ++k) + { + if (Plan.corners[k] != 1) + continue; + for (auto const &bend : bends) + if (bend.corner && glm::distance(plan_of(bend.position), design.vertices[k].position) < 1e-3) + { + Plan.knots.push_back(bend.position); + break; + } + } + return true; +} + +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, (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); + key.push_back(point.y); + } + auto &cache{m_stretch_cache}; + for (auto entry{cache.begin()}; entry != cache.end(); ++entry) + if (entry->first == key) { - design.vertices.front().offset = std::max(1.0, glm::dot(plan_of(Points[1] - Points.front()), design.start_direction)); - design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); + std::rotate(cache.begin(), entry, std::next(entry)); + return cache.front().second; + } + auto const keep = [&](alignment::trace_stretch const &Fit) { + cache.insert(cache.begin(), {key, Fit}); + if (cache.size() > 16) + cache.pop_back(); + }; + alignment::trace_stretch fit; + if (false == m_follow_preview) + { + fit = Going ? alignment::fit_going_on(Base, Points) : alignment::fit_trace_stretch(Base, Points, Radii, Straights); + keep(fit); + return fit; + } + // shown while the cursor moves: fitted on the thread, the frames meanwhile drawn with the fit done last for the same points but + // the one at the cursor, or with no curve yet + if (m_stretch_fitter.done(key, fit)) + { + keep(fit); + return fit; + } + m_stretch_fitter.ask({key, Base, Points, Radii, Straights, Going}); + std::vector last; + 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 alignment::compute_recommended(design, tool.transitions); + return {}; +} + +void editor_mode::lay_trim() +{ + auto &tool{m_lay}; + tool.modes.resize(tool.points.size(), lay_mode::straights); +} + +void editor_mode::lay_switch_mode() +{ + auto &tool{m_lay}; + tool.mode = tool.mode == lay_mode::straights ? lay_mode::curve : lay_mode::straights; + 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 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(); } void editor_mode::lay_click() { auto &tool{m_lay}; tool.status.clear(); + lay_trim(); auto const snap{tool.mouse_snap}; if (snap.track != nullptr && false == tool.points.empty() && glm::distance(snap.position, tool.points.front()) > 1.0) { // reaching a free end finishes the track there, in line with the path it joins tool.points.push_back(snap.position); + tool.modes.push_back(tool.mode); lay_finish(snap); return; } 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}) @@ -963,69 +1876,75 @@ void editor_mode::lay_click() point = from + (to - from) * along; } tool.points.push_back(point); + tool.modes.push_back(tool.mode); return; } + // the points only make the line shown: it's laid by Enter, or by a click on a free end auto const &point{tool.mouse}; if (plan_distance(point, tool.points.back()) < 1.0) return; m_typed.clear(); - if (tool.points.size() == 1) - { - lay_tool::step const previous{tool.start, tool.points.front()}; - tool.points.push_back(tool.mouse); - lay_finish({}); - if (false == tool.points.empty()) - { - tool.points.pop_back(); - return; - } - 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}; - return; - } tool.points.push_back(point); + tool.modes.push_back(tool.mode); } void editor_mode::lay_finish(editor_track::snap_target const &End) { auto &tool{m_lay}; + lay_trim(); double length; std::string error; - auto const pieces{lay_pieces(tool.points, End, length, error)}; + lay_line_plan plan; + auto const pieces{lay_pieces(tool.points, tool.modes, End, length, error, &plan)}; + auto const eased{m_eased_last}; tool.status_time = ImGui::GetTime(); if (pieces.empty()) { tool.status = error.empty() ? std::string{STR_C("Click at least two points")} : error; if (End.track != nullptr && false == tool.points.empty()) + { tool.points.pop_back(); + lay_trim(); + } return; } auto const *style{tool.start.track != nullptr ? tool.start.track : End.track}; if (style == nullptr && tool.copy_selected) style = selected_track(); + // the track laid takes only its heights from the ground: the terrain is shaped under it by the buttons of the terrain model + m_nmt_state.laying = true; std::vector created; for (auto const &piece : pieces) created.push_back(style != nullptr ? editor_track::create_path(*style, piece) : editor_track::create_path(tool.style, piece)); + give_own_speed(created); editor_track::commit(created); - digitize_drawn(created); + // the scenery keeps the design the tracks were laid by, as it is, for the plan to edit; read from the paths again it wouldn't be + // the same, a compound curve with transitions between its arcs least of all. A track laid with no design of the line, between two + // free ends or back from one, keeps none: the plan reads its curves when it's opened + // 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); + tool.history = m_history.size(); push_track_snapshot({}, created); first_step_done(first_step::laid); + if (plan.shape.valid && false == plan.shape.curves.empty()) + first_step_done(first_step::bend); if (End.track != nullptr) first_step_done(first_step::join); - tool.status = format(STR_P("Laid %zu track, %.1f m, Ctrl+Z takes it back", "Laid %zu tracks, %.1f m, Ctrl+Z takes them back", static_cast(created.size())), created.size(), length); + tool.status = format(STR_P("Laid %.1f m of track in %zu piece, Ctrl+Z takes it back", "Laid %.1f m of track in %zu pieces, Ctrl+Z takes it back", static_cast(created.size())), length, created.size()); if (false == error.empty()) tool.status += "\n" + error; + if (eased.has_value() && eased->compound) + tool.status += "\n" + format(STR_C("Compound curve: R %.0f m goes on into R %.0f m through a transition curve of %.0f m"), eased->from_radius, eased->radius, eased->entry); + // the curves next to the free ends going on from the curves of their tracks + if (plan.shape.valid) + for (auto const &curve : plan.shape.curves) + if (auto const on{curve_goes_on(curve)}; false == on.empty()) + tool.status += "\n" + format(STR_C("Curve %d %s"), curve.vertex + 1, on.c_str()); + if (eased.has_value() && false == eased->note.empty()) + tool.status += "\n" + eased->note; + for (auto const &warning : switch_exit_warnings(tool.start.track, tool.points.front(), End.track, End.position, pieces)) + tool.status += "\n" + warning; { std::vector> run; if (tool.start.track != nullptr && tool.start.track->eType == tt_Normal) @@ -1045,11 +1964,15 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) } if (scene::Layers.active() == null_handle && style == nullptr) tool.status += "\n" + STR("The scenery isn't opened for editing: new track won't be saved"); - WriteLog("Editor: " + tool.status, logtype::generic); - tool.points.clear(); - tool.start = {}; m_node = created.back(); ui()->set_node(m_node); + if (nmt_fit_laid(tool.history, created.back()->m_paths.front().points[segment_data::point::end])) + tool.status += "\n" + STR("The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain"); + m_nmt_state.laying = false; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.points.clear(); + tool.modes.clear(); + tool.start = {}; straight_refresh(); } @@ -1061,35 +1984,61 @@ void editor_mode::lay_enter() lay_click(); return; } + lay_trim(); if (tool.points.empty()) return; auto const end{tool.mouse_snap}; if (end.track != nullptr && glm::distance(end.position, tool.points.front()) > 1.0) { tool.points.push_back(end.position); + tool.modes.push_back(tool.mode); lay_finish(end); return; } - auto const point{placement_on_ground(tool.mouse)}; - bool const appended{plan_distance(point, tool.points.back()) > 1.0}; - if (appended) - tool.points.push_back(point); - auto const count{tool.points.size()}; + // a single point is no track: Enter drops it + if (tool.points.size() < 2) + { + lay_cancel(); + return; + } + // 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 (appended && tool.points.size() == count) - tool.points.pop_back(); } void editor_mode::lay_cancel() { auto &tool{m_lay}; tool.points.clear(); + tool.modes.clear(); tool.start = {}; - tool.chain.clear(); tool.preview.clear(); + tool.plan = {}; tool.status.clear(); } +std::size_t editor_mode::lay_pending() const +{ + return m_lay.points.size(); +} + +void editor_mode::lay_escape() +{ + auto &tool{m_lay}; + lay_trim(); + // the points clicked are no track yet and nothing brings them back: a slip of Esc only warns, the second one drops them + auto const pending{lay_pending()}; + if (tool.points.size() < 2 || ImGui::GetTime() - tool.escape_time < 3.0) + { + lay_cancel(); + tool.escape_time = -100.0; + return; + } + tool.escape_time = ImGui::GetTime(); + tool.status = format(STR_P("Esc again drops the point not laid, Enter lays it", "Esc again drops the %zu points not laid, Enter lays them", static_cast(pending)), pending); + tool.status_time = tool.escape_time; +} + void editor_mode::lay_after_history(bool const Undo) { auto &tool{m_lay}; @@ -1100,33 +2049,25 @@ 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.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}; - if (false == tool.chain.empty()) + lay_trim(); + // the points not laid yet, the last one first; the tracks laid go back with Ctrl+Z + if (tool.points.size() > 1) { - undo_last(); - tool.status = STR_C("The last piece was taken back"); - tool.status_time = ImGui::GetTime(); + tool.points.pop_back(); + tool.modes.pop_back(); return; } - if (false == tool.points.empty()) - tool.points.pop_back(); - if (tool.points.empty()) - tool.start = {}; + tool.points.clear(); + tool.modes.clear(); + tool.start = {}; } glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const @@ -1159,7 +2100,7 @@ glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse, std::string &Lab ratio = n; } } - auto const name{axis->name().empty() ? std::string{STR_C("(no name)")} : axis->name()}; + auto const name{name_of(*axis)}; auto const degrees{glm::degrees(std::abs(std::remainder(angle, glm::pi())))}; Label = ratio > 0.0 ? format(STR_C("1:%g to %s"), ratio, name.c_str()) : degrees < 0.1 ? format(STR_C("parallel to %s"), name.c_str()) : format(STR_C("%.0f deg to %s"), degrees, name.c_str()); } @@ -1218,159 +2159,146 @@ editor_mode::track_hover editor_mode::split_spot() const alignment::limits editor_mode::laying_norms() const { auto norms{m_route.design.norms}; - norms.checked = false == m_norms_free; + norms.checked = true; return norms; } -namespace -{ - -// a card of the geometry of the new track: its plan, and under it its curvature along the length, a point running along both; -// a plain arc jumps to its curvature, a railway curve leads into it along a transition, the cant with it -float geometry_card_height(char const *Title, char const *Text, float const Width) -{ - auto const font{ImGui::GetFontSize()}; - auto const pad{std::round(font * 0.5f)}; - auto const wrap{Width - 2.0f * pad}; - return pad + ImGui::CalcTextSize(Title, nullptr, false, wrap).y + pad * 0.5f + std::round(font * 2.6f) + pad * 0.5f + ImGui::CalcTextSize(Text, nullptr, false, wrap).y + pad; -} - -bool geometry_card(char const *Id, char const *Title, char const *Text, bool const Railway, bool const Chosen, float const Width, float const Height) -{ - auto const font{ImGui::GetFontSize()}; - auto const pad{std::round(font * 0.5f)}; - auto const sketch{std::round(font * 2.6f)}; - auto const wrap{Width - 2.0f * pad}; - auto const title{ImGui::CalcTextSize(Title, nullptr, false, wrap)}; - auto const height{Height}; - auto const top{ImGui::GetCursorScreenPos()}; - auto const clicked{ImGui::InvisibleButton(Id, ImVec2(Width, height))}; - auto const hovered{ImGui::IsItemHovered()}; - auto *list{ImGui::GetWindowDrawList()}; - auto const accent{family_colour(0)}; - ImVec2 const bottom{top.x + Width, top.y + height}; - auto const rounding{std::round(font * 0.35f)}; - list->AddRectFilled(top, bottom, Chosen ? (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 46) : ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding); - list->AddRect(top, bottom, Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, Chosen ? 2.0f : 1.0f); - auto y{top.y + pad}; - list->AddText(nullptr, 0.0f, ImVec2(top.x + pad, y), ImGui::GetColorU32(ImGuiCol_Text), Title, nullptr, wrap); - y += title.y + pad * 0.5f; - // the curvature along the length, as a share of the curvature of the arc - auto const curvature = [Railway](double const Share) { - if (Railway) - return std::clamp((Share - 0.22) / 0.3, 0.0, 1.0); - return Share < 0.37 ? 0.0 : 1.0; - }; - int constexpr steps{80}; - std::vector plan; - plan.reserve(steps + 1); - double heading{0.0}, px{0.0}, py{0.0}; - auto const turn{1.45 / (Railway ? 0.63 - 0.15 : 0.63)}; - for (int i = 0; i <= steps; ++i) - { - plan.emplace_back(static_cast(px), static_cast(py)); - auto const share{(i + 0.5) / steps}; - heading += curvature(share) * turn / steps; - px += std::cos(heading) / steps; - py -= std::sin(heading) / steps; - } - ImVec2 low{plan.front()}, high{plan.front()}; - for (auto const &point : plan) - { - low = ImVec2(std::min(low.x, point.x), std::min(low.y, point.y)); - high = ImVec2(std::max(high.x, point.x), std::max(high.y, point.y)); - } - // the plan on the left, the curvature along the length on the right - auto const half{std::floor((wrap - pad) * 0.5f)}; - auto const scale{std::min(half / std::max(1e-3f, high.x - low.x), sketch / std::max(1e-3f, high.y - low.y))}; - ImVec2 const origin{top.x + pad + (half - (high.x - low.x) * scale) * 0.5f, y + sketch - (high.y - low.y) * scale}; - for (auto &point : plan) - point = ImVec2(origin.x + (point.x - low.x) * scale, origin.y + (point.y - low.y) * scale); - auto const line{Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Text, 0.75f)}; - list->AddPolyline(plan.data(), static_cast(plan.size()), line, 0, 2.0f); - auto const label{ImGui::CalcTextSize("1/R")}; - auto const graph_left{top.x + pad + half + pad * 0.5f + label.x + 2.0f}; - auto const graph_width{top.x + pad + wrap - graph_left}; - auto const graph_top{y}; - auto const graph{sketch}; - auto const axis{ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.6f)}; - list->AddLine(ImVec2(graph_left, graph_top + graph), ImVec2(graph_left + graph_width, graph_top + graph), axis); - list->AddLine(ImVec2(graph_left, graph_top), ImVec2(graph_left, graph_top + graph), axis); - list->AddText(ImVec2(graph_left - label.x - 2.0f, graph_top + graph * 0.2f - label.y * 0.5f), axis, "1/R"); - std::vector curve; - for (int i = 0; i <= steps; ++i) - { - auto const share{static_cast(i) / steps}; - curve.emplace_back(graph_left + static_cast(share) * graph_width, graph_top + graph - static_cast(curvature(share)) * graph * 0.8f); - } - list->AddPolyline(curve.data(), static_cast(curve.size()), line, 0, 1.5f); - if (Railway) - for (int i = 0; i < steps; i += 4) - { - auto const share{static_cast(i) / steps}; - auto const next{static_cast(i + 2) / steps}; - list->AddLine(ImVec2(graph_left + static_cast(share) * graph_width, graph_top + graph - static_cast(curvature(share)) * graph * 0.45f), - ImVec2(graph_left + static_cast(next) * graph_width, graph_top + graph - static_cast(curvature(next)) * graph * 0.45f), IM_COL32(240, 170, 60, 220), 1.0f); - } - auto const running{std::fmod(ImGui::GetTime() * 0.22, 1.0)}; - auto const index{std::clamp(static_cast(running * steps), 0, steps)}; - list->AddCircleFilled(plan[index], 3.0f, IM_COL32(255, 255, 255, 230)); - list->AddLine(ImVec2(curve[index].x, graph_top), ImVec2(curve[index].x, graph_top + graph), IM_COL32(255, 255, 255, 70)); - list->AddCircleFilled(curve[index], 2.5f, IM_COL32(255, 255, 255, 230)); - y += sketch + pad * 0.5f; - list->AddText(nullptr, 0.0f, ImVec2(top.x + pad, y), ImGui::GetColorU32(ImGuiCol_TextDisabled), Text, nullptr, wrap); - return clicked; -} - -} // namespace - -bool editor_mode::render_norms_choice() -{ - auto changed{false}; - auto const gap{std::round(ImGui::GetFontSize() * 0.5f)}; - auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; - char const *const titles[]{STR_C("Free laying"), STR_C("To the PKP PLK norms")}; - char const *const texts[]{STR_C("Plain arcs, no transition curves nor cant"), STR_C("Transition curves and cant for the speed, checked against the norms")}; - auto const height{std::max(geometry_card_height(titles[0], texts[0], width), geometry_card_height(titles[1], texts[1], width))}; - ImGui::PushID("geometry"); - for (int i = 0; i < 2; ++i) - { - if (i > 0) - ImGui::SameLine(0.0f, gap); - auto const chosen{m_norms_free == (i == 0)}; - auto const clicked{geometry_card(i == 0 ? "##free" : "##norms", titles[i], texts[i], i == 1, chosen, width, height)}; - item_tooltip(i == 0 ? "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked" - : "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); - if (clicked && false == chosen) - { - m_norms_free = i == 0; - m_lay.transitions = false == m_norms_free; - changed = true; - } - } - ImGui::PopID(); - if (changed && false == m_route.chain.tracks.empty()) - route_update(); - return changed; -} - void editor_mode::render_lay_ui() { auto &tool{m_lay}; - render_norms_choice(); - ImGui::Separator(); + // the points clicked are no track until they're laid: said first, with the keys as buttons + if (tool.points.size() > 1) + { + auto const pending{lay_pending()}; + ImGui::TextColored(ImVec4(1.0f, 0.83f, 0.3f, 1.0f), "%s", format(STR_P("Not laid yet: %zu point", "Not laid yet: %zu points", static_cast(pending)), pending).c_str()); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.45f, 0.22f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.56f, 0.28f, 1.0f)); + if (ImGui::Button(STR_C("Lay (Enter)"))) + { + // the cursor is over the panel: no free end under it to join + tool.mouse_snap = {}; + lay_enter(); + } + ImGui::PopStyleColor(2); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Drop the points"))) + lay_cancel(); + item_tooltip("Esc twice does the same"); + ImGui::Separator(); + } + 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 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); - ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); - item_tooltip("Drag to change, Ctrl+click to type the value. While laying: Ctrl+wheel"); - value_chips("radii", tool.radius, {190.0, 300.0, 500.0, 800.0, 1200.0, 2000.0}, "%.0f"); - tool.radius = std::max(10.0, tool.radius); + if (tool.mode == lay_mode::straights) + { + double low{EditorSettings.corner_low()}; + 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}; + for (auto const &[from, to] : {std::pair{1000.0, 2000.0}, std::pair{2000.0, 4000.0}, std::pair{3000.0, 6000.0}, std::pair{4000.0, 8000.0}}) + { + if (false == first) + ImGui::SameLine(); + first = false; + auto const chosen{std::abs(low - from) < 0.5 && std::abs(high - to) < 0.5}; + if (chosen) + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + if (ImGui::SmallButton(format("%.0f-%.0f", from, to).c_str())) + { + low = from; + high = to; + changed = true; + release = true; + } + if (chosen) + ImGui::PopStyleColor(); + } + ImGui::PopID(); + if (changed) + { + low = std::max(10.0, low); + EditorSettings.corners(low, std::max(low, high)); + } + if (release) + EditorSettings.save(); + } + else + { + ImGui::PopItemWidth(); + segmented("layradii", tool.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"}}); + ImGui::Checkbox(STR_C("Two curves with a straight between them, where the points show one"), &tool.straights_between); + item_tooltip("The points along the curve may make two curves, turning the same way or not, with a straight between them, where these follow the points much closer than one curve"); + if (auto const &traces{tool.plan.traces}; false == traces.empty() && false == traces.back().offsets.empty()) + { + auto const &trace{traces.back()}; + auto const colour = [](double const Departure) { return Departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : Departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255); }; + metric_tiles({{format("%zu", trace.points.size()), STR_C("points"), ImGui::GetColorU32(ImGuiCol_Text)}, + {format("%.0f cm", trace.rms * 100.0), STR_C("departure"), colour(trace.rms)}, + {format("%.0f cm", trace.worst * 100.0), STR_C("at most"), colour(trace.worst)}}); + } + else + { + ImGui::PushTextWrapPos(0.0f); + 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); + } ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f"); - item_tooltip(m_norms_free ? "Speed limit of the new paths; the speed of the track joined comes first" - : "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows"); - if (false == m_norms_free) - value_chips("speeds", tool.style.velocity, {40.0, 60.0, 80.0, 100.0, 120.0, 160.0}, "%.0f"); + item_tooltip("Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows"); + value_chips("speeds", tool.style.velocity, {40.0, 60.0, 80.0, 100.0, 120.0, 160.0}, "%.0f"); tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); ImGui::PopItemWidth(); + ImGui::Checkbox(STR_C("This speed also on from a track with a speed of its own"), &tool.own_speed); + item_tooltip("Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel"); + if (auto const [from, to]{laying_ends()}; from != nullptr || to != nullptr) + ImGui::TextDisabled("%s", design_speed_of(from, to).c_str()); + ImGui::TextDisabled("%s", STR_C("Height of the track on the ground")); + if (int heights{static_cast(EditorSettings.heights())}; + segmented("layheights", heights, + {{0, STR_C("Fitted"), "The grades fitted to the ground as Tracks onto the terrain fits them: vertical curves of the norms, the bumps of the ground gone past; the terrain is left as it is. On the terrain model the whole track laid in a row is fitted again at each click"}, + {1, STR_C("Point to point"), "Each bend of the straights clicked at the height of the ground, an even grade between them: the grade changes only at the bends. A curve traced, or bends clicked close along a curve, goes at an even grade from one end of it to the other"}, + {2, STR_C("Flat"), "The track goes on at the height it's laid from, whatever the ground; the first point on the ground"}})) + { + EditorSettings.heights(static_cast(heights)); + EditorSettings.save(); + } + // the curves of the line shown, those below the norms in the colour of a warning: they're laid all the same + if (auto const &plan{tool.plan}; tool.active && plan.shape.valid && false == plan.shape.curves.empty()) + { + ImGui::SeparatorText(STR_C("Curves of the track laid")); + for (auto const &curve : plan.shape.curves) + { + auto const meets{alignment::meets_norms(curve, plan.design)}; + auto const text{format("%d. %s", curve.vertex + 1, curve_label(curve, laying_norms()).c_str())}; + if (meets) + ImGui::TextUnformatted(text.c_str()); + else + ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.15f, 1.0f), "%s", text.c_str()); + if (ImGui::IsItemHovered()) + { + std::string notes; + auto const tag{format(STR_C("Vertex %zu"), static_cast(curve.vertex + 1))}; + for (auto const &warning : plan.shape.warnings) + if (warning.rfind(tag + ":", 0) == 0) + notes += (notes.empty() ? "" : "\n") + warning; + ImGui::SetTooltip("%s", notes.empty() ? STR_C("Meets the norms for the speed") : notes.c_str()); + } + } + } if (auto *track{selected_track()}; track != nullptr) { std::string reason; @@ -1382,7 +2310,13 @@ void editor_mode::render_lay_ui() render_lay_status(); return; } - render_layout_combo(); + ImGui::TextDisabled("%s", STR_C("Dragged from a free end, or laid from a point to one")); + ImGui::PushItemWidth(120.0f); + ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Drag to change, Ctrl+click to type the value. While dragging: Ctrl+wheel"); + value_chips("radii", tool.radius, {190.0, 300.0, 500.0, 800.0, 1200.0, 2000.0}, "%.0f"); + tool.radius = std::max(10.0, tool.radius); + ImGui::PopItemWidth(); ImGui::Spacing(); ImGui::PushItemWidth(120.0f); ImGui::DragScalar(STR_C("Longest path (m)"), ImGuiDataType_Double, &tool.piece_length, 1.0f, nullptr, nullptr, "%.0f"); diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index e96858d8..b22a19b3 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -9,12 +9,14 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodelineside.h" #include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" +#include "editor/editorKilometrage.hpp" #include "scene/scene.h" #include "scene/scenelayers.h" @@ -34,6 +36,7 @@ namespace using geometry::bezier; using geometry::plan_distance; +using geometry::heading_of; using geometry::plan_of; double const kSampleStep{2.0}; // m between the samples of the line @@ -46,19 +49,6 @@ int const kRailCategory{1}; char const *const kHektoStyles[] = {"nowy", "stary", "zgnity"}; char const *const kFoulingModels[] = {"ip/pkp/w17.inc", "ip/pkp/w17_new.inc", "ip/pkp/w17_old.inc", "ip/pkp/w17_dwuteo.inc"}; -std::string normalized(std::string Text) -{ - std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(Character == '\\' ? '/' : std::tolower(Character)); }); - return Text; -} - -bool numeric(std::string const &Text, double &Value) -{ - char *end{nullptr}; - Value = std::strtod(Text.c_str(), &end); - return false == Text.empty() && *end == '\0'; -} - std::string number(double const Value, int const Decimals) { auto text{format("%.*f", Decimals, Value)}; @@ -67,12 +57,6 @@ std::string number(double const Value, int const Decimals) return text; } -double heading_of(glm::dvec2 const &Direction) -{ - auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))}; - return degrees < 0.0 ? degrees + 360.0 : degrees; -} - // index of the sample at or before the chainage std::size_t sample_index(std::vector const &Samples, double const Chainage) { @@ -237,7 +221,7 @@ std::vector editor_mode::standing_templates(std: auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; - auto const file{normalized(*included.file)}; + auto const file{editor_includes::normalized(*included.file)}; if (false == Accept(file) || included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f}) continue; std::string target; @@ -259,7 +243,41 @@ std::vector editor_mode::standing_templates(std: return result; } -std::size_t editor_mode::place_templates(std::vector const &Items, std::string &Error) +scene::layer_handle editor_mode::output_layer(std::string const &Part) +{ + if (scene::Layers.empty()) + return null_handle; + auto stem{scene::Layers.layer(scene::layer_handle{1}).name}; + if (auto const slash{stem.find_last_of("/\\")}; slash != std::string::npos) + stem.erase(0, slash + 1); + if (auto const dot{stem.rfind('.')}; dot != std::string::npos) + stem.erase(dot); + stem.erase(0, stem.find_first_not_of('$')); + if (stem.empty()) + stem = "scenery"; + std::string reason; + // a file of that name the scenery doesn't include, or one which can't take more, gives way to the next number + for (int attempt = 1; attempt <= 9; ++attempt) + { + auto const name{ToLower(stem + "_" + Part + (attempt > 1 ? std::to_string(attempt) : std::string{}) + ".scm")}; + auto layer{null_handle}; + for (std::size_t index = 1; index <= scene::Layers.size() && layer == null_handle; ++index) + if (auto const &existing{scene::Layers.layer(static_cast(index))}; false == existing.dead && false == existing.removed && existing.name == name) + layer = scene::Layers.resolve(static_cast(index)); + if (layer == null_handle) + layer = scene::Layers.create(name, scene::layer_handle{1}, reason, false); + if (layer != null_handle && scene::Layers.accepts(layer, &reason)) + { + scene::Layers.visible(layer, true); + scene::Layers.locked(layer, false); + return layer; + } + } + WriteLog("Editor: no layer for " + Part + ", the active one takes it: " + reason, logtype::generic); + return null_handle; +} + +std::size_t editor_mode::place_templates(std::vector const &Items, std::string &Error, scene::layer_handle const Layer) { if (Items.empty()) return 0; @@ -268,7 +286,7 @@ std::size_t editor_mode::place_templates(std::vector const &Items Error = STR_C("Templates can be placed only in a scenery opened for editing (-edit)"); return 0; } - auto const layer{scene::Layers.resolve(scene::Layers.active())}; + auto const layer{scene::Layers.resolve(Layer != null_handle ? Layer : scene::Layers.active())}; if (std::string reason; false == scene::Layers.accepts(layer, &reason)) { Error = STR_C("The active layer can't take them: ") + (reason.empty() ? std::string{STR_C("there's no active layer")} : reason); @@ -294,7 +312,8 @@ std::size_t editor_mode::place_templates(std::vector const &Items } else { - std::vector values{"none", number(local.x, 3), number(local.y, 3), number(local.z, 3), number(item.yaw, 2)}; + auto const name{item.values.find("name")}; + std::vector values{name != item.values.end() ? name->second : std::string{"none"}, number(local.x, 3), number(local.y, 3), number(local.z, 3), number(item.yaw, 2)}; values.insert(values.end(), item.rest.begin(), item.rest.end()); directive = editor_includes::compose_directive(item.file, values); } @@ -365,18 +384,18 @@ std::size_t editor_mode::remove_templates(std::vector co return count; } -void editor_mode::hekto_start(TTrack &Track, glm::dvec3 const &Point) +void hekto_tool::start(TTrack &Track, glm::dvec3 const &Point) { auto &tool{m_hekto}; tool.track = &Track; tool.point = Point; tool.km_read = false; tool.dirty = true; - tool.expand = true; + m_tab.expand = true; tool.status.clear(); } -void editor_mode::hekto_read_kilometrage() +void hekto_tool::hekto_read_kilometrage() { auto &tool{m_hekto}; auto const &found{tool.found}; @@ -417,13 +436,13 @@ void editor_mode::hekto_read_kilometrage() tool.status = format(STR_C("Kilometrage read from %zu posts standing along the line"), found.size()); } -void editor_mode::hekto_update() +void hekto_tool::hekto_update() { auto &tool{m_hekto}; - if (false == tool.dirty && tool.history == m_history.size()) + if (false == tool.dirty && tool.history == m_history.history_revision()) return; tool.dirty = false; - tool.history = m_history.size(); + tool.history = m_history.history_revision(); tool.posts.clear(); tool.found.clear(); tool.error.clear(); @@ -448,7 +467,7 @@ void editor_mode::hekto_update() double distance; tool.origin = project(tool.samples, tool.point, distance); auto const length{tool.samples.back().chainage}; - for (auto const &standing : standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) + for (auto const &standing : m_templates.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) { hekto_found post; post.post = standing; @@ -459,65 +478,101 @@ void editor_mode::hekto_update() continue; tool.found.push_back(post); } - if (false == tool.km_read) + tool.line_set = kilometrage::line_at(tool.point); + tool.line = tool.own_km ? std::string{} : tool.line_set; + if (tool.line.empty() && false == tool.km_read) { tool.km_read = true; hekto_read_kilometrage(); } - auto const chainage_of = [&](int const Hectometres) { return tool.origin + tool.growth * (Hectometres / 10.0 - tool.km) * 1000.0; }; - for (auto &post : tool.found) - { - post.expected = chainage_of(post.hectometres); - post.error = post.chainage - post.expected; - post.outside = true; - } - auto const km_at = [&](double const Chainage) { return tool.km + tool.growth * (Chainage - tool.origin) / 1000.0; }; - auto const low{std::max(0, static_cast(std::ceil(std::min(km_at(0.0), km_at(length)) * 10.0 - 1e-6)))}; - auto const high{static_cast(std::floor(std::max(km_at(0.0), km_at(length)) * 10.0 + 1e-6))}; - for (int hectometres = low; hectometres <= high && tool.posts.size() < 5000; ++hectometres) - { - hekto_post post; - post.hectometres = hectometres; - post.chainage = chainage_of(hectometres); - if (post.chainage < 0.0 || post.chainage > length) - continue; - auto const axis{editor_track::sampled_position(tool.samples, post.chainage)}; - auto const growing{sampled_direction(tool.samples, post.chainage) * static_cast(tool.growth)}; + auto const place = [&](hekto_post &Post) { + auto const axis{editor_track::sampled_position(tool.samples, Post.chainage)}; + auto const growing{sampled_direction(tool.samples, Post.chainage) * static_cast(Post.growth)}; glm::dvec2 const left{-growing.y, growing.x}; // alternately: the even hectometres on the right, the odd ones on the left of the growing kilometrage - auto const side{tool.side != 0 ? tool.side : (hectometres % 2 == 0 ? 1 : -1)}; + auto const side{tool.side != 0 ? tool.side : (Post.hectometres % 2 == 0 ? 1 : -1)}; auto const planar{plan_of(axis) + left * (side * tool.offset)}; - post.position = placement_on_ground({planar.x, axis.y, planar.y}); - post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0); - // of the posts with this number the one nearest to the place stands for it, the others are duplicates. it stands wrong - // when it's farther from the place than allowed: along the line, across it, or on the other side - auto best{std::numeric_limits::max()}; - for (int i = 0; i < static_cast(tool.found.size()); ++i) + Post.position = m_ground.placement_on_ground({planar.x, axis.y, planar.y}); + Post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0); + tool.posts.push_back(Post); + }; + auto const *axis{tool.line.empty() ? nullptr : kilometrage::axis_of(tool.line)}; + if (axis != nullptr) + { + // the hectometres of the kilometrage of the line, the jumps in it too + double km{0.0}; + if (kilometrage::km_at(tool.line, tool.point, km)) + tool.km = std::round(km * 1000.0) / 1000.0; + tool.growth = kilometrage::growth_at(tool.line, tool.point, sampled_direction(tool.samples, tool.origin)) < 0 ? -1 : 1; + for (auto const &hectometre : kilometrage::hectometres(*axis)) { - auto &found{tool.found[i]}; - if (found.hectometres != hectometres) + hekto_post post; + post.hectometres = hectometre.number; + post.chainage = project(tool.samples, hectometre.at, distance); + if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length || hectometre.number < 0 || tool.posts.size() >= 5000) continue; - found.outside = false; - found.distance = plan_distance(found.post.location, post.position); - if (found.distance < best) + post.growth = glm::dot(hectometre.rising, sampled_direction(tool.samples, post.chainage)) < 0.0 ? -1 : 1; + place(post); + } + std::sort(tool.posts.begin(), tool.posts.end(), [](hekto_post const &A, hekto_post const &B) { return A.chainage < B.chainage; }); + } + else + { + auto const km_at = [&](double const Chainage) { return tool.km + tool.growth * (Chainage - tool.origin) / 1000.0; }; + auto const low{std::max(0, static_cast(std::ceil(std::min(km_at(0.0), km_at(length)) * 10.0 - 1e-6)))}; + auto const high{static_cast(std::floor(std::max(km_at(0.0), km_at(length)) * 10.0 + 1e-6))}; + for (int hectometres = low; hectometres <= high && tool.posts.size() < 5000; ++hectometres) + { + hekto_post post; + post.hectometres = hectometres; + post.chainage = tool.origin + tool.growth * (hectometres / 10.0 - tool.km) * 1000.0; + post.growth = tool.growth; + if (post.chainage < 0.0 || post.chainage > length) + continue; + place(post); + } + } + // each post standing goes with the place of its number nearest to it; of the posts going with a place the one nearest to it + // stands for it, the others are duplicates. it stands wrong when it's farther from the place than allowed: along the line, + // across it, or on the other side + for (auto &found : tool.found) + { + found.outside = true; + found.place = -1; + for (int i = 0; i < static_cast(tool.posts.size()); ++i) + { + auto const &post{tool.posts[i]}; + if (post.hectometres != found.hectometres) + continue; + auto const distance{plan_distance(found.post.location, post.position)}; + if (found.place < 0 || distance < found.distance) { - best = found.distance; - post.standing = i; + found.outside = false; + found.place = i; + found.distance = distance; + found.expected = post.chainage; + found.error = found.chainage - post.chainage; } } - for (int i = 0; i < static_cast(tool.found.size()); ++i) - { - auto &found{tool.found[i]}; - if (found.hectometres != hectometres) - continue; - found.duplicate = i != post.standing; - found.wrong = false == found.duplicate && found.distance > tool.tolerance; - } - tool.posts.push_back(post); + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto const &found{tool.found[i]}; + if (found.place < 0) + continue; + auto &post{tool.posts[found.place]}; + if (post.standing < 0 || found.distance < tool.found[post.standing].distance) + post.standing = i; + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto &found{tool.found[i]}; + found.duplicate = found.place >= 0 && tool.posts[found.place].standing != i; + found.wrong = found.place >= 0 && false == found.duplicate && found.distance > tool.tolerance; } } -void editor_mode::hekto_remove_duplicates() +void hekto_tool::hekto_remove_duplicates() { auto &tool{m_hekto}; hekto_update(); @@ -525,13 +580,13 @@ void editor_mode::hekto_remove_duplicates() for (auto const &found : tool.found) if (found.duplicate) instances.push_back(found.post.instance); - auto const removed{remove_templates(instances)}; + auto const removed{m_templates.remove_templates(instances)}; tool.status = format(STR_P("%zu duplicate hectometre post removed, Ctrl+Z brings it back", "%zu duplicate hectometre posts removed, Ctrl+Z brings them back", static_cast(removed)), removed); WriteLog("Editor: " + tool.status); tool.dirty = true; } -void editor_mode::hekto_place() +void hekto_tool::hekto_place() { auto &tool{m_hekto}; hekto_update(); @@ -548,15 +603,18 @@ void editor_mode::hekto_place() items.push_back(std::move(item)); } std::string error; - auto const placed{place_templates(items, error)}; + auto const layer{items.empty() ? null_handle : m_templates.output_layer("hektometry")}; + auto const placed{m_templates.place_templates(items, error, layer)}; tool.status = placed > 0 ? format(STR_P("%zu hectometre post placed, Ctrl+Z takes it back", "%zu hectometre posts placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; + if (placed > 0 && layer != null_handle) + tool.status += format(STR_C(" (layer \"%s\")"), scene::Layers.layer(layer).name.c_str()); if (placed > 0) - first_step_done(first_step::posts); + m_guide.first_step_done(editor_first_step::posts); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -void editor_mode::hekto_fix() +void hekto_tool::hekto_fix() { auto &tool{m_hekto}; hekto_update(); @@ -570,16 +628,16 @@ void editor_mode::hekto_fix() item.yaw = post.yaw; moves.emplace_back(tool.found[post.standing].post, item); } - auto const moved{move_templates(moves)}; + auto const moved{m_templates.move_templates(moves)}; tool.status = format(STR_P("%zu hectometre post moved to its place, Ctrl+Z takes it back", "%zu hectometre posts moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -void editor_mode::render_hekto_ui() +void hekto_tool::render_options() { auto &tool{m_hekto}; - auto *selected{selected_track()}; + auto *selected{m_selection.selected_track()}; if (tool.track == nullptr || (selected != nullptr && selected != tool.track && ImGui::SmallButton(STR_C("Along the line of the selected path")))) { if (selected == nullptr) @@ -587,27 +645,47 @@ void editor_mode::render_hekto_ui() ImGui::TextDisabled("%s", STR_C("Select a path of the line, or RMB on it: Hectometre posts")); return; } - hekto_start(*selected, bezier{selected->m_paths.front()}.point(0.0)); + start(*selected, bezier{selected->m_paths.front()}.point(0.0)); } auto const changed = [&](bool const Changed) { if (Changed) tool.dirty = true; }; - if (begin_fields("##hektokm", 0.45f)) + if (false == tool.line.empty()) { - field_row("Kilometre at the point (km)"); - changed(ImGui::InputDouble("##hektokilometre", &tool.km, 0.0, 0.0, "%.3f")); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(1); - if (ImGui::Button(STR_C("Read from the posts"), ImVec2(-FLT_MIN, 0.0f))) + ImGui::TextWrapped("%s", format(STR_C("The km of line %s, as the Kilometrage tool sets them, its jumps too"), tool.line.c_str()).c_str()); + ImGui::TextDisabled("%s", format(STR_C("km %s at the point"), kilometrage::text(tool.km).c_str()).c_str()); + if (ImGui::SmallButton(STR_C("Give the km here instead"))) { + tool.own_km = true; tool.km_read = false; tool.dirty = true; } - ImGui::EndTable(); + item_tooltip("The posts go by the km given at the point and the way it grows, without the jumps of the line"); + } + else + { + if (false == tool.line_set.empty() && ImGui::SmallButton(format(STR_C("Follow the km of line %s"), tool.line_set.c_str()).c_str())) + { + tool.own_km = false; + tool.dirty = true; + } + if (begin_fields("##hektokm", 0.45f)) + { + field_row("Kilometre at the point (km)"); + changed(ImGui::InputDouble("##hektokilometre", &tool.km, 0.0, 0.0, "%.3f")); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Read from the posts"), ImVec2(-FLT_MIN, 0.0f))) + { + tool.km_read = false; + tool.dirty = true; + } + ImGui::EndTable(); + } + ImGui::TextDisabled("%s", STR_C("The kilometrage grows")); + changed(segmented("growth", tool.growth, {{1, STR_C("along the line")}, {-1, STR_C("against it")}})); } - ImGui::TextDisabled("%s", STR_C("The kilometrage grows")); - changed(segmented("growth", tool.growth, {{1, STR_C("along the line")}, {-1, STR_C("against it")}})); ImGui::TextDisabled("%s", STR_C("Side, looking where it grows")); changed(segmented("side", tool.side, {{1, STR_C("on the right")}, {-1, STR_C("on the left")}, {0, STR_C("alternately"), "The even hectometres on the right, the odd ones on the left"}})); if (ImGui::TreeNode(STR_C("More settings"))) @@ -648,11 +726,8 @@ void editor_mode::render_hekto_ui() tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), tool.found.size(), static_cast(wrong)); if (missing > 0) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(format(STR_P("Place %d post", "Place %d posts", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + if (main_button(format(STR_P("Place %d post", "Place %d posts", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall_button_height()))) hekto_place(); - ImGui::PopStyleColor(2); } if (duplicates > 0) { @@ -678,7 +753,7 @@ void editor_mode::render_hekto_ui() auto const label{post.duplicate ? format(STR_C("km %s duplicate##%u"), km_text(post.hectometres).c_str(), post.post.instance) : format(STR_C("km %s stands %.1f m from its place##%u"), km_text(post.hectometres).c_str(), post.distance, post.post.instance)}; if (ImGui::Selectable(label.c_str())) { - fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); + m_view.fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); } } ImGui::EndChild(); @@ -687,10 +762,10 @@ void editor_mode::render_hekto_ui() ImGui::TextWrapped("%s", tool.status.c_str()); } -void editor_mode::draw_hekto_overlay() const +void hekto_tool::draw_overlay() const { auto const &tool{m_hekto}; - if (false == tool.open || tool.track == nullptr) + if (false == m_tab.open || tool.track == nullptr) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; @@ -731,16 +806,16 @@ void editor_mode::draw_hekto_overlay() const } } -void editor_mode::fouling_update() +void fouling_tool::fouling_update() { auto &tool{m_fouling}; - auto *selected{selected_track()}; + auto *selected{m_selection.selected_track()}; if (tool.scope == 0 && selected != tool.selected) tool.dirty = true; - if (false == tool.dirty && tool.history == m_history.size()) + if (false == tool.dirty && tool.history == m_history.history_revision()) return; tool.dirty = false; - tool.history = m_history.size(); + tool.history = m_history.history_revision(); tool.selected = selected; tool.points.clear(); tool.found.clear(); @@ -791,7 +866,7 @@ void editor_mode::fouling_update() point.track = track; auto const middle{(onbranch + onmain) * 0.5}; // the points of the paths are at the rail foot, the heads of the rails are the height of the rail over them - point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : placement_on_ground({middle.x, std::max(onbranch.y, onmain.y), middle.z}); + point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : m_ground.placement_on_ground({middle.x, std::max(onbranch.y, onmain.y), middle.z}); auto const back{plan_of(previous - branch[i])}; point.yaw = std::fmod(heading_of(glm::length(back) > 1e-9 ? glm::normalize(back) : glm::dvec2{0.0, 1.0}) + tool.turn, 360.0); tool.points.push_back(point); @@ -799,7 +874,7 @@ void editor_mode::fouling_update() } } // markers standing: each goes to the nearest fouling point, the others are left alone - auto const markers{standing_templates([](std::string const &File) { return File.find("w17") != std::string::npos || File.find("ukres") != std::string::npos; })}; + auto const markers{m_templates.standing_templates([](std::string const &File) { return File.find("w17") != std::string::npos || File.find("ukres") != std::string::npos; })}; for (auto const &marker : markers) { int nearest{-1}; @@ -837,7 +912,7 @@ void editor_mode::fouling_update() tool.status = format(STR_C("%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong"), switches.size(), tool.points.size(), static_cast(missing), static_cast(wrong)); } -void editor_mode::fouling_place(int const Only) +void fouling_tool::place(int const Only) { auto &tool{m_fouling}; fouling_update(); @@ -856,13 +931,13 @@ void editor_mode::fouling_place(int const Only) items.push_back(std::move(item)); } std::string error; - auto const placed{place_templates(items, error)}; + auto const placed{m_templates.place_templates(items, error)}; tool.status = placed > 0 ? format(STR_P("%zu fouling point marker placed, Ctrl+Z takes it back", "%zu fouling point markers placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -void editor_mode::fouling_fix(int const Only) +void fouling_tool::fix(int const Only) { auto &tool{m_fouling}; fouling_update(); @@ -887,13 +962,13 @@ void editor_mode::fouling_fix(int const Only) } moves.emplace_back(found.marker, item); } - auto const moved{move_templates(moves)}; + auto const moved{m_templates.move_templates(moves)}; tool.status = format(STR_P("%zu fouling point marker moved to its place, Ctrl+Z takes it back", "%zu fouling point markers moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -std::optional> editor_mode::fouling_hit() const +std::optional> fouling_tool::hit() const { auto const &tool{m_fouling}; screen_projection const projection; @@ -921,7 +996,7 @@ std::optional> editor_mode::fouling_hit() const return result; } -void editor_mode::render_fouling_ui() +void fouling_tool::render_options() { auto &tool{m_fouling}; auto const changed = [&](bool const Changed) { @@ -937,21 +1012,18 @@ void editor_mode::render_fouling_ui() ImGui::TextWrapped("%s", tool.status.c_str()); auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })}; auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })}; - auto const tall{std::round(ImGui::GetFrameHeight() * 1.3f)}; + auto const tall{tall_button_height()}; if (missing > 0) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(format(STR_P("Place %d marker", "Place %d markers", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall))) - fouling_place(); - ImGui::PopStyleColor(2); + if (main_button(format(STR_P("Place %d marker", "Place %d markers", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall))) + place(); } if (wrong > 0) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.4f, 0.1f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.48f, 0.15f, 1.0f)); if (ImGui::Button(format(STR_P("Move %d wrong marker to its place", "Move %d wrong markers to their places", static_cast(wrong)), static_cast(wrong)).c_str(), ImVec2(-1.0f, tall))) - fouling_fix(); + fix(); ImGui::PopStyleColor(2); } if (missing + wrong > 0) @@ -967,10 +1039,10 @@ void editor_mode::render_fouling_ui() continue; ImGui::PushID(i); if (ImGui::SmallButton(STR_C("Place"))) - fouling_place(i); + place(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: no W17"), point.track->name().c_str()).c_str())) - fly_to(point.position, look, 25.0); + m_view.fly_to(point.position, look, 25.0); ImGui::PopID(); } for (int i = 0; i < static_cast(tool.found.size()); ++i) @@ -980,10 +1052,10 @@ void editor_mode::render_fouling_ui() continue; ImGui::PushID(1000000 + i); if (ImGui::SmallButton(STR_C("Fix"))) - fouling_fix(i); + fix(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: W17 %.1f m off, %+.2f m high"), tool.points[found.point].track->name().c_str(), found.error, found.height_error).c_str())) - fly_to(found.marker.location, look, 25.0); + m_view.fly_to(found.marker.location, look, 25.0); ImGui::PopID(); } ImGui::EndChild(); @@ -1031,10 +1103,10 @@ void editor_mode::render_fouling_ui() tool.tolerance = std::clamp(tool.tolerance, 0.2, 20.0); } -void editor_mode::draw_fouling_overlay() const +void fouling_tool::draw_overlay() const { auto const &tool{m_fouling}; - if (false == tool.open) + if (false == m_tab.open) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; @@ -1063,3 +1135,128 @@ void editor_mode::draw_fouling_overlay() const projection.line(drawlist, found.marker.location, tool.points[found.point].position, IM_COL32(255, 70, 60, 160), 1.5f); } } + +editor_selftest_result hekto_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "hekto") + { + auto values{selftest_values(Words)}; + auto *track{simulation::Paths.find(values["track"])}; + if (track == nullptr) + { + WriteLog("SELFTEST hekto: no such track"); + return editor_selftest_result::handled; + } + start(*track, track->m_paths.front().points[segment_data::point::start]); + auto &tool{m_hekto}; + if (values.count("reach")) + tool.reach = std::stod(values["reach"]); + tool.own_km = values["own"] == "1"; + tool.dirty = true; + hekto_update(); + if (values.count("km")) + { + tool.km = std::stod(values["km"]); + tool.growth = values["growth"] == "-1" ? -1 : 1; + } + tool.side = values["side"] == "alt" ? 0 : values["side"] == "left" ? -1 : 1; + tool.dirty = true; + hekto_update(); + auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; + auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.wrong; })}; + auto const duplicates{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.duplicate; })}; + WriteLog(format("SELFTEST hekto: line %s km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing, %d wrong, %d duplicate. %s", tool.line.empty() ? "-" : tool.line.c_str(), tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), + static_cast(wrong), static_cast(duplicates), tool.status.c_str())); + for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i) + { + auto const &post{tool.posts[i]}; + double lateral; + glm::dvec3 const axis{editor_track::sampled_position(tool.samples, post.chainage)}; + auto const direction{editor_track::sampled_position(tool.samples, post.chainage + 1.0) - axis}; + glm::dvec2 const offset{post.position.x - axis.x, post.position.z - axis.z}; + lateral = glm::length(offset); + auto const right{(direction.x * offset.y - direction.z * offset.x) * post.growth >= 0.0}; + WriteLog(format("SELFTEST hekto post %d.%d at %.1f m, %.2f m from the axis, %s of the growing kilometrage, at %.1f %.1f %.1f", post.hectometres / 10, post.hectometres % 10, post.chainage, lateral, right ? "right" : "left", post.position.x, post.position.y, + post.position.z)); + } + if (values["list"] == "1") + { + std::string list; + for (auto const &post : tool.posts) + list += format(" %d@%.0f", post.hectometres, post.chainage); + WriteLog("SELFTEST hekto posts:" + list); + } + if (values["place"] == "1") + { + hekto_place(); + WriteLog("SELFTEST hekto: " + tool.status); + } + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} + +glm::dvec3 fouling_tool::position_of(std::pair const &Hit) const +{ + return Hit.second ? m_fouling.found[Hit.first].marker.location : m_fouling.points[Hit.first].position; +} + +void fouling_tool::check_selected_switch() +{ + m_fouling.scope = 0; + m_fouling.dirty = true; + m_tab.expand = true; +} + +editor_selftest_result fouling_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "fouling") + { + auto values{selftest_values(Words)}; + auto &tool{m_fouling}; + if (values["show"] == "1") + { + m_view.show_track_tab(editor_track_tab::lineside); + m_tab.expand = true; + } + tool.scope = std::stoi(values.count("scope") ? values["scope"] : "2"); + tool.height_mode = values["mode"] == "ground" ? 0 : 1; + tool.height = std::stod(values.count("height") ? values["height"] : "0"); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling: " + tool.status); + int shown{0}; + for (auto const &found : tool.found) + if (found.wrong && shown++ < 5) + WriteLog(format("SELFTEST fouling: %s %.2f m off, %+.3f m high", found.marker.file.c_str(), found.error, found.height_error)); + if (values["place"] == "one") + { + // the first fouling point without a marker alone, as a click on it in the view + for (int index = 0; index < static_cast(tool.points.size()); ++index) + if (tool.points[index].standing < 0) + { + place(index); + break; + } + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling one: " + tool.status); + } + if (values["place"] == "1") + { + place(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after placing: " + tool.status); + } + if (values["fix"] == "1") + { + fix(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after the fix: " + tool.status); + } + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodelineside.h b/application/editormodelineside.h new file mode 100644 index 00000000..1ace4a1c --- /dev/null +++ b/application/editormodelineside.h @@ -0,0 +1,156 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editortool.h" + +#include +#include +#include +#include + +// hectometre posts along the line, at every 100 m of the kilometrage +class hekto_tool final : public editor_tool +{ + public: + hekto_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates, editor_ground &Ground, editor_guide &Guide) : m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates}, m_ground{Ground}, m_guide{Guide} {} + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + tool_tab &tab() { return m_tab; } + void start(TTrack &Track, glm::dvec3 const &Point); + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_templates &m_templates; + editor_ground &m_ground; + editor_guide &m_guide; + tool_tab m_tab; + struct hekto_post + { + int hectometres{0}; + double chainage{0.0}; + glm::dvec3 position{0.0}; + double yaw{0.0}; + int growth{1}; // 1: the kilometrage grows along the line there, -1: against + int standing{-1}; // post already there with this number + }; + struct hekto_found + { + standing_template post; + int hectometres{0}; + double chainage{0.0}; // where it stands along the line + double expected{0.0}; // where its number belongs + double error{0.0}; // m along the line + double distance{0.0}; // m from where it belongs, in the plan + bool wrong{false}; + bool duplicate{false}; // another post with the same number stands nearer to its place + bool outside{false}; // its number belongs past the ends of the line + int place{-1}; // of the posts, the one with its number nearest to it + }; + struct hekto_state + { + TTrack *track{nullptr}; // the line runs through it + glm::dvec3 point{0.0}; // where the kilometrage is given + double km{0.0}; + bool km_read{false}; // the kilometrage was read from the posts already standing + int growth{1}; // 1: grows along the line, -1: against + std::string line; // of the kilometrage set for the line in the Kilometrage tool the posts follow, empty for the one given here + std::string line_set; // the line the Kilometrage tool sets the kilometrage of at the point, empty for none + bool own_km{false}; // the kilometrage given here, even where the line has one set + int side{1}; // 1: to the right of the growing kilometrage, -1: to the left, 0: alternately + double offset{3.0}; // m from the axis + int style{0}; // nowy, stary, zgnity, mixed + int turn{0}; // degrees added to the rotation + double reach{10000.0}; // m each way + double tolerance{5.0}; // m along the line, for the posts standing + bool dirty{true}; + std::size_t history{0}; + editor_track::route route; + std::vector samples; + double origin{0.0}; // chainage of the point + std::vector posts; + std::vector found; + std::string status; + std::string error; + }; + hekto_state m_hekto; + void hekto_update(); + void hekto_read_kilometrage(); + void hekto_place(); + void hekto_fix(); + void hekto_remove_duplicates(); +}; + +// fouling point markers (W17) where the tracks meeting at a switch are the spacing apart +class fouling_tool final : public editor_tool +{ + public: + fouling_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates, editor_ground &Ground) : m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates}, m_ground{Ground} {} + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + tool_tab &tab() { return m_tab; } + // the markers put at the fouling points without one, or at the one only + void place(int const Only = -1); + // the markers standing wrong moved to their places, or the one found only + void fix(int const Only = -1); + // the fouling point without a marker or the marker standing wrong under the cursor: the index of the point or of the + // marker found, and whether it's a marker + std::optional> hit() const; + glm::dvec3 position_of(std::pair const &Hit) const; + void check_selected_switch(); + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_templates &m_templates; + editor_ground &m_ground; + tool_tab m_tab; + struct fouling_point + { + TTrack *track{nullptr}; // the switch + glm::dvec3 position{0.0}; + double yaw{0.0}; + int standing{-1}; // marker already there + }; + struct fouling_found + { + standing_template marker; + int point{-1}; // the fouling point it belongs to + double error{0.0}; // m from where it belongs + double height_error{0.0}; // m over where it belongs + bool wrong{false}; + }; + struct fouling_state + { + int scope{1}; // 0: the selected switch, 1: around the camera, 2: the whole scenery + double reach{1500.0}; + double spacing{3.5}; // m between the axes + double tolerance{2.0}; // m, a marker farther than this from its point is wrong + int height_mode{1}; // 0: on the ground, 1: at the height given over the rail heads + double height{0.0}; // m over the rail heads + double height_tolerance{0.03}; // m, a marker standing higher or lower than this is wrong + int model{0}; // w17, w17_new, w17_old, w17_dwuteo + int turn{0}; + bool dirty{true}; + std::size_t history{0}; + TTrack *selected{nullptr}; + std::vector points; + std::vector found; + std::string status; + std::string error; + }; + fouling_state m_fouling; + void fouling_update(); +}; diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index a540e010..9285c063 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -14,6 +14,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" #include "application/editormodeshared.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorSettings.hpp" @@ -28,7 +30,10 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include +#include +#include namespace { @@ -139,10 +144,116 @@ bool ground_card(char const *Id, char const *Title, char const *Text, bool const return clicked && Enabled; } -// a bridge or a tunnel keeps the model under it as it is +// 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, 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 +// would be one formation +std::string formation_name(TTrack const &Track) +{ + if (false == Track.name().empty() || Track.m_paths.empty()) + return Track.name(); + auto const &points{Track.m_paths.front().points}; + auto const &start{points[segment_data::point::start]}; + auto const &end{points[segment_data::point::end]}; + return format("@%.2f,%.2f>%.2f,%.2f", start.x, start.z, end.x, end.z); +} + +// an end of a path no other path is linked to +struct loose_end +{ + TTrack *track{nullptr}; + int path{0}; + bool atend{false}; + glm::dvec3 point{0.0}; + glm::dvec3 outward{0.0}; // off the path along it +}; + +loose_end end_of(TTrack &Track, int const Path, bool const Atend) +{ + auto const &points{Track.m_paths[Path].points}; + auto const &point{points[Atend ? segment_data::point::end : segment_data::point::start]}; + auto const &control{points[Atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const &other{points[Atend ? segment_data::point::start : segment_data::point::end]}; + 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() +{ + double constexpr kLevel{2.0}; // m + auto const linked = [](TTrack const &Track, int const Path, bool const Atend) { + if (Track.eType == tt_Switch && Track.SwitchExtension) + return (Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]) != nullptr; + return (Atend ? Track.trNext : Track.trPrev) != nullptr; + }; + std::map, std::vector> cells; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || track->Length() < 0.01) + continue; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const atend : {false, true}) + if (false == linked(*track, i, atend)) + { + auto const end{end_of(*track, i, atend)}; + cells[{static_cast(std::floor(end.point.x)), static_cast(std::floor(end.point.z))}].push_back(end); + } + } + std::vector> result; + std::set> taken; + for (auto const &[cell, ends] : cells) + for (auto const &a : ends) + { + if (taken.contains({a.track, a.path, a.atend})) + continue; + loose_end const *match{nullptr}; + for (int dx = -1; dx <= 1 && match == nullptr; ++dx) + for (int dz = -1; dz <= 1 && match == nullptr; ++dz) + { + auto const found{cells.find({cell.first + dx, cell.second + dz})}; + if (found == cells.end()) + continue; + auto const outward{geometry::plan_of(a.outward)}; + for (auto const &b : found->second) + if (b.track != a.track && false == taken.contains({b.track, b.path, b.atend}) && geometry::plan_distance(a.point, b.point) <= geometry::same_point && + std::abs(a.point.y - b.point.y) <= kLevel && glm::dot(outward, geometry::plan_of(b.outward)) < -0.5 * glm::length(outward) * glm::length(geometry::plan_of(b.outward))) + { + match = &b; + break; + } + } + if (match == nullptr) + continue; + taken.insert({a.track, a.path, a.atend}); + taken.insert({match->track, match->path, match->atend}); + result.emplace_back(a, *match); + } + return result; } } // namespace @@ -239,6 +350,27 @@ std::vector editor_mode::nmt_track_places(double const S for (std::size_t i = 0; i < result.size(); ++i) if (over[i] != 0) result[i].shapes = false; + // nor where the grade lines went over the model falling away by the abutments of a bridge or a viaduct + double constexpr bridged_near{1.0}; + double constexpr bridged_level{1.0}; + auto const &bridged{m_nmt_state.bridged}; + if (false == bridged.empty()) + for (auto &place : result) + { + if (false == place.shapes) + continue; + auto const cx{static_cast(std::floor(place.position.x / kPlanCell))}; + auto const cz{static_cast(std::floor(place.position.z / kPlanCell))}; + for (int dx = -1; dx <= 1 && place.shapes; ++dx) + for (int dz = -1; dz <= 1 && place.shapes; ++dz) + if (auto const cell{bridged.find({cx + dx, cz + dz})}; cell != bridged.end()) + for (auto const &point : cell->second) + if (glm::length(glm::dvec2{point.x - place.position.x, point.z - place.position.z}) <= bridged_near && std::abs(point.y - place.position.y) <= bridged_level) + { + place.shapes = false; + break; + } + } return result; } @@ -252,6 +384,7 @@ void editor_mode::nmt_fit() { nmt_locate(); auto &state{m_nmt_state}; + state.error.clear(); state.under = false; state.fit.clear(); auto const places{nmt_track_places(25.0)}; @@ -276,6 +409,7 @@ void editor_mode::nmt_under_start() { nmt_locate(); auto &state{m_nmt_state}; + state.error.clear(); state.fit.clear(); state.anchors.clear(); state.anchor_cells.clear(); @@ -395,7 +529,12 @@ std::vector editor_mode::nmt_keys_around() const auto const first{-(m_terrain_add_count - 1) / 2}; for (int dz = 0; dz < m_terrain_add_count; ++dz) for (int dx = 0; dx < m_terrain_add_count; ++dx) - result.push_back({centre.first + first + dx, centre.second + first + dz}); + { + heightmap::chunk_key const key{centre.first + first + dx, centre.second + first + dz}; + // none outside the download area + if (auto const low{heightmap::chunk_corner(key)}; in_download_area(low, low + glm::dvec2{heightmap::chunk_size})) + result.push_back(key); + } return result; } @@ -413,14 +552,14 @@ std::vector editor_mode::nmt_keys_along_tracks() const auto const low{heightmap::chunk_corner(key)}; // the chunk reaches within the band of the track glm::dvec2 const nearest{std::clamp(place.position.x, low.x, low.x + heightmap::chunk_size), std::clamp(place.position.z, low.y, low.y + heightmap::chunk_size)}; - if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band) + if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band && in_download_area(low, low + glm::dvec2{heightmap::chunk_size})) keys.insert(key); } } return {keys.begin(), keys.end()}; } -void editor_mode::nmt_make(std::vector const &Keys, bool const Rebuild) +void editor_mode::nmt_make(std::vector const &Keys, bool const Reshape) { auto &state{m_nmt_state}; if (false == ensure_terrain()) @@ -429,43 +568,195 @@ void editor_mode::nmt_make(std::vector const &Keys, bool c return; } nmt_locate(); - if (Rebuild) - { - // the terrain made for the tracks before, as they lay then: the formation of the earlier runs, and the chunks along them, - // whose heights went with it - auto const dropped{m_streamer.drop_modifiers("nmt ")}; - std::size_t removed{0}; - for (auto const &key : Keys) - if (m_streamer.exists(key)) - { - m_streamer.remove_chunk(key.first, key.second); - ++removed; - } - forget_ground(); - WriteLog(format("Editor: NMT - made anew: %zu chunks, %zu formations of the tracks forgotten", removed, dropped), logtype::generic); - } + 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; + state.error.clear(); if (queued > 0) + { state.grown = false; - // the terrain comes with its photos - if (queued > 0 && false == m_orthophoto.enabled()) + state.reshape = Reshape; + } + // 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 + if (Reshape && queued == 0 && state.shape) + { + nmt_shape_tracks(); + state.status += format(STR_P("; the formation of %zu path shaped", "; the formation of %zu paths shaped", state.shaped), state.shaped); + } } -bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) +bool editor_mode::nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max) +{ + return m_nmt.request(Min, Max) && false == m_nmt.retrying(Min, Max); +} + +std::optional editor_mode::nmt_terrain_offset(std::vector const &Points, bool &Foreign) const +{ + Foreign = false; + std::vector differences; + for (auto const &point : Points) + if (double terrain, model; m_streamer.height_at(point.x, point.y, terrain) && m_nmt.height(point.x, point.y, model)) + differences.push_back(terrain - model); + if (differences.size() < 5) + return std::nullopt; + std::nth_element(differences.begin(), differences.begin() + differences.size() / 2, differences.end()); + auto const median{differences[differences.size() / 2]}; + // the formations of the tracks and the touch-ups of the brushes leave the terrain here and there, the rest keeps to the model + auto const along{std::count_if(differences.begin(), differences.end(), [&](double const Difference) { return std::abs(Difference - median) <= 0.3; })}; + if (static_cast(along) < 0.7 * static_cast(differences.size())) + { + Foreign = true; + return std::nullopt; + } + return median; +} + +std::optional editor_mode::nmt_seam_offset(heightmap::chunk_key const &Key, bool &Foreign) const +{ + auto const low{heightmap::chunk_corner(Key)}; + auto const size{heightmap::chunk_size}; + int constexpr steps{16}; + struct side + { + int dx; + int dz; + glm::dvec2 from; + glm::dvec2 to; + }; + side const sides[]{{-1, 0, low, {low.x, low.y + size}}, {1, 0, {low.x + size, low.y}, low + glm::dvec2{size}}, {0, -1, low, {low.x + size, low.y}}, {0, 1, {low.x, low.y + size}, low + glm::dvec2{size}}}; + std::vector points; + for (auto const &edge : sides) + if (m_streamer.exists({Key.first + edge.dx, Key.second + edge.dz})) + for (int i = 0; i <= steps; ++i) + points.push_back(edge.from + (edge.to - edge.from) * (static_cast(i) / steps) + glm::dvec2{edge.dx, edge.dz} * 0.01); + return nmt_terrain_offset(points, Foreign); +} + +void editor_mode::nmt_track_changed(std::vector const &Tracks) +{ + auto &state{m_nmt_state}; + if (state.restoring || state.laying || state.stage != nmt_state::automatic::idle || Tracks.empty() || false == m_streamer.active()) + return; + for (auto *track : Tracks) + if (track != nullptr) + state.changed.insert(track); + state.changed_time = ImGui::GetTime(); +} + +void editor_mode::nmt_track_gone(TTrack const &Track) +{ + auto &state{m_nmt_state}; + if (state.restoring || state.stage != nmt_state::automatic::idle || Track.name().empty() || false == m_streamer.active()) + return; + state.gone.insert(Track.name()); + state.changed_time = ImGui::GetTime(); +} + +void editor_mode::nmt_follow_tracks() +{ + auto &state{m_nmt_state}; + if (state.changed.empty() && state.gone.empty()) + return; + if (false == state.follow) + { + state.changed.clear(); + state.gone.clear(); + return; + } + if (state.stage != nmt_state::automatic::idle || state.fitting || ImGui::GetTime() - state.changed_time < 0.3 || ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right)) + return; + // laying on, from the end of the track laid or dragged from a free end, the terrain waits for the laying to end: shaping it + // goes through every chunk under the tracks, a stall each click + if ((m_lay.active && false == m_lay.points.empty()) || m_extend.active) + return; + if (false == m_streamer.active()) + { + state.changed.clear(); + state.gone.clear(); + return; + } + auto const &modifiers{m_streamer.modifiers()}; + auto const of = [](heightmap::modifier const &Modifier, std::string const &Track) { return Modifier.name == "nmt " + Track || Modifier.name.starts_with("nmt " + Track + " "); }; + std::vector dropped; + for (auto const &name : state.gone) + if (simulation::Paths.find(name) == nullptr) + for (auto const &modifier : modifiers) + if (of(modifier, name)) + dropped.push_back(modifier.name); + state.gone.clear(); + // the tracks with a formation take it along; one with none leaves the terrain as it is, until it's drawn to the track + std::vector shaped; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == state.changed.contains(track) || track->m_editorremoved || false == rail(*track) || track->name().empty()) + continue; + if (std::any_of(modifiers.begin(), modifiers.end(), [&](heightmap::modifier const &Modifier) { return of(Modifier, track->name()); })) + shaped.push_back(track); + } + state.changed.clear(); + if (dropped.empty() && shaped.empty()) + return; + // the terrain follows in the step of the history of the change of the tracks + terrain_history_step(); + auto const *top{m_history.empty() ? nullptr : m_history.back().terrain.get()}; + for (auto const &name : dropped) + m_streamer.remove_modifier(name); + auto const laid{shaped.empty() ? std::size_t{0} : nmt_shape_tracks(&shaped)}; + if (false == dropped.empty()) + forget_ground(); + terrain_history_step(); + auto const count{m_history.size()}; + auto const of_tracks = [](EditorSnapshot const &Step) { return Step.action == EditorSnapshot::Action::TrackEdit || (Step.action == EditorSnapshot::Action::Terrain && Step.joined); }; + if (count >= 2 && m_history.back().action == EditorSnapshot::Action::Terrain && m_history.back().terrain.get() != top && of_tracks(m_history[count - 2])) + m_history.back().joined = true; + WriteLog(format("Editor: NMT - the terrain followed the tracks: %zu formations laid, %zu taken away", laid, dropped.size()), logtype::generic); +} + +bool editor_mode::nmt_ahead(double const X, double const Z, double &Height) const +{ + auto const &state{m_nmt_state}; + if (false == state.grow || false == state.ahead.has_value() || false == m_nmt.placed() || false == m_nmt.height(X, Z, Height)) + return false; + Height += *state.ahead; + return true; +} + +bool editor_mode::nmt_ground(double const X, double const Z, double &Height) const +{ + if (nmt_ahead(X, Z, Height)) + return true; + if (false == m_nmt.placed() || false == m_nmt.height(X, Z, Height)) + return false; + Height += nmt_offset_at(X, Z); + return true; +} + +editor_mode::nmt_chunk editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional const Offset) { auto const low{heightmap::chunk_corner(Key)}; auto const high{low + glm::dvec2{heightmap::chunk_size}}; - if (false == m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0})) - return false; + if (false == nmt_ready(low - glm::dvec2{2.0}, high + glm::dvec2{2.0})) + return nmt_chunk::waiting; auto const cells{heightmap::cells_for(m_terrain_spacing)}; auto const side{static_cast(cells) + 1}; auto const step{heightmap::chunk_size / cells}; @@ -476,7 +767,7 @@ bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) std::array offsets; for (int iz = 0; iz <= coarse; ++iz) for (int ix = 0; ix <= coarse; ++ix) - offsets[iz * (coarse + 1) + ix] = nmt_offset_at(low.x + ix * heightmap::chunk_size / coarse, low.y + iz * heightmap::chunk_size / coarse); + offsets[iz * (coarse + 1) + ix] = Offset.has_value() ? *Offset : nmt_offset_at(low.x + ix * heightmap::chunk_size / coarse, low.y + iz * heightmap::chunk_size / coarse); auto const offset_at = [&](double const X, double const Z) { auto const u{std::clamp((X - low.x) / heightmap::chunk_size * coarse, 0.0, static_cast(coarse))}; auto const v{std::clamp((Z - low.y) / heightmap::chunk_size * coarse, 0.0, static_cast(coarse))}; @@ -499,15 +790,44 @@ bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) any = true; } } + if (false == any) + return nmt_chunk::empty; // where the model has no data the chunk goes on level with its nearest point - if (any) - fill_from_nearest(heights, found, side); - m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, [&](double const X, double const Z) { + fill_from_nearest(heights, found, side); + 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; }); - 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) @@ -525,6 +845,7 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) double constexpr step{5.0}; std::pair previous{nullptr, -1}; bool broken{false}; + std::vector over; for (auto const &place : nmt_track_places(step)) { if (false == wanted(place.track)) @@ -536,6 +857,7 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) if (false == place.shapes) { broken = true; + over.push_back(place); continue; } auto &piece{pieces[path]}; @@ -547,18 +869,46 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) auto &modifier{modifiers[{place.track, place.path, piece}]}; if (modifier.name.empty()) { - modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}) + (piece > 0 ? " #" + std::to_string(piece) : std::string{}); + modifier.name = "nmt " + formation_name(*place.track) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}) + (piece > 0 ? " #" + std::to_string(piece) : std::string{}); modifier.slope = state.slope; modifier.reach = state.reach; } modifier.points.push_back({{place.position.x, place.position.z}, place.formation, place.half_width}); } + // a formation which ends by a bridge or a tunnel ends square against its abutment, its portal, whichever path goes on over it + auto const walled = [&](heightmap::corridor_point const &End) { + return std::any_of(over.begin(), over.end(), [&](nmt_place const &Place) { + return glm::distance(glm::dvec2{Place.position.x, Place.position.z}, End.position) <= step * 1.5 && std::abs(Place.formation - End.formation) <= 1.0; + }); + }; std::vector laid; for (auto &[path, modifier] : modifiers) if (modifier.points.size() >= 2) + { + modifier.open_start = walled(modifier.points.front()); + modifier.open_end = walled(modifier.points.back()); laid.push_back(std::move(modifier)); + } if (Only != nullptr) { + // the formations as they are already aren't led into the terrain again + auto const same = [](heightmap::modifier const &A, heightmap::modifier const &B) { + return A.slope == B.slope && A.reach == B.reach && A.rounding == B.rounding && A.open_start == B.open_start && A.open_end == B.open_end && A.points.size() == B.points.size() && + std::equal(A.points.begin(), A.points.end(), B.points.begin(), [](heightmap::corridor_point const &P, heightmap::corridor_point const &Q) { + return glm::distance(P.position, Q.position) < 1e-6 && std::abs(P.formation - Q.formation) < 1e-6 && std::abs(P.half_width - Q.half_width) < 1e-6; + }); + }; + auto const &existing{m_streamer.modifiers()}; + // the formations the tracks have, the ones as they are already too: those only aren't led again, nor taken out as gone + std::unordered_set kept; + for (auto const &modifier : laid) + kept.insert(modifier.name); + std::erase_if(laid, [&](heightmap::modifier const &Modifier) { + auto const found{std::find_if(existing.begin(), existing.end(), [&](heightmap::modifier const &Other) { return Other.name == Modifier.name; })}; + return found != existing.end() && same(*found, Modifier); + }); + for (auto *track : *Only) + state.changed.erase(track); // the modifiers the tracks don't have any more: of the pieces of a path gone, of the tracks removed std::vector gone; for (auto const *track : *Only) @@ -567,24 +917,46 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) continue; auto const own{"nmt " + track->name()}; for (auto const &modifier : m_streamer.modifiers()) - if ((modifier.name == own || modifier.name.starts_with(own + " ")) && std::none_of(laid.begin(), laid.end(), [&](heightmap::modifier const &Laid) { return Laid.name == modifier.name; })) + if ((modifier.name == own || modifier.name.starts_with(own + " ")) && false == kept.contains(modifier.name)) gone.push_back(modifier.name); } for (auto const &name : gone) m_streamer.remove_modifier(name); } + if (Only == nullptr) + { + // the formations of the tracks there are no more, or are on bridges now, and of the tracks with no name laid over each + // other under one name before + std::vector gone; + for (auto const &modifier : m_streamer.modifiers()) + if (modifier.name.starts_with("nmt ") && std::none_of(laid.begin(), laid.end(), [&](heightmap::modifier const &Laid) { return Laid.name == modifier.name; })) + gone.push_back(modifier.name); + for (auto const &name : gone) + m_streamer.remove_modifier(name); + } auto const count{laid.size()}; m_streamer.modifiers(std::move(laid)); forget_ground(); if (Only == nullptr) + { state.shaped = modifiers.size(); + state.changed.clear(); + state.gone.clear(); + } return count; } void editor_mode::nmt_update() { auto &state{m_nmt_state}; + // the squares of the model in the download area to the disk cache, where the scenery is placed and its terrain is there + if (auto const wanted{m_nmt.placed() && m_streamer.active() ? m_orthophoto.area() : std::vector{}}; wanted != m_nmt_prefetched) + { + m_nmt_prefetched = wanted; + m_nmt.prefetch(wanted); + } nmt_offer(); + nmt_follow_tracks(); if (state.stage != nmt_state::automatic::idle) { nmt_auto_step(); @@ -594,7 +966,7 @@ void editor_mode::nmt_update() { bool all{true}; for (auto const &place : state.fit) - all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; + all = nmt_ready(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; if (all) nmt_finish_fit(); return; @@ -602,52 +974,125 @@ void editor_mode::nmt_update() nmt_grow(); if (state.pending.empty()) return; - // a few chunks a frame, those whose model is in + // a few chunks a frame, those whose model is in. the terrain made ahead of the track laid isn't a step of the history: + // Ctrl+Z takes back the track, not the ground under it + auto const unrecorded{state.grown && m_streamer.recording()}; + if (unrecorded) + { + terrain_history_step(); + m_streamer.end_change(); + } 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; } - if (add_chunk_from_nmt(*it)) + // 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 && false == refilled) { - it = state.pending.erase(it); - ++made; + bool foreign{false}; + offset = nmt_seam_offset(*it, foreign); + if (foreign) + { + state.given_up.insert(*it); + it = state.pending.erase(it); + ++state.foreign; + ++made; + continue; + } + if (false == offset.has_value()) + { + ++apart; + ++it; + continue; + } + } + auto const chunk{add_chunk_from_nmt(*it, offset)}; + if (chunk == nmt_chunk::waiting) + { + ++it; + continue; + } + 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) ++state.made; - continue; - } - ++it; + else + ++state.empty; + } + // the chunks ahead which no terrain made from the model touches any more are given up + if (made == 0 && apart > 0 && apart == state.pending.size()) + { + 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); + if (state.foreign > 0) + state.status += format(STR_P("; %zu chunk not made ahead, the terrain beside isn't the real one", "; %zu chunks not made ahead, the terrain beside isn't the real one", state.foreign), state.foreign); if (state.pending.empty()) { - if (state.shape && false == state.grown) + if (state.shape && state.reshape) { nmt_shape_tracks(); state.status += format(STR_P("; the formation of %zu path shaped", "; the formation of %zu paths shaped", state.shaped), state.shaped); } // the photos laid onto the new ground m_orthophoto.refit(); + for (auto &layer : m_wms) + layer->refit(); first_step_done(first_step::nmt); } } - if (auto const error{m_nmt.error()}; false == error.empty()) - state.error = STR(error); + if (unrecorded) + m_streamer.begin_change(); } void editor_mode::nmt_grow() { auto &state{m_nmt_state}; auto const now{ImGui::GetTime()}; - if (false == state.grow || now - state.grow_time < 0.5 || (false == m_lay.active && false == m_extend.active) || m_streamer.resident() == 0 || false == m_nmt.placed()) + if (false == state.grow || now - state.grow_time < 0.5 || false == m_nmt.placed()) return; state.grow_time = now; + // 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.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 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}); + 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 auto const reach{static_cast(std::ceil(grow_reach / heightmap::chunk_size))}; @@ -660,7 +1105,8 @@ void editor_mode::nmt_grow() auto const low{heightmap::chunk_corner(key)}; glm::dvec2 const nearest{std::clamp(cursor.x, low.x, low.x + heightmap::chunk_size), std::clamp(cursor.y, low.y, low.y + heightmap::chunk_size)}; auto const distance{glm::distance(nearest, cursor)}; - if (distance <= grow_reach && false == m_streamer.exists(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end()) + if (distance <= grow_reach && false == m_streamer.exists(key) && false == state.given_up.contains(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end() && + in_download_area(low, low + glm::dvec2{heightmap::chunk_size})) missing.emplace_back(distance, key); } if (missing.empty()) @@ -670,171 +1116,710 @@ void editor_mode::nmt_grow() { state.grown = true; state.made = 0; + state.empty = 0; + state.foreign = 0; } 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); } -void editor_mode::render_nmt_ui() +bool editor_mode::nmt_busy() const +{ + auto const &state{m_nmt_state}; + return state.fitting || state.stage != nmt_state::automatic::idle || (false == state.pending.empty() && false == state.grown); +} + +void editor_mode::nmt_stop() { auto &state{m_nmt_state}; - nmt_locate(); - auto const busy{state.fitting || state.stage != nmt_state::automatic::idle}; - step_heading(1, STR_C("Location")); - if (false == m_nmt.placed() || state.moving) - { - // where the scenery is, right here: the terrain model and the orthophoto take it - ImGui::PushTextWrapPos(0.0f); - ImGui::TextDisabled("%s", STR_C("Where the point 0,0 of the scenery lies on the map:")); - ImGui::PopTextWrapPos(); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputDouble("X##nmtplacenorth", &state.north, 0.0, 0.0, "%.0f"); - item_tooltip("X of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the north, as geoportal.gov.pl gives it"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputDouble("Y##nmtplaceeast", &state.east, 0.0, 0.0, "%.0f"); - item_tooltip("Y of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the east, as geoportal.gov.pl gives it"); - ImGui::BeginDisabled(state.north < 100000.0 || state.east < 100000.0); - if (ImGui::Button(STR_C("The scenery is there"))) - { - nmt_place_scenery(); - state.moving = false; - } - ImGui::EndDisabled(); - if (state.moving) - { - ImGui::SameLine(); - if (ImGui::Button(STR_C("Cancel"))) - state.moving = false; - } - if (false == m_nmt.placed()) - return; - } - else - { - auto const &config{m_orthophoto.settings()}; - ImGui::Text(STR_C("N %.0f, E %.0f"), config.north, config.east); - ImGui::SameLine(); - ImGui::TextDisabled("%s", STR_C("PUWG 1992")); - ImGui::SameLine(); - ImGui::BeginDisabled(busy); - if (ImGui::SmallButton(STR_C("Change..."))) - { - state.north = config.north; - state.east = config.east; - state.moving = true; - } - ImGui::EndDisabled(); - item_tooltip("The place is shared with the orthophoto"); - } + if (false == nmt_busy()) + return; + auto const lines{state.stage == nmt_state::automatic::fitting && state.lines > 0}; + m_nmt.cancel(); + state.fitting = false; + state.fit.clear(); + state.probes.clear(); + state.left.clear(); + state.done.clear(); + 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) + state.status += STR("; so do the grade lines fitted, Ctrl+Z takes them back"); + WriteLog("Editor: NMT - " + state.status, logtype::generic); +} - auto const tracks{std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })}; - step_heading(2, tracks ? STR_C("What gives way") : STR_C("Terrain")); - if (tracks) - { - auto const spacing{ImGui::GetStyle().ItemSpacing.x}; - auto const width{std::floor((ImGui::GetContentRegionAvail().x - spacing) * 0.5f)}; - char const *const titles[]{STR_C("Tracks onto the terrain"), STR_C("Terrain under the tracks")}; - char const *const texts[]{STR_C("The tracks go down or up onto the real ground: grade lines fitted to it within the limits, switches level with their tracks"), - STR_C("The tracks stay where they are; the real ground is drawn to them and keeps its relief farther away")}; - auto const wrap{width - 16.0f}; - auto const textheight{std::max(ImGui::CalcTextSize(texts[0], nullptr, false, wrap).y, ImGui::CalcTextSize(texts[1], nullptr, false, wrap).y)}; - for (int index = 0; index < 2; ++index) - { - if (index > 0) - ImGui::SameLine(); - if (nmt_choice_card(index, titles[index], texts[index], width, textheight, state.mode == index) && false == busy) - state.mode = index; - } - } - else - { - ImGui::PushTextWrapPos(0.0f); - ImGui::TextDisabled("%s", STR_C("No tracks yet: the terrain is made around the camera, the tracks laid then go along it")); - ImGui::PopTextWrapPos(); - } - - step_heading(3, STR_C("Load")); - if (tracks && m_streamer.resident() > 0) - { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.95f, 0.75f, 0.3f, 1.0f)); - ImGui::TextWrapped("%s", STR_C("The scenery has terrain already: along the tracks it's made anew from the model, its painting too")); - ImGui::PopStyleColor(); - } - ImGui::BeginDisabled(busy); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.45f, 0.75f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.55f, 0.88f, 1.0f)); - char const *label{tracks ? (state.mode == 0 ? STR_C("Load the terrain, put the tracks onto it") : STR_C("Load the terrain under the tracks")) : STR_C("Load the terrain around the camera")}; - if (ImGui::Button(label, ImVec2(-1.0f, ImGui::GetFrameHeight() * 1.5f))) - { - if (false == tracks) - { - state.offset = 0.0; - nmt_make(nmt_keys_around()); - } - else if (state.mode == 0) - nmt_auto_start(); - else - nmt_under_start(); - } - ImGui::PopStyleColor(2); - ImGui::EndDisabled(); - if (ImGui::IsItemHovered()) - { - auto const *text{tracks ? (state.mode == 0 ? STR_C("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") : - STR_C("The tracks aren't touched: the terrain model is loaded along them and drawn to them, at each place by as much as the tracks lie above or below it there, fading out away from them, so its relief stays and the tracks don't end up in deep cuttings; then the formation and the slopes along them. For a scenery built level or at its own heights")) : - STR_C("Loads the terrain model, the real relief of the ground, around the camera")}; - ImGui::SetTooltip("%s\n%s", text, STR_C("Terrain model: GUGiK, geoportal.gov.pl")); - } - toggle_chips("nmtgrow", {{STR_C("Make the terrain ahead while laying"), &state.grow, "Where a track is laid past the edge of the terrain, the terrain model is made around the cursor as it goes"}}); - // how far it got +void editor_mode::render_nmt_progress(bool const Compact) +{ + auto &state{m_nmt_state}; + float fraction{0.0f}; + std::string text; if (state.stage == nmt_state::automatic::fetching || state.fitting) { auto const waiting{m_nmt.waiting()}; - ImGui::ProgressBar(-1.0f * static_cast(ImGui::GetTime()), ImVec2(-1.0f, 0.0f), format(STR_P("Terrain model: %zu square left", "Terrain model: %zu squares left", waiting), waiting).c_str()); + fraction = -1.0f * static_cast(ImGui::GetTime()); + text = format(STR_P("Terrain model: %zu square left", "Terrain model: %zu squares left", waiting), waiting); } else if (state.stage == nmt_state::automatic::fitting && state.tracks > 0) { - auto const fraction{1.0f - static_cast(state.left.size()) / static_cast(std::max(state.tracks, state.left.size()))}; - ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f), format(STR_P("Grade lines: %zu fitted, %zu track left", "Grade lines: %zu fitted, %zu tracks left", state.left.size()), state.lines, state.left.size()).c_str()); + fraction = 1.0f - static_cast(state.left.size()) / static_cast(std::max(state.tracks, state.left.size())); + text = format(STR_P("Grade lines: %zu fitted, %zu track left", "Grade lines: %zu fitted, %zu tracks left", state.left.size()), state.lines, state.left.size()); } else if (false == state.pending.empty()) { - auto const total{state.made + state.pending.size()}; - ImGui::ProgressBar(static_cast(state.made) / static_cast(std::max(1, total)), ImVec2(-1.0f, 0.0f), format(STR_C("Chunks: %zu of %zu"), state.made, total).c_str()); + auto const total{state.made + state.empty + state.pending.size()}; + fraction = static_cast(state.made + state.empty) / static_cast(std::max(1, total)); + text = format(STR_C("Chunks: %zu of %zu"), state.made + state.empty, total); } + else + return; + auto const stoppable{nmt_busy()}; + auto const stop{STR_C("Stop")}; + auto const button{ImGui::CalcTextSize(stop).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + auto const width{Compact ? std::round(ImGui::GetFontSize() * 16.0f) : stoppable ? -(button + ImGui::GetStyle().ItemSpacing.x) : -FLT_MIN}; + ImGui::ProgressBar(fraction, ImVec2(width, Compact ? 0.0f : tall_button_height()), text.c_str()); + if (false == stoppable) + return; + ImGui::SameLine(); + if (ImGui::Button(stop, ImVec2(0.0f, Compact ? 0.0f : tall_button_height()))) + nmt_stop(); + item_tooltip("Stops loading: the terrain made so far stays"); +} + +void editor_mode::nmt_pull_terrain(std::vector const &Tracks) +{ + auto &state{m_nmt_state}; + std::vector rails; + for (auto *track : Tracks) + if (track != nullptr && false == track->m_editorremoved && rail(*track)) + rails.push_back(track); + if (rails.empty()) + { + state.status = STR("No tracks to draw the terrain to"); + return; + } + if (false == ensure_terrain()) + { + state.status = STR("The heightmap terrain can't be opened"); + return; + } + terrain_history_step(); + auto const laid{nmt_shape_tracks(&rails)}; + terrain_history_step(); + state.status = format(STR_P("The terrain drawn to %zu track: %zu formations led into it, Ctrl+Z takes it back", "The terrain drawn to %zu tracks: %zu formations led into it, Ctrl+Z takes it back", rails.size()), rails.size(), laid); + WriteLog("Editor: NMT - " + state.status, logtype::generic); +} + +void editor_mode::nmt_restore_terrain(std::vector const &Tracks) +{ + auto &state{m_nmt_state}; + if (false == m_streamer.active()) + return; + std::vector gone; + for (auto const *track : Tracks) + { + if (track == nullptr) + continue; + auto const own{"nmt " + formation_name(*track)}; + for (auto const &modifier : m_streamer.modifiers()) + if ((modifier.name == own || modifier.name.starts_with(own + " ")) && std::find(gone.begin(), gone.end(), modifier.name) == gone.end()) + gone.push_back(modifier.name); + } + terrain_history_step(); + for (auto const &name : gone) + m_streamer.remove_modifier(name); + if (false == gone.empty()) + forget_ground(); + terrain_history_step(); + state.status = format(STR_P("%zu formation taken out of the terrain, Ctrl+Z takes it back", "%zu formations taken out of the terrain, Ctrl+Z takes it back", gone.size()), gone.size()); + WriteLog("Editor: NMT - " + state.status, logtype::generic); +} + +void editor_mode::render_track_terrain(std::vector const &Tracks) +{ + auto &state{m_nmt_state}; + // a scenery with no terrain made nor any place on the map has nothing to draw to its tracks + if (false == m_streamer.active() && false == m_nmt.placed()) + return; + if (false == ImGui::CollapsingHeader(format("%s###trackterrain", STR_C("Terrain")).c_str())) + return; + auto const busy{nmt_busy()}; + auto const spacing{ImGui::GetStyle().ItemSpacing.x}; + auto const half{std::floor((ImGui::GetContentRegionAvail().x - spacing) * 0.5f)}; + { + std::size_t rails{0}, done{0}; + for (auto const *track : Tracks) + if (track != nullptr && rail(*track)) + { + ++rails; + if (grade_fitted(*track)) + ++done; + } + if (rails == 1) + ImGui::TextDisabled("%s", done > 0 ? STR_C("Its grade line is fitted to the terrain") : STR_C("Its grade line isn't fitted to the terrain yet")); + else if (rails > 1) + ImGui::TextDisabled(STR_C("Grade lines fitted to the terrain: %zu of %zu"), done, rails); + item_tooltip("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"); + } + ImGui::BeginDisabled(busy); + if (ImGui::Button(STR_C("Draw the terrain to the track"), ImVec2(half, 0.0f))) + nmt_pull_terrain(Tracks); + item_tooltip("The formation and the slopes of the selected tracks led into the terrain, down or up to it; elsewhere the terrain stays as it is"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("The whole run"), ImVec2(-FLT_MIN, 0.0f))) + { + m_track_set = Tracks; + track_set_grow(); + select_tracks(m_track_set); + nmt_pull_terrain(m_track_set); + } + item_tooltip("The tracks joined to the selected ones added to the selection, by the rules of the spread, and the terrain drawn to all of them"); + if (ImGui::Button(STR_C("Restore the terrain"), ImVec2(half, 0.0f))) + nmt_restore_terrain(Tracks); + item_tooltip("The formations of the selected tracks taken out of the terrain: the ground goes back to how it was before them"); + ImGui::EndDisabled(); + toggle_chips("nmtfollow", {{STR_C("The terrain follows the tracks edited"), &state.follow, "A track with its formation moved or changed takes it along; off: the terrain stays as it is until it's drawn to the tracks again"}}); + ImGui::Separator(); + ImGui::TextDisabled("%s", STR_C("All the tracks of the scenery")); + render_nmt_all_tracks(false); + if (busy) + render_nmt_progress(true); if (false == state.status.empty()) { ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", state.status.c_str()); ImGui::PopTextWrapPos(); } - if (false == state.error.empty()) +} + +// the tracks of the whole scenery and the terrain model: the way to take first big, what it does told under it, then the other +// two, each with the case it's for; step 3 of the terrain model in the field of the terrain (Terrain_panel), and in the panel of +// the tracks +void editor_mode::render_nmt_all_tracks(bool const Terrain_panel) +{ + auto &state{m_nmt_state}; + auto const busy{nmt_busy()}; + auto const placed{m_nmt.placed()}; + std::size_t unfitted{0}, all{0}; + for (auto const *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && rail(*track)) + { + ++all; + if (false == grade_fitted(*track)) + ++unfitted; + } + auto const told{[](char const *Text) { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", Text); + ImGui::PopTextWrapPos(); + }}; + // 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(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) { ImGui::PushTextWrapPos(0.0f); - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", state.error.c_str()); + 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(); } - ImGui::PushID("nmtmore"); - if (tracks && ImGui::TreeNode(STR_C("More settings"))) + 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 || 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(); + ImGui::BeginDisabled(busy || false == placed || all == 0); + if (ImGui::Button(STR_C("Terrain under the tracks"), ImVec2(-FLT_MIN, 0.0f))) + nmt_under_start(); + item_tooltip("The tracks aren't touched: the terrain model is loaded along them and drawn to them, at each place by as much as the tracks lie above or below it there, fading out away from them, so its relief stays and the tracks don't end up in deep cuttings; then the formation and the slopes along them. For a scenery built level or at its own heights"); + ImGui::EndDisabled(); + told(STR_C("The tracks stay at their heights and the terrain comes up or down to them")); + if (Terrain_panel) { - if (begin_fields("##nmtband", 0.5f)) + ImGui::Spacing(); + told(STR_C("The terrain to some of the tracks only: select them (F3), Terrain in the inspector")); + } +} + +void editor_mode::draw_nmt_pending() const +{ + auto const &state{m_nmt_state}; + auto const imagery{m_orthophoto.waiting()}; + if (state.pending.empty() && imagery.empty()) + return; + // where the ground will be: by the terrain there or beside it, else the model with the offset of the scenery, else level + // with the nearest chunk there is, or the nearest track + glm::dvec2 const camera{Camera.Pos.x, Camera.Pos.z}; + double fallback{0.0}; + { + auto nearest{std::numeric_limits::max()}; + std::vector chunks; + m_streamer.collect(chunks); + for (auto const *chunk : chunks) + if (auto const middle{chunk->centre()}; glm::distance(glm::dvec2{middle.x, middle.z}, camera) < nearest) + { + nearest = glm::distance(glm::dvec2{middle.x, middle.z}, camera); + fallback = middle.y; + } + if (chunks.empty()) + for (auto const *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_paths.empty()) + if (auto const &point{track->m_paths.front().points[segment_data::point::start]}; glm::distance(geometry::plan_of(point), camera) < nearest) + { + nearest = glm::distance(geometry::plan_of(point), camera); + fallback = point.y; + } + } + auto const height = [&](double const X, double const Z, double &Height) { + if (m_streamer.height_at(X, Z, Height) || nmt_ground(X, Z, Height)) + return true; + Height = fallback; + return true; + }; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + auto const time{ImGui::GetTime()}; + auto const pulse{static_cast(0.5 + 0.5 * std::sin(time * 4.0))}; + auto const dots{std::string(static_cast(time * 3.0) % 4, '.')}; + int constexpr steps{8}; // points along each edge, the outline following the ground + auto const square = [&](glm::dvec2 const &Low, glm::dvec2 const &High, int const Red, int const Green, int const Blue, std::string const &Label) { + auto const size{High - Low}; + std::vector outline; + for (int side = 0; side < 4; ++side) + for (int i = 0; i < steps; ++i) + { + auto const along{static_cast(i) / steps}; + glm::dvec2 const point{side == 0 ? Low + glm::dvec2{size.x * along, 0.0} : side == 1 ? Low + glm::dvec2{size.x, size.y * along} : side == 2 ? High - glm::dvec2{size.x * along, 0.0} : High - glm::dvec2{size.x, size.y * along}}; + double y; + ImVec2 screen; + if (false == height(point.x, point.y, y) || false == projection.project({point.x, y + 0.5, point.y}, screen)) + return; + outline.push_back(screen); + } + auto const edge{IM_COL32(Red, Green, Blue, static_cast(140.0f + 100.0f * pulse))}; + drawlist->AddConvexPolyFilled(outline.data(), static_cast(outline.size()), IM_COL32(Red, Green, Blue, static_cast(10.0f + 15.0f * pulse))); + drawlist->AddPolyline(outline.data(), static_cast(outline.size()), edge, ImDrawFlags_Closed, 2.0f); + // named where the name fits in + auto const middle{(Low + High) * 0.5}; + auto const text{Label + dots}; + auto const extent{ImGui::CalcTextSize(text.c_str())}; + auto const [left, right]{std::minmax_element(outline.begin(), outline.end(), [](ImVec2 const &A, ImVec2 const &B) { return A.x < B.x; })}; + double y; + ImVec2 label; + if (right->x - left->x > extent.x * 1.1f && height(middle.x, middle.y, y) && projection.project({middle.x, y + 0.5, middle.y}, label)) + drawlist->AddText({label.x - extent.x * 0.5f, label.y - extent.y * 0.5f}, edge, text.c_str()); + }; + // the nearest first, as many as can be drawn without the frame feeling it + auto const nearest = [&](std::vector> Areas, std::size_t const Most) { + auto const away = [&](std::pair const &Area) { return glm::distance((Area.first + Area.second) * 0.5, camera); }; + if (Areas.size() > Most) { - field_row("Band along the tracks (m)"); - ImGui::InputDouble("##nmtbandwidth", &state.band, 50.0, 250.0, "%.0f"); - item_tooltip("How far from the tracks, on both sides, the terrain model is loaded"); + std::nth_element(Areas.begin(), Areas.begin() + Most, Areas.end(), [&](auto const &A, auto const &B) { return away(A) < away(B); }); + Areas.resize(Most); + } + return Areas; + }; + for (auto const &[low, high] : nearest(imagery, 64)) + square(low, high, 255, 190, 70, STR("Orthophoto loading")); + std::vector> chunks; + for (auto const &key : state.pending) + chunks.emplace_back(heightmap::chunk_corner(key), heightmap::chunk_corner(key) + glm::dvec2{heightmap::chunk_size}); + for (auto const &[low, high] : nearest(chunks, 64)) + square(low, high, 90, 170, 255, STR("Terrain model loading")); +} + +// --------------------------------------------------------------------------------------------- +// the download area + +void editor_mode::area_start() +{ + auto &drawing{m_area_drawing}; + drawing = {}; + drawing.active = true; + // 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 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(); + WriteLog("Editor: drawing the download area"); +} + +void editor_mode::area_finish(bool const Keep) +{ + auto &drawing{m_area_drawing}; + if (Keep && drawing.points.size() >= 3) + { + std::vector polygon; + for (auto const &point : drawing.points) + polygon.push_back(m_orthophoto.to_puwg(point)); + m_orthophoto.area(std::move(polygon)); + WriteLog(format("Editor: the download area of %zu points, %d tiles of the orthophoto", drawing.points.size(), m_orthophoto.stats().area_tiles)); + } + drawing = {}; +} + +glm::dvec3 editor_mode::area_point(glm::dvec2 const &Point) const +{ + double height{Camera.Pos.y - 2.0}; + if (false == Global.EditorOrtho && false == m_streamer.height_at(Point.x, Point.y, height) && false == nmt_ground(Point.x, Point.y, height)) + height = 0.0; + return {Point.x, height, Point.y}; +} + +void editor_mode::area_click(bool const Shift) +{ + auto &drawing{m_area_drawing}; + screen_projection const projection; + // the point under the cursor, within 10 pixels + int nearest{-1}; + auto best{100.0f}; + for (std::size_t i = 0; i < drawing.points.size(); ++i) + if (auto const distance{projection.mouse_distance2(area_point(drawing.points[i]))}; distance < best) + { + best = distance; + nearest = static_cast(i); + } + if (nearest >= 0) + { + if (Shift) + drawing.points.erase(drawing.points.begin() + nearest); + else + drawing.dragged = nearest; + return; + } + if (Shift) + return; + auto const cursor{std::as_const(*this).cursor_ground()}; + drawing.points.emplace_back(cursor.x, cursor.z); +} + +void editor_mode::area_update() +{ + auto &drawing{m_area_drawing}; + if (false == drawing.active || drawing.dragged < 0) + return; + if (false == ImGui::IsMouseDown(0) || drawing.dragged >= static_cast(drawing.points.size())) + { + drawing.dragged = -1; + return; + } + auto const cursor{std::as_const(*this).cursor_ground()}; + drawing.points[drawing.dragged] = {cursor.x, cursor.z}; +} + +void editor_mode::draw_download_area() const +{ + auto const &drawing{m_area_drawing}; + std::vector points; + if (drawing.active) + points = drawing.points; + else + for (auto const &point : m_orthophoto.area()) + points.push_back(m_orthophoto.to_world(point)); + // 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 && orthophoto_settings_open()) + for (auto const &square : m_orthophoto.area_squares()) + { + if (square.state == 0) + continue; + std::array corners; + auto seen{true}; + glm::dvec2 const at[]{square.low, {square.high.x, square.low.y}, square.high, {square.low.x, square.high.y}}; + for (int i = 0; i < 4 && seen; ++i) + seen = projection.project(area_point(at[i]), corners[i]); + if (seen) + drawlist->AddQuadFilled(corners[0], corners[1], corners[2], corners[3], square.state == 1 ? IM_COL32(90, 220, 120, 45) : IM_COL32(240, 80, 70, 80)); + } + auto const edge{IM_COL32(80, 220, 255, 230)}; + auto const closing{IM_COL32(80, 220, 255, drawing.active ? 110 : 230)}; + // the edge back to the first point from three points on + auto const edges{points.size() >= 3 ? points.size() : points.size() - 1}; + for (std::size_t i = 0; i < edges; ++i) + { + auto const last{i + 1 == points.size()}; + projection.line(drawlist, area_point(points[i]), area_point(points[(i + 1) % points.size()]), last ? closing : edge, 2.0f); + } + if (false == drawing.active) + return; + // the points, the one under the cursor larger, and the line on to the cursor + ImVec2 screen; + for (auto const &point : points) + if (projection.project(area_point(point), screen)) + drawlist->AddCircleFilled(screen, projection.mouse_distance2(area_point(point)) < 100.0f ? 6.0f : 4.0f, edge); + if (false == points.empty() && drawing.dragged < 0 && false == ImGui::GetIO().WantCaptureMouse) + { + auto const cursor{std::as_const(*this).cursor_ground()}; + projection.line(drawlist, area_point(points.back()), area_point({cursor.x, cursor.z}), closing, 1.5f); + } +} + +bool editor_mode::in_download_area(glm::dvec2 const &Low, glm::dvec2 const &High) const +{ + auto const &area{m_orthophoto.area()}; + if (area.empty()) + return true; + // the square in PUWG: the easting grows to the west of the scenery, the northing with z + auto const &config{m_orthophoto.settings()}; + return geometry::square_touches(area, config.east - High.x, config.north + Low.y, High.x - Low.x); +} + +void editor_mode::render_download_area_ui(editor_orthophoto::statistics const &Stats) +{ + auto &drawing{m_area_drawing}; + ImGui::TextUnformatted(STR_C("Download area")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A polygon drawn on the map: what the settings ask for inside it, the orthophoto and the terrain model,\n" + "goes to the disk cache in the background, and nothing outside it is downloaded, also around the camera")); + if (drawing.active) + { + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%s", STR_C("Click the points of the area on the map. Enter closes it, Backspace takes the last point back, Esc gives up;\n" + "a point dragged moves, Shift+click removes it")); + ImGui::Text(STR_C("%zu points"), drawing.points.size()); + ImGui::SameLine(); + if (drawing.points.size() >= 3 && ImGui::Button(STR_C("Done"))) + area_finish(true); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + area_finish(false); + return; + } + auto const &area{m_orthophoto.area()}; + if (ImGui::Button(area.empty() ? STR_C("Draw on the map") : STR_C("Draw again"))) + area_start(); + if (area.empty()) + { + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("none: the tiles around the camera, wherever it is")); + return; + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Remove"))) + m_orthophoto.area({}); + ImGui::Text(STR_C("Area: %d of %d tiles in the cache, %d failed"), Stats.area_cached, Stats.area_tiles, Stats.area_failed); + ImGui::SameLine(); + if (ImGui::SmallButton(m_area_paused ? STR_C("Resume") : STR_C("Pause"))) + { + m_area_paused = false == m_area_paused; + m_orthophoto.area_paused(m_area_paused); + m_nmt.prefetch_paused(m_area_paused); + } + if (auto const terrain{m_nmt.prefetched()}; terrain.total > 0) + { + ImGui::Text(STR_C("Terrain model: %d of %d squares in the cache, %d failed"), terrain.cached, terrain.total, terrain.failed); + // the ones there are only looked up again + if (terrain.failed > 0) + { + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{STR_C("Retry")} + "##terrainarea").c_str())) + m_nmt.prefetch(m_nmt_prefetched); + } + } +} + +void editor_mode::render_nmt_banner() +{ + auto const &state{m_nmt_state}; + if (false == nmt_busy() || state.shown >= ImGui::GetFrameCount() - 1) + return; + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + ImGui::SetNextWindowPos(ImVec2((low.x + high.x) * 0.5f, low.y + std::round(ImGui::GetFontSize() * 3.6f)), ImGuiCond_Always, ImVec2(0.5f, 0.0f)); + ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID); + ImGui::SetNextWindowBgAlpha(0.88f); + if (ImGui::Begin("##nmtbanner", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking)) + { + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(STR_C("Loading the real terrain")); + ImGui::SameLine(); + render_nmt_progress(true); + bool again{false}; + if (auto const error{m_nmt.error(&again)}; false == error.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR(error).c_str()); + if (again) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Try again"))) + m_nmt.retry(); + } + } + } + ImGui::End(); +} + +bool editor_mode::render_nmt_place(bool const Moving, bool const Button) +{ + auto &state{m_nmt_state}; + auto &map{m_nmt_map}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const height{std::round(std::min(width * 0.65f, ImGui::GetFontSize() * 16.0f))}; + ImGui::TextUnformatted(STR_C("Click the middle of the scenery on the map")); + item_tooltip("The point 0,0,0 of the scenery. The wheel zooms the map, a drag moves it"); + ImGui::BeginChild("##nmtmap", ImVec2(width, height), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + if (map.render({width, height})) + { + auto const degrees{editor_geo::to_degrees(map.point())}; + std::snprintf(state.place, sizeof(state.place), "%.6f, %.6f", degrees.x, degrees.y); + } + ImGui::EndChild(); + ImGui::SetNextItemWidth(-FLT_MIN); + glm::dvec2 typed; + auto const understood{editor_geo::parse_place(state.place, typed)}; + if (ImGui::InputTextWithHint("##nmtplace", STR_C("or type it: 52.2297, 21.0122"), state.place, sizeof(state.place)) && editor_geo::parse_place(state.place, typed)) + { + map.pick(typed); + map.show(typed, 8.0); + } + item_tooltip("Degrees as the online maps copy them (latitude, longitude), or the X and Y of PUWG 1992, the coordinates of the Polish maps of geoportal.gov.pl"); + if (state.place[0] != '\0' && false == understood) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("Not a place in Poland: two numbers, degrees or the X and Y of PUWG 1992")); + else if (map.picked()) + ImGui::TextDisabled(STR_C("X %.0f, Y %.0f in PUWG 1992"), map.point().y, map.point().x); + if (Moving && m_streamer.resident() > 0) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.95f, 0.75f, 0.3f, 1.0f)); + ImGui::TextWrapped("%s", STR_C("The terrain made already stays as it is")); + ImGui::PopStyleColor(); + } + if (false == Button) + return false; + auto const cancel{STR_C("Cancel")}; + auto const cancel_width{ImGui::CalcTextSize(cancel).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + bool placed{false}; + ImGui::BeginDisabled(false == map.picked()); + if (main_button(STR_C("The scenery is here"), ImVec2(Moving ? -(cancel_width + ImGui::GetStyle().ItemSpacing.x) : -FLT_MIN, tall_button_height()))) + { + nmt_place_scenery(map.point()); + placed = true; + } + ImGui::EndDisabled(); + if (Moving) + { + ImGui::SameLine(); + if (ImGui::Button(cancel, ImVec2(0.0f, tall_button_height()))) + state.moving = false; + } + return placed; +} + +void editor_mode::render_nmt_ui() +{ + auto &state{m_nmt_state}; + state.shown = ImGui::GetFrameCount(); + nmt_locate(); + auto const busy{nmt_busy()}; + step_heading(1, STR_C("Location")); + if (false == m_nmt.placed() || state.moving) + { + if (render_nmt_place(state.moving, true)) + state.moving = false; + if (false == m_nmt.placed()) + return; + } + else + { + auto const &config{m_orthophoto.settings()}; + auto const degrees{editor_geo::to_degrees({config.east, config.north})}; + ImGui::AlignTextToFramePadding(); + ImGui::Text(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("X %.0f, Y %.0f in PUWG 1992, the coordinates of the Polish maps; the place is shared with the orthophoto"), config.north, config.east); + ImGui::SameLine(); + ImGui::BeginDisabled(busy); + if (ImGui::SmallButton(STR_C("Change..."))) + { + glm::dvec2 const point{config.east, config.north}; + m_nmt_map.pick(point); + m_nmt_map.show(point, 8.0); + std::snprintf(state.place, sizeof(state.place), "%.6f, %.6f", degrees.x, degrees.y); + state.moving = true; + } + ImGui::EndDisabled(); + } + + auto const tracks{std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })}; + // the progress shown at the step whose button began it + auto const tracks_busy{state.stage != nmt_state::automatic::idle || (state.fitting && state.under)}; + step_heading(2, STR_C("Load")); + if (busy && false == tracks_busy) + render_nmt_progress(false); + else + { + ImGui::BeginDisabled(busy); + auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; + if (tracks) + { + if (main_button(STR_C("Load along the tracks"), ImVec2(half, tall_button_height()))) + nmt_make(nmt_keys_along_tracks()); + item_tooltip("Loads the terrain model as it is along the tracks, with the height offset above; the tracks stay where they are"); + ImGui::SameLine(); + } + if (main_button(STR_C("Load around the camera"), ImVec2(-FLT_MIN, tall_button_height()))) + { + if (false == tracks && m_streamer.resident() == 0) + state.offset = 0.0; + nmt_make(nmt_keys_around()); + } + item_tooltip("Loads the terrain model as it is around the camera, with the height offset above; the tracks stay where they are"); + if (begin_fields("##nmtload", 0.5f)) + { + if (tracks) + { + field_row("Band along the tracks (m)"); + ImGui::InputDouble("##nmtbandwidth", &state.band, 50.0, 250.0, "%.0f"); + item_tooltip("How far from the tracks, on both sides, the terrain model is loaded"); + } + else + { + field_row("Area"); + segmented("nmtarea", m_terrain_add_count, {{2, "0.5 km", "A square of 500 m around the camera"}, {4, "1 km", "A square of 1 km around the camera"}, {8, "2 km", "A square of 2 km around the camera"}}); + } ImGui::EndTable(); } - 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::TreePop(); + ImGui::EndDisabled(); + } + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("A chunk there is already is never made anew")); + ImGui::PopTextWrapPos(); + 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(); } - ImGui::PopID(); state.band = std::clamp(state.band, 50.0, 3000.0); - if (tracks && ImGui::TreeNode(STR_C("By hand: the model as it is, the tracks stay"))) + if (tracks && ImGui::TreeNode(STR_C("Height offset of the model"))) { if (begin_fields("##nmtoffset", 0.5f)) { @@ -847,86 +1832,60 @@ void editor_mode::render_nmt_ui() item_tooltip("Compares the formation of the tracks with the terrain model and moves the model up or down to meet them, as a scenery built at height 0 needs"); ImGui::EndTable(); } - auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; - ImGui::BeginDisabled(busy); - if (ImGui::Button(STR_C("Load along the tracks"), ImVec2(half, 0.0f))) - nmt_make(nmt_keys_along_tracks()); - item_tooltip("Loads the terrain model as it is along the tracks, with the height offset above; the tracks stay where they are"); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Load around the camera"), ImVec2(-FLT_MIN, 0.0f))) - nmt_make(nmt_keys_around()); - item_tooltip("Loads the terrain model as it is around the camera, with the height offset above; the tracks stay where they are"); - ImGui::EndDisabled(); ImGui::TreePop(); } -} -bool editor_mode::nmt_choice_card(int const Index, char const *Title, char const *Text, float const Width, float const TextHeight, bool const Chosen) const -{ - auto const line{ImGui::GetTextLineHeight()}; - auto const picture{std::round(line * 3.2f)}; - auto const pad{8.0f}; - ImVec2 const size{Width, pad + picture + pad * 0.5f + line + 4.0f + TextHeight + pad}; - auto const at{ImGui::GetCursorScreenPos()}; - ImGui::PushID(Index); - auto const clicked{ImGui::InvisibleButton("##nmtcard", size)}; - ImGui::PopID(); - auto const hovered{ImGui::IsItemHovered()}; - auto *draw{ImGui::GetWindowDrawList()}; - ImVec2 const end{at.x + size.x, at.y + size.y}; - draw->AddRectFilled(at, end, ImGui::GetColorU32(Chosen ? ImGuiCol_Header : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), 6.0f); - draw->AddRect(at, end, Chosen ? ImGui::GetColorU32(ImGuiCol_CheckMark) : ImGui::GetColorU32(ImGuiCol_Border), 6.0f, 0, Chosen ? 2.0f : 1.0f); - // the cross-section: the ground and the track over it, the one giving way moving - ImVec2 const low{at.x + pad, at.y + pad}; - ImVec2 const high{end.x - pad, at.y + pad + picture}; - draw->AddRectFilled(low, high, IM_COL32(18, 22, 30, 255), 4.0f); - draw->PushClipRect(low, high, true); - auto const phase{0.5f - 0.5f * std::cos(static_cast(ImGui::GetTime()) * 1.6f)}; - auto const amount{Chosen || hovered ? phase : 1.0f}; - auto const steps{48}; - auto const ground = [&](float const U) { return 0.62f + 0.22f * std::sin(U * 6.2f + 0.6f) * std::sin(U * 2.3f + 1.1f); }; - auto const level{0.4f}; - std::vector terrain, rails; - for (int i = 0; i <= steps; ++i) + if (tracks) { - auto const u{static_cast(i) / steps}; - auto const x{low.x + (high.x - low.x) * u}; - auto const closeness{std::clamp(1.0f - std::abs(u - 0.5f) * 2.4f, 0.0f, 1.0f)}; - float soil{ground(u)}; - float track{level}; - if (Index == 0) - track = level + (ground(u) - 0.1f - level) * amount; + step_heading(3, STR_C("Tracks and terrain")); + if (tracks_busy) + render_nmt_progress(false); else - soil = ground(u) + (level + 0.1f - ground(u)) * amount * (0.35f + 0.65f * closeness); - terrain.emplace_back(x, low.y + (high.y - low.y) * soil); - rails.emplace_back(x, low.y + (high.y - low.y) * track); + render_nmt_all_tracks(true); } - for (int i = 0; i < steps; ++i) - draw->AddQuadFilled(terrain[i], terrain[i + 1], ImVec2(terrain[i + 1].x, high.y), ImVec2(terrain[i].x, high.y), IM_COL32(120, 85, 50, 255)); - draw->AddPolyline(terrain.data(), static_cast(terrain.size()), IM_COL32(170, 120, 70, 255), 0, 2.0f); - draw->AddPolyline(rails.data(), static_cast(rails.size()), IM_COL32(215, 220, 230, 255), 0, 3.0f); - for (int i = 2; i < steps; i += 4) - draw->AddLine(ImVec2(rails[i].x, rails[i].y - 3.0f), ImVec2(rails[i].x, rails[i].y + 3.0f), IM_COL32(150, 120, 90, 255), 2.0f); - draw->PopClipRect(); - auto *font{ImGui::GetFont()}; - auto const titley{high.y + pad * 0.5f}; - draw->AddText(font, ImGui::GetFontSize(), ImVec2(low.x, titley), ImGui::GetColorU32(Chosen ? ImGuiCol_Text : ImGuiCol_Text, Chosen ? 1.0f : 0.85f), Title); - if (Chosen) - draw->AddText(font, ImGui::GetFontSize(), ImVec2(low.x + 0.6f, titley), ImGui::GetColorU32(ImGuiCol_Text), Title); - draw->AddText(font, ImGui::GetFontSize(), ImVec2(low.x, titley + line + 4.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), Text, nullptr, Width - 2.0f * pad); - return clicked; + ImGui::Spacing(); + if (false == state.status.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", state.status.c_str()); + ImGui::PopTextWrapPos(); + } + if (false == state.error.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", state.error.c_str()); + ImGui::PopTextWrapPos(); + } + bool again{false}; + if (auto const error{m_nmt.error(&again)}; false == error.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR(error).c_str()); + ImGui::PopTextWrapPos(); + if (again && ImGui::Button(STR_C("Try again"))) + m_nmt.retry(); + if (again) + item_tooltip("The squares of the model the service didn't give are asked for again; it happens by itself every 15 s too"); + } + ImGui::TextDisabled("%s", STR_C("Terrain model: GUGiK, geoportal.gov.pl")); } -void editor_mode::nmt_place_scenery() +void editor_mode::nmt_place_scenery(glm::dvec2 const &Point) { auto &state{m_nmt_state}; auto config{m_orthophoto.settings()}; - config.north = state.north; - config.east = state.east; + config.north = Point.y; + config.east = Point.x; m_orthophoto.settings(config); m_orthophoto_origin_edit = {config.north, config.east}; save_orthophoto_settings(); nmt_locate(); + // the heights fitted to the tracks at the place before mean nothing here + state.offset = 0.0; + state.fitted = false; + state.under = false; + state.anchors.clear(); + state.anchor_cells.clear(); } void editor_mode::nmt_offer() @@ -938,16 +1897,14 @@ void editor_mode::nmt_offer() if (m_streamer.resident() > 0 || std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })) return; nmt_locate(); - state.north = m_orthophoto.settings().north; - state.east = m_orthophoto.settings().east; state.prompt = true; } void editor_mode::nmt_accept_offer() { auto &state{m_nmt_state}; - if (false == m_nmt.placed()) - nmt_place_scenery(); + if (false == m_nmt.placed() && m_nmt_map.picked()) + nmt_place_scenery(m_nmt_map.point()); // no tracks to fit to: the heights of the model as they are state.offset = 0.0; nmt_make(nmt_keys_around()); @@ -968,15 +1925,8 @@ void editor_mode::render_nmt_prompt() ImGui::TextWrapped("%s", STR_C("The scenery has no terrain yet. Load the terrain model and the aerial photos around here? The tracks laid then go along the real ground.")); bool const placed{m_nmt.placed()}; if (false == placed) - { - ImGui::TextWrapped("%s", STR_C("Where is it? PUWG 1992 of this point, X to the north and Y to the east, as geoportal.gov.pl gives them:")); - ImGui::SetNextItemWidth(150.0f); - ImGui::InputDouble("X##nmtnorth", &state.north, 0.0, 0.0, "%.0f"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(150.0f); - ImGui::InputDouble("Y##nmteast", &state.east, 0.0, 0.0, "%.0f"); - } - auto const known{placed || (state.north > 100000.0 && state.east > 100000.0)}; + render_nmt_place(false, false); + auto const known{placed || m_nmt_map.picked()}; ImGui::Spacing(); auto const gap{std::round(ImGui::GetFontSize() * 0.6f)}; auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; @@ -986,7 +1936,7 @@ void editor_mode::render_nmt_prompt() ImGui::CloseCurrentPopup(); } if (false == known) - item_tooltip("Type where the scenery lies first"); + item_tooltip("Pick where the scenery lies first"); ImGui::SameLine(0.0f, gap); if (ground_card("##flat", STR_C("Without terrain"), STR_C("The tracks lie on the flat plane at the height 0; the terrain can be loaded later in Terrain (F5)"), false, true, width)) { @@ -1003,96 +1953,12 @@ 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() { - // ends which meet in the plan, laid apart in height by hand, so the game doesn't join them, are one joint before the grade - // lines run over them; farther apart than a track over another can be, they lie on two levels - double constexpr kLevel{2.0}; // m - struct end - { - TTrack *track; - int path; - bool atend; - glm::dvec3 point; - glm::dvec2 outward; - }; - auto const linked = [](TTrack const &Track, int const Path, bool const Atend) { - if (Track.eType == tt_Switch && Track.SwitchExtension) - return (Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]) != nullptr; - return (Atend ? Track.trNext : Track.trPrev) != nullptr; - }; - std::map, std::vector> cells; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || false == rail(*track) || track->Length() < 0.01) - continue; - for (int i = 0; i < static_cast(track->m_paths.size()); ++i) - for (auto const atend : {false, true}) - { - if (linked(*track, i, atend)) - continue; - auto const &path{track->m_paths[i]}; - auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]}; - auto const &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 outward{geometry::plan_of(control != glm::dvec3{} ? -control : point - other)}; - cells[{static_cast(std::floor(point.x)), static_cast(std::floor(point.z))}].push_back({track, i, atend, point, outward}); - } - } + // the loose ends are one joint before the grade lines run over them; of two switches, which can't be changed alone, the + // lines over them take each other over as they're fitted std::vector> states; std::vector changed; - std::set> taken; auto const remember = [&](TTrack *Track) { if (std::find(changed.begin(), changed.end(), Track) != changed.end()) return; @@ -1100,76 +1966,191 @@ std::size_t editor_mode::nmt_join_ends() changed.push_back(Track); }; std::size_t joined{0}; - for (auto const &[cell, ends] : cells) - for (auto const &a : ends) + m_nmt_state.loose.clear(); + for (auto pair : loose_pairs()) + { + if (pair.first.track->eType != tt_Normal) + std::swap(pair.first, pair.second); + auto const &a{pair.first}; + auto const &b{pair.second}; + if (a.track->eType != tt_Normal) { - if (a.track->eType != tt_Normal || taken.contains({a.track, a.path, a.atend})) - continue; - end const *match{nullptr}; - for (int dx = -1; dx <= 1 && match == nullptr; ++dx) - for (int dz = -1; dz <= 1 && match == nullptr; ++dz) - { - auto const found{cells.find({cell.first + dx, cell.second + dz})}; - if (found == cells.end()) - continue; - for (auto const &b : found->second) - if (b.track != a.track && false == taken.contains({b.track, b.path, b.atend}) && geometry::plan_distance(a.point, b.point) <= geometry::same_point && - std::abs(a.point.y - b.point.y) <= kLevel && glm::dot(a.outward, b.outward) < -0.5 * glm::length(a.outward) * glm::length(b.outward)) - { - match = &b; - break; - } - } - if (match == nullptr || (match->track->eType != tt_Normal && false == editor_track::is_supported(*match->track))) - continue; - auto const height{match->track->eType == tt_Normal ? (a.point.y + match->point.y) * 0.5 : match->point.y}; - for (auto const *side : {&a, match}) - { - taken.insert({side->track, side->path, side->atend}); - if (side->track->eType != tt_Normal) - continue; - remember(side->track); - side->track->m_paths[side->path].points[side->atend ? segment_data::point::end : segment_data::point::start].y = height; - } - ++joined; + // two switches: the lines over them meet as they're fitted + m_nmt_state.loose[{a.track, a.path, a.atend}] = {b.track, b.path, b.atend}; + m_nmt_state.loose[{b.track, b.path, b.atend}] = {a.track, a.path, a.atend}; + continue; } + if (b.track->eType != tt_Normal && false == editor_track::is_supported(*b.track)) + continue; + auto const height{b.track->eType == tt_Normal ? (a.point.y + b.point.y) * 0.5 : b.point.y}; + for (auto const *side : {&a, &b}) + { + if (side->track->eType != tt_Normal) + continue; + remember(side->track); + side->track->m_paths[side->path].points[side->atend ? segment_data::point::end : segment_data::point::start].y = height; + } + ++joined; + } if (changed.empty()) 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; } -void editor_mode::nmt_auto_start() +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}; + state.error.clear(); state.under = false; + 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) - state.probes.push_back(place); + (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..."); } -bool editor_mode::nmt_fit_line(TTrack &Track) +bool editor_mode::nmt_fit_line(TTrack &Track, std::function const &Fitted, bool const Laid) { auto &profile{m_profile}; - auto const &done{m_nmt_state.done}; - auto const fitted = [&](TTrack const *Other) { return std::find(done.begin(), done.end(), Other) != done.end(); }; + auto const &fitted{Fitted}; // the line runs over the tracks not fitted yet around the track: the ones fitted stay as they are auto route{editor_track::run_route(Track, profile.run_length)}; auto const at{std::find_if(route.spans.begin(), route.spans.end(), [&](auto const &Span) { return Span.track == &Track; })}; @@ -1207,22 +2188,52 @@ bool editor_mode::nmt_fit_line(TTrack &Track) }; profile.context.start_joined = profile.context.start_joined && joint(false, profile.context.start_elevation); profile.context.end_joined = profile.context.end_joined && joint(true, profile.context.end_elevation); + auto const on_route = [&](TTrack const *Other) { return std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == Other; }); }; + // an end on a switch which meets a switch fitted already unlinked, laid apart in height, takes it over as if it went on into it + auto const partner_of = [&](editor_track::route_span const &Span, bool const Atend) -> std::optional { + auto const found{m_nmt_state.loose.find({Span.track, Span.path, Atend})}; + if (found == m_nmt_state.loose.end() || on_route(std::get<0>(found->second))) + return std::nullopt; + auto const end{end_of(*std::get<0>(found->second), std::get<1>(found->second), std::get<2>(found->second))}; + if (glm::length(geometry::plan_of(end.outward)) < 1e-6) + return std::nullopt; + return end; + }; + auto const loose_into = [&](editor_track::route_span const &Span, bool const Atend) -> std::optional { + auto const end{partner_of(Span, Atend)}; + return end.has_value() && fitted(end->track) ? end : std::nullopt; + }; + // rising off the end met, into the route at its start and against it at its end + auto const rising = [](loose_end const &End) { return End.outward.y / glm::length(geometry::plan_of(End.outward)); }; + if (false == profile.context.start_joined) + if (auto const end{loose_into(profile.route.spans.front(), false == profile.route.spans.front().forward)}) + { + profile.context.start_joined = true; + profile.context.start_elevation = end->point.y; + profile.context.start_grade = rising(*end); + } + if (false == profile.context.end_joined) + if (auto const end{loose_into(profile.route.spans.back(), profile.route.spans.back().forward)}) + { + profile.context.end_joined = true; + profile.context.end_elevation = end->point.y; + profile.context.end_grade = -rising(*end); + } // the ground of the line: the terrain model on its axis, the top of the ballast with the rails on it, and the rail tops of // the lines fitted already beside it on the same level as the model here, which tell its height the more closely the // nearer they are. What doesn't agree with the line, a bridge over it, the ground under a viaduct, the fit lets go of by // itself - double constexpr kModel{0.25}; // m, the tolerance of the model + double constexpr kModel{0.15}; // m, the tolerance of the model double constexpr kRail{0.02}; // m, of a rail top next to the line, growing by kSpread a metre of the distance double constexpr kSpread{0.01}; double constexpr kLevel{0.5}; // m, a rail top farther from the model or from the line lies on another level + double constexpr kSameLevel{1.0}; // m, tracks authored closer in height lie on one level, farther one over the other double constexpr kReach{50.0}; - double constexpr kSteepest{40.0}; // per mille, the steepest grade the ground may give a line - double constexpr kCell{50.0}; auto const nearest = [&](glm::dvec3 const &Point, double &Distance) { Distance = std::numeric_limits::max(); double height{0.0}; - auto const cx{static_cast(std::floor(Point.x / kCell))}; - auto const cz{static_cast(std::floor(Point.z / kCell))}; + auto const cx{static_cast(std::floor(Point.x / kPlanCell))}; + auto const cz{static_cast(std::floor(Point.z / kPlanCell))}; for (int dx = -1; dx <= 1; ++dx) for (int dz = -1; dz <= 1; ++dz) { @@ -1230,22 +2241,49 @@ bool editor_mode::nmt_fit_line(TTrack &Track) if (cell == m_nmt_state.laid.end()) continue; for (auto const &laid : cell->second) - if (auto const distance{glm::length(glm::dvec2{laid.x - Point.x, laid.z - Point.z})}; distance < Distance) + if (auto const distance{glm::length(glm::dvec2{laid.position.x - Point.x, laid.position.z - Point.z})}; distance < Distance) { Distance = distance; - height = laid.y; + height = laid.position.y; } } return height; }; // an end of the line, or the other path of a switch of it, which leads through the tracks not fitted yet into a line fitted // already, as at a crossover, ties the two where it leaves the line: there the line keeps to the height of the other, on - // along its grade over the tracks between, give or take what these can climb with curves of the least radius at both ends + // along its grade over the tracks between, give or take what the plain tracks among these can climb with curves of the least + // radius. A switch lies in its plane and climbs nothing. Where they can climb more than a little, the curves of the norms of + // the line come first: what it can't keep to, the tracks between take over double constexpr kJoin{0.002}; // m + double constexpr kClimbing{0.05}; // m double constexpr kFarthest{200.0}; profile::ground_fit options; + // an end on the rim of a turntable runs up to it level at its height, as far as kRunUp off it or half the line, which turns to + // the ground only past that, not at the edge of the pit; the model in the pit isn't its ground + auto const first_chainage{profile.samples.front().chainage}; + auto const last_chainage{profile.samples.back().chainage}; + for (auto const atend : {false, true}) + { + auto const &span{atend ? profile.route.spans.back() : profile.route.spans.front()}; + auto const end{end_of(*span.track, span.path, atend ? span.forward : false == span.forward)}; + auto const table{std::find_if(m_nmt_state.turntables.begin(), m_nmt_state.turntables.end(), [&](nmt_state::turntable const &Table) { return on_rim(end, Table.centre, Table.rim); })}; + if (table == m_nmt_state.turntables.end()) + continue; + auto const height{table->centre.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(); }; - auto const on_route = [&](TTrack const *Other) { return std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == Other; }); }; std::vector exits; exits.push_back({profile.route.spans.front().track, profile.route.spans.front().path, false == profile.route.spans.front().forward}); exits.push_back({profile.route.spans.back().track, profile.route.spans.back().path, profile.route.spans.back().forward}); @@ -1253,45 +2291,113 @@ bool editor_mode::nmt_fit_line(TTrack &Track) if (span.track->eType == tt_Switch && span.track->m_paths.size() >= 2) for (auto const outward : {true, false}) exits.push_back({span.track, 1 - span.path, outward}); + // a switch entered at the far end of a path has the far end of its other path on its plane as high, when as far along it + auto const frog_beside = [](TTrack const &Switch, int const Path) { + if (Switch.m_paths.size() < 2) + return false; + auto const &entered{Switch.m_paths[Path].points}; + auto const &other{Switch.m_paths[1 - Path].points}; + auto const &origin{entered[segment_data::point::start]}; + auto const &control{entered[segment_data::point::control1]}; + auto const along{geometry::plan_of(control != glm::dvec3{} ? control : entered[segment_data::point::end] - origin)}; + if (glm::length(along) < 1e-6) + return false; + auto const ahead = [&](glm::dvec3 const &Point) { return glm::dot(geometry::plan_of(Point - origin), glm::normalize(along)); }; + return std::abs(ahead(entered[segment_data::point::end]) - ahead(other[segment_data::point::end])) <= 0.5; + }; for (auto const &exiting : exits) { auto const &exit{exiting.track->m_paths[exiting.path].points[exiting.forward ? segment_data::point::end : segment_data::point::start]}; - editor_track::route step; - step.spans.push_back(exiting); - double between{0.0}; - for (int hops = 0; hops < 50 && between < kFarthest; ++hops) + auto const bind = [&](double const Height, double const Between, double const Bending) { + std::size_t nearest{0}; + for (std::size_t i = 1; i < profile.samples.size(); ++i) + if (geometry::plan_distance(profile.samples[i].position, exit) < geometry::plan_distance(profile.samples[nearest].position, exit)) + nearest = i; + auto const climbing{Bending * Between / (4.0 * profile.line.norms.radius_min)}; + options.bounds.push_back({profile.samples[nearest].chainage, Height - kJoin - climbing, Height + kJoin + climbing, climbing >= kClimbing}); + }; + // the tracks it leads into, the nearest first, through both paths of a switch met at its start and across the frog of one + // met at the end of a path, onto the end of its other path + struct walk { - double grade; - TTrack *next{nullptr}; - int path{0}; - bool forward{true}; - if (false == editor_track::adjoining_grade(step, true, grade, &next, &path, &forward) || next == nullptr || on_route(next)) - break; - if (fitted(next)) + editor_track::route_span span; + double between{0.0}; // m, over the tracks not fitted yet + double bending{0.0}; // m, of these the plain tracks + }; + std::vector open{{exiting, 0.0, 0.0}}; + std::set> seen; + while (false == open.empty()) + { + auto const nearest{std::min_element(open.begin(), open.end(), [](walk const &A, walk const &B) { return A.between < B.between; })}; + auto const step{*nearest}; + open.erase(nearest); + if (step.between >= kFarthest || false == seen.insert({step.span.track, step.span.path, step.span.forward}).second) + continue; + auto onward{editor_track::continuations(step.span)}; + onward.erase(std::remove_if(onward.begin(), onward.end(), [&](editor_track::continuation const &Next) { return on_route(Next.span.track); }), onward.end()); + // met unlinked, laid apart in height: on as if linked + if (onward.empty()) + if (auto const end{partner_of(step.span, step.span.forward)}) + onward.push_back({{end->track, end->path, false == end->atend}, -rising(*end)}); + for (auto const &[next, grade] : onward) { - std::size_t nearest{0}; - for (std::size_t i = 1; i < profile.samples.size(); ++i) - if (geometry::plan_distance(profile.samples[i].position, exit) < geometry::plan_distance(profile.samples[nearest].position, exit)) - nearest = i; - auto const &points{next->m_paths[path].points}; - auto const height{points[forward ? segment_data::point::start : segment_data::point::end].y - grade * between}; - auto const leeway{kJoin + between * between / (4.0 * profile.line.norms.radius_min)}; - options.bounds.push_back({profile.samples[nearest].chainage, height - leeway, height + leeway}); - break; + auto const &points{next.track->m_paths[next.path].points}; + auto const entry{points[next.forward ? segment_data::point::start : segment_data::point::end].y}; + if (fitted(next.track)) + { + bind(entry - grade * step.between, step.between, step.bending); + continue; + } + auto const length{length_of(*next.track, next.path)}; + open.push_back({next, step.between + length, step.bending + (next.track->eType == tt_Switch ? 0.0 : length)}); + if (false == next.forward && next.track->eType == tt_Switch && frog_beside(*next.track, next.path)) + open.push_back({{next.track, 1 - next.path, true}, step.between, step.bending}); } - step.spans = {{next, path, forward}}; - between += length_of(*next, path); } } + // the lines fitted already close beside it on its level, as authored, are ground it never lets go of, however far they are + // off: side by side the tracks lie on one formation, in one plane, as near as the curves of the norms let them. The nearest + // on each side. Where they run so near that the timbers carry both, at a switch, a crossover, they are at one height, on + // any vertical curve the timbers lie on + double constexpr kNear{8.0}; // m between the axes + double constexpr kHeld{5.0}; // m, the reach of their rail tops + double constexpr kTimbers{2.65}; // m between the axes, about the length of a sleeper + double constexpr kTimbered{0.005}; // m + auto const alongside = [&](editor_track::route_sample const &Sample) { + std::array, 2> sides{std::pair{kNear, nullptr}, std::pair{kNear, nullptr}}; + auto const along{glm::normalize(Sample.direction)}; + auto const cx{static_cast(std::floor(Sample.position.x / kPlanCell))}; + auto const cz{static_cast(std::floor(Sample.position.z / kPlanCell))}; + for (int dx = -1; dx <= 1; ++dx) + for (int dz = -1; dz <= 1; ++dz) + { + auto const cell{m_nmt_state.laid.find({cx + dx, cz + dz})}; + if (cell == m_nmt_state.laid.end()) + continue; + for (auto const &point : cell->second) + { + glm::dvec2 const offset{point.position.x - Sample.position.x, point.position.z - Sample.position.z}; + auto const across{along.x * offset.y - along.y * offset.x}; + if (std::abs(glm::dot(offset, along)) > 1.0 || std::abs(across) >= kNear || std::abs(point.was - Sample.position.y) > kSameLevel) + continue; + auto &side{sides[across > 0.0 ? 0 : 1]}; + if (std::abs(across) < side.first) + side = {std::abs(across), &point}; + } + } + return sides; + }; auto const count{profile.samples.size()}; std::vector sampled(count), model(count, std::numeric_limits::quiet_NaN()); + std::vector plan(count); std::vector chainages, ground, tolerances, reaches; for (std::size_t i = 0; i < count; ++i) { auto const &sample{profile.samples[i]}; auto const *track{sample.span < profile.route.spans.size() ? profile.route.spans[sample.span].track : nullptr}; sampled[i] = sample.chainage; - if (double height; track != nullptr && m_nmt.height(sample.position.x, sample.position.z, height)) + plan[i] = {sample.position.x, sample.position.z}; + if (double height; track != nullptr && shapes_ground(*track) && false == in_pit(sample.position) && m_nmt.height(sample.position.x, sample.position.z, height)) model[i] = height + m_nmt_state.offset + track->rail_height(); chainages.push_back(sample.chainage); ground.push_back(model[i]); @@ -1306,11 +2412,108 @@ bool editor_mode::nmt_fit_line(TTrack &Track) tolerances.push_back(std::max(kRail, kSpread * distance)); reaches.push_back(kLevel); } + if (track != nullptr) + for (auto const &[apart, point] : alongside(sample)) + if (point != nullptr) + { + chainages.push_back(sample.chainage); + ground.push_back(point->position.y + point->rails - track->rail_height()); + tolerances.push_back(apart < kTimbers ? kTimbered : std::max(kRail, kSpread * apart)); + reaches.push_back(kHeld); + } } if (std::none_of(ground.begin(), ground.end(), [](double const Height) { return std::isfinite(Height); })) return false; + // a path which the line crosses on its level, as authored, makes a crossing with it: over it the two lie in one plane, the + // grade doesn't break nor does a vertical curve reach; the line crosses a line fitted already at its height, the rail tops + // of the two at one + double constexpr kCrossing{0.75}; // m, the nearest the samples of the two come, a metre apart + double constexpr kCrossed{0.035}; // the sine of the least angle the axes cross at + double constexpr kGauge{1.435}; // m + struct crossing + { + double chainage; + double height; + double sine; + double distance; + TTrack const *track; + glm::dvec2 at; // where the axes meet + glm::dvec2 along; // the way of the line there + }; + auto const crossings_of = [&](std::map, std::vector> const &Points) { + std::vector found; + auto const across = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; + for (auto const &sample : profile.samples) + { + auto const *track{profile.route.spans[sample.span].track}; + auto const cx{static_cast(std::floor(sample.position.x / kPlanCell))}; + auto const cz{static_cast(std::floor(sample.position.z / kPlanCell))}; + auto const along{glm::normalize(sample.direction)}; + for (int dx = -1; dx <= 1; ++dx) + for (int dz = -1; dz <= 1; ++dz) + { + auto const cell{Points.find({cx + dx, cz + dz})}; + if (cell == Points.end()) + continue; + for (auto const &point : cell->second) + { + glm::dvec2 const offset{point.position.x - sample.position.x, point.position.z - sample.position.z}; + auto const other{glm::normalize(point.direction)}; + auto const sine{across(along, other)}; + if (point.track == track || glm::length(offset) > kCrossing || std::abs(point.was - sample.position.y) > kSameLevel || std::abs(sine) < kCrossed) + continue; + // where the axes meet + auto const here{across(offset, other) / sine}; + auto const there{across(offset, along) / sine}; + found.push_back({sample.chainage + here, point.position.y + there * point.grade + point.rails - track->rail_height(), std::abs(sine), glm::length(offset), point.track, glm::dvec2{sample.position.x, sample.position.z} + along * here, along}); + } + } + } + // the nearest samples of each crossing tell it + std::sort(found.begin(), found.end(), [](crossing const &A, crossing const &B) { return std::tie(A.track, A.chainage) < std::tie(B.track, B.chainage); }); + std::vector result; + for (auto const &one : found) + if (one.chainage < 0.0 || one.chainage > profile.route.length || on_route(one.track)) + continue; + else if (result.empty() || result.back().track != one.track || one.chainage - result.back().chainage > 5.0) + result.push_back(one); + else if (one.distance < result.back().distance) + result.back() = one; + return result; + }; + // no farther than the path crossed has rails: the middle of a double slip, a few metres crossed at a low angle, keeps to + // itself, the switches about it have zones of their own + for (auto const &found : crossings_of(m_nmt_state.paths)) + { + auto const half{std::min(30.0, kGauge / found.sine + 1.5)}; + auto first{std::numeric_limits::max()}; + auto last{std::numeric_limits::lowest()}; + for (auto const &path : found.track->m_paths) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + auto const along{glm::dot(glm::dvec2{path.points[end].x, path.points[end].z} - found.at, found.along)}; + first = std::min(first, along); + last = std::max(last, along); + } + auto const from{std::max(-half, first - 1.5)}; + auto const to{std::min(half, last + 1.5)}; + if (from < to) + profile.context.switches.push_back({found.chainage + from, found.chainage + to, found.track->name()}); + } + // the height of a crossing of a line fitted already is ground the line keeps to as closely as to the switches beside, though + // not past the curves of the norms: where an end fitted another way is too near, as near as they let it + double constexpr kCrossingHeight{0.005}; // m + for (auto const &found : crossings_of(m_nmt_state.laid)) + { + auto const at{static_cast(std::distance(chainages.begin(), std::upper_bound(chainages.begin(), chainages.end(), found.chainage)))}; + chainages.insert(chainages.begin() + at, found.chainage); + ground.insert(ground.begin() + at, found.height); + tolerances.insert(tolerances.begin() + at, kCrossingHeight); + reaches.insert(reaches.begin() + at, kLevel); + } // the switches of the tracks beside, the ones of the crossovers above all, lie on bearers common with this track or close // to it: no break of the grade on them either, so the tracks of the station stay in one plane + double constexpr kShared{7.0}; // m, about the spacing of the tracks for (auto *other : simulation::Paths.sequence()) { if (other == nullptr || other->eType != tt_Switch || other->m_paths.empty() || false == rail(*other)) @@ -1323,7 +2526,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) for (auto const end : {segment_data::point::start, segment_data::point::end}) { auto const &point{points[end]}; - double best{25.0}; + double best{kShared}; double chainage{0.0}; for (auto const &sample : profile.samples) if (auto const distance{glm::length(glm::dvec2{sample.position.x - point.x, sample.position.z - point.z})}; distance < best) @@ -1331,7 +2534,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) best = distance; chainage = sample.chainage; } - if (best < 25.0) + if (best < kShared) { from = std::min(from, chainage); to = std::max(to, chainage); @@ -1344,13 +2547,16 @@ bool editor_mode::nmt_fit_line(TTrack &Track) auto settings{profile.line}; settings.join_start = true; settings.join_end = true; - settings.speed = 0.0; - for (auto const &span : profile.route.spans) - settings.speed = std::max(settings.speed, editor_track::velocity(*span.track)); - if (settings.speed <= 0.0) - settings.speed = 100.0; + settings.speed = editor_track::route_speed(profile.route); + // the grades as steep as the ground has them; the vertical curves of the norms, sharper only on a hump of a marshalling yard options.tolerance = kModel; - options.grade_max = kSteepest; + options.grade_max = std::numeric_limits::max(); + options.follow_length = 60.0; + options.follow_height = 4.0; + options.follow_steepest = 100.0; + options.follow_tolerance = 0.05; + options.humps = profile::humps(chainages, ground, profile.context, kModel, tolerances); + options.hump_radius = 300.0; auto line{profile::fit_ground(chainages, ground, profile.context, settings, options, tolerances, reaches)}; if (line.points.size() < 2) return false; @@ -1358,7 +2564,29 @@ bool editor_mode::nmt_fit_line(TTrack &Track) profile.selected = -1; profile_check(); auto const fitted_line{profile.line}; - profile_apply(); + auto const authored{profile.samples}; + // where the model falls away under the line by the abutments, the line goes over on a bridge or a viaduct: its paths there + // are cut off and set to the bridge, the formation is laid up to it, the ground under it stays as the model has it + double constexpr kBridged{1.0}; // m + double constexpr kAbutment{100.0}; // per mille + // spans a few metres apart are one bridge on its piers; what is shorter than that is a culvert under the embankment + double constexpr kPier{5.0}; // m + double constexpr kCulvert{3.0}; // m + double constexpr kFewest{0.03}; // m, how far a path just laid may part from the line before it's cut + std::vector heights(count); + for (std::size_t i = 0; i < count; ++i) + heights[i] = profile::elevation(fitted_line, sampled[i]); + std::vector bridges; + for (auto const &stretch : Laid ? std::vector{} : profile::bridged(sampled, heights, model, kBridged, kAbutment)) + if (false == bridges.empty() && stretch.from - bridges.back().to < kPier) + { + bridges.back().to = stretch.to; + bridges.back().depth = std::min(bridges.back().depth, stretch.depth); + } + else + bridges.push_back(stretch); + bridges.erase(std::remove_if(bridges.begin(), bridges.end(), [&](profile::departure const &Bridge) { return Bridge.to - Bridge.from < kCulvert; }), bridges.end()); + profile_apply(false, bridges, Laid ? kFewest : 0.0); auto const applied{profile.error.empty()}; // where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track auto const parted{profile::departures(fitted_line, sampled, model, 2.0 * kModel)}; @@ -1409,13 +2637,30 @@ bool editor_mode::nmt_fit_line(TTrack &Track) } if (applied) for (auto const &sample : profile.samples) - m_nmt_state.laid[{static_cast(std::floor(sample.position.x / kCell)), static_cast(std::floor(sample.position.z / kCell))}].push_back(sample.position); + { + auto const *track{sample.span < profile.route.spans.size() ? profile.route.spans[sample.span].track : nullptr}; + m_nmt_state.laid[{static_cast(std::floor(sample.position.x / kPlanCell)), static_cast(std::floor(sample.position.z / kPlanCell))}].push_back( + {sample.position, sample.direction, sample.grade, editor_track::sampled_elevation(authored, sample.chainage), track != nullptr ? static_cast(track->rail_height()) : 0.0, track}); + } + auto const spans{applied ? bridges : std::vector{}}; + for (auto const &stretch : spans) + for (std::size_t i = 0; i < count; ++i) + if (sampled[i] >= stretch.from && sampled[i] <= stretch.to) + m_nmt_state.bridged[{static_cast(std::floor(plan[i].x / kPlanCell)), static_cast(std::floor(plan[i].y / kPlanCell))}].push_back({plan[i].x, heights[i], plan[i].y}); // the line in the log: how much of it lies on the model, where it parts from it, its grades, why it wasn't applied, its // course in the plan std::string text{format("Editor: NMT line %s: %s .. %s, %.0f m, on the model within %.2f m over %.1f%%, parts from it", applied ? "applied" : "refused", from->name().c_str(), to->name().c_str(), profile.route.length, kModel, 100.0 * static_cast(agreeing) / static_cast(std::max(1, modelled)))}; for (auto const &stretch : parted) text += format(" %s %.0f..%.0f %.1f m", stretch.depth < 0.0 ? "under" : "over", stretch.from, stretch.to, std::abs(stretch.depth)); + if (false == spans.empty()) + { + text += ", bridged"; + for (auto const &stretch : spans) + text += format(" %.0f..%.0f", stretch.from, stretch.to); + } + for (auto const &hump : options.humps) + text += format(", a hump %.0f..%.0f, its crest at %.0f %.1f m high", hump.from, hump.to, hump.crest, hump.height); text += ", grades"; for (std::size_t i = 1; i < fitted_line.points.size(); ++i) { @@ -1438,6 +2683,203 @@ bool editor_mode::nmt_fit_line(TTrack &Track) return applied; } +void editor_mode::nmt_sample_paths(TTrack const &Track, std::map, 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}; + auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + auto const count{std::max(1, static_cast(std::ceil(chord * 1.1)))}; + for (int i = 0; i <= count; ++i) + { + auto const t{static_cast(i) / count}; + auto const at{curve.point(t)}; + auto const tangent{curve.first(t)}; + auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})}; + if (run > 1e-9) + Points[{static_cast(std::floor(at.x / kPlanCell)), static_cast(std::floor(at.z / kPlanCell))}].push_back( + {at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast(Track.rail_height()), &Track}); + } + } +} + +void editor_mode::nmt_merge_laid(std::unordered_set &Laid) +{ + auto const direction = [](segment_data const &Path) { + auto const control{geometry::plan_of(Path.points[segment_data::point::control1])}; + auto const chord{geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; + auto const along{glm::length(control) > 1e-6 ? control : chord}; + return along / std::max(1e-12, glm::length(along)); + }; + auto const straight = [&](segment_data const &Path) { + if (std::abs(Path.radius) > 1e-3f) + return false; + auto const along{direction(Path)}; + return std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start]))) < 1e-3 + && std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::control2]))) < 1e-3; + }; + struct circle + { + glm::dvec2 centre{0.0}; + double radius{0.0}; + int side{0}; + double angle{0.0}; + }; + auto const arc = [&](segment_data const &Path) -> std::optional { + auto const radius{std::abs(static_cast(Path.radius))}; + if (radius < 1e-3 || glm::length(geometry::plan_of(Path.points[segment_data::point::control1])) < 1e-6) + return std::nullopt; + auto const start{geometry::plan_of(Path.points[segment_data::point::start])}; + auto const end{geometry::plan_of(Path.points[segment_data::point::end])}; + auto const along{direction(Path)}; + auto const side{geometry::cross(along, end - start) > 0.0 ? 1 : -1}; + circle result{start + glm::dvec2{-along.y, along.x} * (side * radius), radius, side, 0.0}; + if (std::abs(glm::distance(result.centre, end) - radius) > 0.01) + return std::nullopt; + result.angle = std::abs(geometry::signed_angle(start - result.centre, end - result.centre)); + return result; + }; + 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; + }; + editor_track::edit edit; + for (bool merged{true}; merged;) + { + merged = false; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == Laid.contains(track) || track->eType != tt_Normal || track->m_paths.size() != 1) + continue; + auto *next{track->trNext}; + if (next == nullptr || next == track || next->m_editorremoved || false == Laid.contains(next) || next->eType != tt_Normal || next->m_paths.size() != 1 || next->trPrev != track || false == alike(*track, *next)) + continue; + auto const &first{track->m_paths.front()}; + auto const &second{next->m_paths.front()}; + if (glm::distance(first.points[segment_data::point::end], second.points[segment_data::point::start]) > 1e-3) + continue; + auto const length{geometry::bezier(first).plan_length() + geometry::bezier(second).plan_length()}; + if (length > geometry::longest_path) + continue; + auto path{first}; + path.pieces.clear(); + if (straight(first) && straight(second) && std::abs(geometry::cross(direction(first), direction(second))) < 1e-5 && glm::dot(direction(first), direction(second)) > 0.0) + { + path.points[segment_data::point::control1] = {}; + path.points[segment_data::point::control2] = {}; + path.radius = 0.f; + } + else if (auto const a{arc(first)}, b{arc(second)}; + a && b && a->side == b->side && std::abs(a->radius - b->radius) < 1e-3 * a->radius && glm::distance(a->centre, b->centre) < 0.01 && a->angle + b->angle <= glm::radians(90.0)) + { + auto const pieces{geometry::arc_pieces(first.points[segment_data::point::start], direction(first), 0.0, a->radius, a->angle + b->angle, a->side)}; + if (pieces.size() != 1) + continue; + path.points[segment_data::point::control1] = pieces.front().points[segment_data::point::control1]; + path.points[segment_data::point::control2] = pieces.front().points[segment_data::point::control2]; + } + else + continue; + path.points[segment_data::point::end] = second.points[segment_data::point::end]; + path.rolls[1] = second.rolls[1]; + edit.capture(*track); + editor_track::retire(*next); + Laid.erase(next); + edit.removed.push_back(next); + track->m_paths.front() = path; + editor_track::commit({track}); + merged = true; + } + } + 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) +{ + auto &state{m_nmt_state}; + if (EditorSettings.heights() != editorSettings::lay_heights::fitted || state.stage != nmt_state::automatic::idle || false == m_nmt.placed() || From >= m_history.size()) + return false; + std::unordered_set laid; + for (auto step{From}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved && rail(*track)) + laid.insert(track); + auto const at_end = [&]() -> TTrack * { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && laid.contains(track) && track->eType == tt_Normal && geometry::plan_distance(track->m_paths.front().points[segment_data::point::end], End) < 0.01) + return track; + return nullptr; + }; + auto const steps{m_history.size()}; + nmt_merge_laid(laid); + auto *last{at_end()}; + if (last == nullptr) + return false; + // the tracks around stay as they are, as lines fitted already: the line keeps level with the ones beside it, the more closely + // the nearer they are, and crosses the ones it crosses at their height + double constexpr kAround{60.0}; // m + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const *track : laid) + for (auto const &path : track->m_paths) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + low = glm::min(low, geometry::plan_of(path.points[end])); + high = glm::max(high, geometry::plan_of(path.points[end])); + } + low -= kAround; + high += kAround; + std::map, std::vector> around; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == rail(*track) || laid.contains(track)) + continue; + auto const &points{track->m_paths.front().points}; + auto const a{geometry::plan_of(points[segment_data::point::start])}; + auto const b{geometry::plan_of(points[segment_data::point::end])}; + if (std::max(a.x, b.x) < low.x || std::min(a.x, b.x) > high.x || std::max(a.y, b.y) < low.y || std::min(a.y, b.y) > high.y) + continue; + nmt_sample_paths(*track, around); + } + // the state of the tracks going onto the terrain lent to the one line, the profile tool left as it was + auto const profile{m_profile}; + 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 + for (auto step{steps}; step < m_history.size(); ++step) + m_history[step].joined = true; + if (applied) + { + laid.clear(); + for (auto step{From}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved) + laid.insert(track); + if (auto *track{at_end()}; track != nullptr) + { + m_node = track; + ui()->set_node(m_node); + } + } + else if (false == error.empty()) + WriteLog("Editor: the track laid not put onto the terrain model - " + error, logtype::generic); + return applied; +} + void editor_mode::nmt_auto_step() { auto &state{m_nmt_state}; @@ -1445,17 +2887,19 @@ void editor_mode::nmt_auto_step() { bool all{true}; for (auto const &place : state.probes) - all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; + all = nmt_ready(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; auto const waiting{m_nmt.waiting()}; 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; @@ -1464,46 +2908,74 @@ 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; - state.offset = 0.0; - if (std::abs(lift) > 0.05 && tracks_editable(scenery, reason)) + if (state.unfitted && false == kept.empty()) { - nmt_lift_scenery(lift); - state.lifted = lift; + // 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 if (std::abs(lift) > 0.05) + else { - 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)) + 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(); state.split = 0; state.done.clear(); + // the tracks fitted before are lines fitted already: the new ones run up to them and meet them as they are + if (state.unfitted) + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && rail(*track) && grade_fitted(*track)) + state.done.push_back(track); state.laid.clear(); + state.bridged.clear(); + // every path sampled as laid, for the crossings of the lines + state.paths.clear(); + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && rail(*track)) + nmt_sample_paths(*track, state.paths); state.stage = nmt_state::automatic::fitting; return; } - // a few lines a frame + // a few lines a frame: the longest track next to the lines fitted already, so that a line meets the ones it leads into as + // it's fitted, not when the lines between them come; the longest of all where none is next to them for (int line = 0; line < 2 && false == state.left.empty(); ++line) { + std::unordered_set const fitted(state.done.begin(), state.done.end()); + auto const touches = [&](TTrack *Track) { + for (int path = 0; path < static_cast(Track->m_paths.size()); ++path) + for (auto const forward : {true, false}) + { + auto const onward{editor_track::continuations({Track, path, forward})}; + if (std::any_of(onward.begin(), onward.end(), [&](editor_track::continuation const &Next) { return fitted.contains(Next.span.track); })) + return true; + if (auto const loose{state.loose.find({Track, path, forward})}; loose != state.loose.end() && fitted.contains(std::get<0>(loose->second))) + return true; + } + return false; + }; + auto const next{std::find_if(state.left.begin(), state.left.end(), touches)}; + if (next != state.left.end()) + std::rotate(state.left.begin(), next, next + 1); auto *track{state.left.front()}; auto const name{track->name()}; auto const known{simulation::Paths.sequence().size()}; - if (nmt_fit_line(*track)) + if (nmt_fit_line(*track, [&](TTrack const *Other) { return fitted.contains(Other); }, false)) ++state.lines; else ++state.refused; @@ -1537,14 +3009,19 @@ 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; state.done.clear(); 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/editormodeparallel.cpp b/application/editormodeparallel.cpp index 7fb5b893..4a35bdd2 100644 --- a/application/editormodeparallel.cpp +++ b/application/editormodeparallel.cpp @@ -8,8 +8,7 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" -#include "application/editoruilayer.h" +#include "application/editormodeparallel.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" @@ -123,11 +122,6 @@ bool side_picker(int &Side, double const Spacing) return changed; } -std::string name_of(TTrack const &Track) -{ - return Track.name().empty() ? STR("(no name)") : Track.name(); -} - segment_data reversed(segment_data const &Path) { segment_data result{Path}; @@ -209,21 +203,23 @@ double lateral_of(TTrack const &Src, TTrack const &Dst) } // namespace -TTrack *editor_mode::parallel_source() const +TTrack *parallel_tool::parallel_source() const { - if (m_track_set.size() >= 2) - return m_track_set.front(); - return selected_track(); + auto const &set{m_selection.track_set()}; + if (set.size() >= 2) + return set.front(); + return m_selection.selected_track(); } -TTrack *editor_mode::parallel_target() const +TTrack *parallel_tool::parallel_target() const { - if (m_track_set.size() != 2) + auto const &set{m_selection.track_set()}; + if (set.size() != 2) return nullptr; - return m_track_set.back() != m_track_set.front() ? m_track_set.back() : nullptr; + return set.back() != set.front() ? set.back() : nullptr; } -void editor_mode::parallel_update() +void parallel_tool::parallel_update() { auto &tool{m_parallel}; auto *track{parallel_source()}; @@ -244,13 +240,13 @@ void editor_mode::parallel_update() else if (track != nullptr && target != nullptr) tool.measured = std::abs(lateral_of(*track, *target)); - if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.size() == tool.built_history) + if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.history_revision() == tool.built_history) return; tool.built_for = track; tool.built_spacing = tool.spacing; tool.built_side = tool.side; tool.built_scope = tool.scope; - tool.built_history = m_history.size(); + tool.built_history = m_history.history_revision(); tool.pieces.clear(); tool.styles.clear(); tool.length = 0.0; @@ -319,7 +315,7 @@ void editor_mode::parallel_update() } } -void editor_mode::parallel_build() +void parallel_tool::parallel_build() { auto &tool{m_parallel}; parallel_update(); @@ -329,8 +325,8 @@ void editor_mode::parallel_build() for (std::size_t i = 0; i < tool.pieces.size(); ++i) created.push_back(editor_track::create_path(*tool.styles[i], tool.pieces[i])); editor_track::commit(created); - digitize_drawn(created); - push_track_snapshot({}, created); + m_design.digitize_drawn(created); + m_history.push_track_snapshot({}, created); tool.status = format(STR_C("Parallel track of %zu paths, %.1f m, %.2f m away; Ctrl+Z takes it back"), created.size(), tool.length, tool.spacing); if (scene::Layers.active() == null_handle) tool.status += "\n" + std::string{STR_C("The scenery isn't opened for editing, the new track won't be saved")}; @@ -338,7 +334,7 @@ void editor_mode::parallel_build() tool.built_for = nullptr; } -void editor_mode::parallel_apply() +void parallel_tool::parallel_apply() { auto &tool{m_parallel}; auto *target{parallel_target()}; @@ -375,14 +371,14 @@ void editor_mode::parallel_apply() if (joint.other != nullptr) committed.push_back(joint.other); editor_track::commit(committed); - push_track_snapshot(std::move(states)); + m_history.push_track_snapshot(std::move(states)); tool.status = format(STR_C("%s set parallel to %s, %.2f m away; Ctrl+Z takes it back"), name_of(*target).c_str(), name_of(*parallel_source()).c_str(), tool.spacing); WriteLog("Editor: " + tool.status, logtype::generic); tool.built_for = nullptr; - straight_refresh(); + m_design.straight_refresh(); } -void editor_mode::render_parallel_controls(bool const Scope) +void parallel_tool::render_parallel_controls(bool const Scope) { auto &tool{m_parallel}; if (Scope) @@ -396,7 +392,7 @@ void editor_mode::render_parallel_controls(bool const Scope) value_chips("spacings", tool.spacing, {kSpacings[0], kSpacings[1], kSpacings[2], kSpacings[3], kSpacings[4], kSpacings[5]}, "%.2f"); } -void editor_mode::render_parallel_ui() +void parallel_tool::render_options() { auto &tool{m_parallel}; ImGui::PushTextWrapPos(0.0f); @@ -415,18 +411,15 @@ void editor_mode::render_parallel_ui() } else if (false == tool.pieces.empty()) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(format(STR_P("Lay %zu track, %.1f m", "Lay %zu tracks, %.1f m", static_cast(tool.pieces.size())), tool.pieces.size(), tool.length).c_str(), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + if (main_button(format(STR_P("Lay %zu track, %.1f m", "Lay %zu tracks, %.1f m", static_cast(tool.pieces.size())), tool.pieces.size(), tool.length).c_str(), ImVec2(-1.0f, tall_button_height()))) parallel_build(); - ImGui::PopStyleColor(2); item_tooltip("A new track beside the one selected, as in the preview; the selected stays as it is. Ctrl+Z takes it back"); } if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); } -void editor_mode::render_parallel_set_ui() +void parallel_tool::render_set_options() { auto &tool{m_parallel}; auto *source{parallel_source()}; @@ -450,16 +443,16 @@ void editor_mode::render_parallel_set_ui() ImGui::TextWrapped("%s", tool.status.c_str()); } -bool editor_mode::parallel_preview_visible() const +bool parallel_tool::parallel_preview_visible() const { if (m_parallel.pieces.empty() || parallel_source() != m_parallel.built_for) return false; - if (m_track_tab == track_tab::parallel) + if (m_view.current_track_tab() == editor_track_tab::parallel) return true; - return m_track_tab == track_tab::path && parallel_target() != nullptr; + return m_view.current_track_tab() == editor_track_tab::path && parallel_target() != nullptr; } -void editor_mode::draw_parallel_preview() const +void parallel_tool::draw_overlay() const { if (false == parallel_preview_visible()) return; @@ -488,3 +481,17 @@ void editor_mode::draw_parallel_preview() const drawlist->AddText(ImVec2(screen.x + 12.0f, screen.y - 8.0f), IM_COL32(90, 255, 120, 255), label.c_str()); } } + +editor_selftest_result parallel_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "parallel") + { + Words >> m_parallel.spacing >> m_parallel.side; + m_parallel.scope = 1; + m_parallel.built_for = nullptr; + parallel_build(); + WriteLog("SELFTEST parallel: " + m_parallel.status + " " + m_parallel.error); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodeparallel.h b/application/editormodeparallel.h new file mode 100644 index 00000000..d10ef41b --- /dev/null +++ b/application/editormodeparallel.h @@ -0,0 +1,61 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editortool.h" + +#include +#include + +// second track alongside the selected path or the line through it, at a spacing, curves included +class parallel_tool final : public editor_tool +{ + public: + parallel_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_track_design &Design) : m_selection{Selection}, m_view{View}, m_history{History}, m_design{Design} {} + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + tool_tab &tab() { return m_tab; } + void render_set_options(); + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_track_design &m_design; + tool_tab m_tab; + struct parallel_state + { + double spacing{4.0}; + int side{1}; // 1: to the right, -1: to the left, of the direction of the line + int scope{1}; // 0: the selected path, 1: the line through it, up to the switches + TTrack *built_for{nullptr}; + double built_spacing{0.0}; + int built_side{0}; + int built_scope{-1}; + std::size_t built_history{0}; + TTrack *pair_src{nullptr}; + TTrack *pair_dst{nullptr}; + double measured{0.0}; + std::vector pieces; + std::vector styles; + double length{0.0}; + std::string error; + std::string status; + }; + parallel_state m_parallel; + TTrack *parallel_source() const; + TTrack *parallel_target() const; + void parallel_update(); + void render_parallel_controls(bool const Scope = true); + void parallel_build(); + void parallel_apply(); + bool parallel_preview_visible() const; +}; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index c49b12b7..cc11fa0b 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -82,15 +82,8 @@ double nice_step(double const Approximate) return power * 10.0; } -double route_speed(editor_track::route const &Route) -{ - double speed{-1.0}; - for (auto const &span : Route.spans) - speed = std::max(speed, editor_track::velocity(*span.track)); - return speed > 0.0 ? speed : 100.0; -} - std::string const kProfileMark{"//$p"}; +std::string const kFittedMark{"//$f"}; profile::line reversed_line(profile::line Line, double const Length) { @@ -256,6 +249,108 @@ void editor_mode::profile_library_mark(std::vector Layers) } } +void editor_mode::profiles_to_history(std::vector Library) +{ + if (m_history.empty() || m_history.back().action != EditorSnapshot::Action::TrackEdit || m_history.back().profiles) + return; + m_history.back().profiles = true; + m_history.back().profile_library = std::move(Library); +} + +void editor_mode::profiles_restore(std::vector Library) +{ + std::vector layers; + for (auto const *library : {&std::as_const(m_profiles), &std::as_const(Library)}) + for (auto const &entry : *library) + layers.push_back(entry.layer); + m_profiles = std::move(Library); + profile_library_mark(std::move(layers)); +} + +std::uint64_t editor_mode::fitted_shape(TTrack const &Track) +{ + // the points of the paths to a centimetre: what's read back from the files is the same shape + std::uint64_t hash{1469598103934665603ull}; + auto const add = [&](std::int64_t const Value) { + for (int i = 0; i < 8; ++i) + hash = (hash ^ static_cast((Value >> (8 * i)) & 0xff)) * 1099511628211ull; + }; + for (auto const &path : Track.m_paths) + for (auto const &point : path.points) + for (int axis = 0; axis < 3; ++axis) + add(std::llround(point[axis] * 100.0)); + return hash; +} + +void editor_mode::fitted_load() +{ + if (m_fitted_loaded) + return; + m_fitted_loaded = true; + m_fitted.clear(); + for (auto const layer : scene::Layers.marked_layers(kFittedMark)) + for (auto const &text : scene::Layers.marked(layer, kFittedMark)) + { + std::istringstream line{text}; + std::string keyword, name, shape; + line >> keyword >> name >> shape; + if (keyword == "fitted" && false == line.fail()) + m_fitted[name] = {layer, std::strtoull(shape.c_str(), nullptr, 16)}; + } +} + +void editor_mode::fitted_set(std::vector const &Tracks) +{ + fitted_load(); + std::vector layers; + for (auto const *track : Tracks) + { + if (track == nullptr || track->m_editorremoved || track->name().empty()) + continue; + scene::layer_handle layer{null_handle}; + if (false == scene::Layers.empty()) + { + layer = scene::Layers.resolve(track->layer()); + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + } + if (auto const found{m_fitted.find(track->name())}; found != m_fitted.end() && found->second.layer != layer) + layers.push_back(found->second.layer); + m_fitted[track->name()] = {layer, fitted_shape(*track)}; + layers.push_back(layer); + } + if (scene::Layers.empty()) + return; + std::sort(layers.begin(), layers.end()); + layers.erase(std::unique(layers.begin(), layers.end()), layers.end()); + for (auto const layer : layers) + { + if (false == scene::Layers.valid(layer)) + continue; + // the marks of the tracks there are no more, or changed since, aren't written again + std::vector lines; + for (auto const &[name, mark] : m_fitted) + { + if (mark.layer != layer) + continue; + auto const *track{simulation::Paths.find(name)}; + if (track == nullptr || track->m_editorremoved || fitted_shape(*track) != mark.shape) + continue; + if (lines.empty()) + lines.emplace_back("# the tracks whose grade line was fitted in the track editor, with their shape then; the simulation skips these lines"); + lines.push_back(format("fitted %s %016llx", name.c_str(), static_cast(mark.shape))); + } + scene::Layers.mark(layer, kFittedMark, std::move(lines)); + } +} + +bool editor_mode::grade_fitted(TTrack const &Track) +{ + fitted_load(); + auto const found{m_fitted.find(Track.name())}; + return found != m_fitted.end() && found->second.shape == fitted_shape(Track); +} + int editor_mode::profile_find(bool &Reversed) const { auto const &state{m_profile}; @@ -630,6 +725,20 @@ void editor_mode::profile_edited() first_step_done(first_step::profile_point); } +void editor_mode::profile_remove_point(int const Index) +{ + auto &state{m_profile}; + auto &points{state.line.points}; + if (Index <= 0 || Index + 1 >= static_cast(points.size())) + return; + points.erase(points.begin() + Index); + for (auto const neighbour : {Index - 1, Index}) + if (neighbour > 0 && neighbour + 1 < static_cast(points.size()) && false == points[neighbour].joint) + points[neighbour].automatic = true; + state.selected = -1; + profile_edited(); +} + void editor_mode::profile_open(TTrack *From, TTrack *To) { auto &state{m_profile}; @@ -732,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() @@ -901,7 +1008,7 @@ void editor_mode::profile_recognize() auto const norms{state.line.norms}; state.line = profile::recognize(chainages, elevations, grades, {}); state.line.norms = norms; - state.line.speed = route_speed(state.route); + state.line.speed = editor_track::route_speed(state.route); state.selected = -1; profile_check(); state.status = format(STR_P("Grade line recognized from the track: %zu point, largest departure %.3f m", "Grade line recognized from the track: %zu points, largest departure %.3f m", state.line.points.size()), state.line.points.size(), state.departure); @@ -919,7 +1026,7 @@ void editor_mode::profile_fit_ground() return; } auto settings{state.line}; - settings.speed = route_speed(state.route); + settings.speed = editor_track::route_speed(state.route); // the grade line is the rail top: it's fitted so the formation under it, not the rails, follows the ground auto ground{state.terrain}; for (std::size_t i = 0; i < ground.size() && i < state.beds.size(); ++i) @@ -1357,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() @@ -1444,7 +1562,7 @@ void editor_mode::profile_frame_view() fly_to(centre - right * (shift.x * pixel) + above * (shift.y * pixel), look, distance, 0.35); } -void editor_mode::profile_apply(bool const Terrain) +void editor_mode::profile_apply(bool const Terrain, std::vector const &Bridges, double const Fewest) { auto &state{m_profile}; state.status.clear(); @@ -1469,11 +1587,43 @@ void editor_mode::profile_apply(bool const Terrain) auto const before{state.samples}; std::vector> states; std::vector created; + std::vector ends; + for (auto const &span : state.route.spans) + ends.insert(ends.end(), {span.from, span.to}); + auto cuts{Fewest > 0.0 ? profile::fewest_breaks(line, ends, Fewest) : profile::breaks(line)}; + // a bridge ends at a joint or a break of the line by its abutment rather than leave a sliver of a path there + double constexpr kBridgeSnap{1.0}; // m + auto joints{cuts}; + joints.insert(joints.end(), ends.begin(), ends.end()); + auto bridges{Bridges}; + for (auto &bridge : bridges) + for (auto *end : {&bridge.from, &bridge.to}) + if (auto const nearest{std::min_element(joints.begin(), joints.end(), [&](double const A, double const B) { return std::abs(A - *end) < std::abs(B - *end); })}; + nearest != joints.end() && std::abs(*nearest - *end) <= kBridgeSnap) + *end = *nearest; + for (auto const &bridge : bridges) + cuts.insert(cuts.end(), {bridge.from, bridge.to}); + std::sort(cuts.begin(), cuts.end()); auto const gaps{editor_track::apply_profile( - state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, profile::breaks(line), states, created)}; + state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, cuts, states, created)}; + // the paths over a bridge sound and light as on one, and get no formation; within what a cut too near an end leaves on + double constexpr kBridgeEnd{0.5}; // m + std::size_t bridged{0}; + for (auto const &span : state.route.spans) + if (span.track->eEnvironment != e_bridge && span.track->eEnvironment != e_tunnel + && std::any_of(bridges.begin(), bridges.end(), [&](profile::departure const &Bridge) { return span.from >= Bridge.from - kBridgeEnd && span.to <= Bridge.to + kBridgeEnd; })) + { + span.track->eEnvironment = e_bridge; + ++bridged; + } auto const changed{states.size()}; auto const added{created.size()}; state.created = created; + { + auto marked{tracks}; + marked.insert(marked.end(), created.begin(), created.end()); + fitted_set(marked); + } // the stored design changes with the paths, an undo takes both back profile_library_load(); auto library{m_profiles}; @@ -1493,7 +1643,9 @@ void editor_mode::profile_apply(bool const Terrain) state.status = format(STR_P("Grade line applied: %zu path changed, %zu added, largest departure %.3f m", "Grade line applied: %zu paths changed, %zu added, largest departure %.3f m", changed), changed, added, state.departure); if (false == scene::Layers.empty()) state.status += STR_C("\nThe paths and the design (//$p lines) go to the scenery files on save"); - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; + if (bridged > 0) + state.status += format(STR_P("\nOver a bridge or a viaduct: %zu path", "\nOver a bridge or a viaduct: %zu paths", bridged), bridged); + auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const closed{std::count_if(gaps.begin(), gaps.end(), [](auto const &Gap) { return Gap.closed; })}; if (closed > 0) state.status += STR_C("\nPaths off the route brought to its height:"); @@ -1912,6 +2064,8 @@ bool editor_mode::render_profile_parameters() ImGui::EndTable(); } norms.grade_switch_max = std::max(0.0, norms.grade_switch_max); + norms.radius_factor = std::max(0.0, norms.radius_factor); + norms.radius_min = std::max(10.0, norms.radius_min); if (ImGui::SmallButton(STR_C("Automatic R at all points"))) { for (auto &point : line.points) @@ -2084,9 +2238,8 @@ void editor_mode::render_profile_point() ImGui::SameLine(); if (ImGui::Button(STR_C("Remove Del"))) { - points.erase(points.begin() + index); - state.selected = -1; - changed = true; + profile_remove_point(index); + return; } } } @@ -2232,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) @@ -2254,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(); @@ -2511,11 +2669,7 @@ bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hover else state.curve_grip = -1; if ((Hovered || ImGui::IsWindowFocused()) && state.selected > 0 && state.selected + 1 < static_cast(points.size()) && false == points[state.selected].joint && ImGui::IsKeyPressed(ImGuiKey_Delete)) - { - points.erase(points.begin() + state.selected); - state.selected = -1; - changed = true; - } + profile_remove_point(state.selected); return changed; } diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp index 711dba03..3e855066 100644 --- a/application/editormoderecords.cpp +++ b/application/editormoderecords.cpp @@ -340,6 +340,128 @@ std::optional editor_mode::double_of(TTrack const &T return std::nullopt; } +std::optional editor_mode::slip_of(TTrack const &Track) const +{ + if (Track.m_editorremoved) + return std::nullopt; + auto const record_of = [](TTrack const &Each) -> std::optional { + if (Each.eType != tt_Switch || Each.m_editorremoved) + return std::nullopt; + auto found{switch_store::named(Each)}; + return found.has_value() && found->kind == "slip" ? found : std::nullopt; + }; + // the record of the switch, or of a switch of the slip the track between them is joined to + auto known{record_of(Track)}; + if (false == known.has_value() && Track.eType == tt_Normal && Track.m_paths.size() == 1) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + for (auto const &[other, point] : editor_track::connected_points(Track, Track.m_paths.front().points[index])) + if (false == known.has_value()) + known = record_of(*other); + if (false == known.has_value()) + return std::nullopt; + slip_set slip; + slip.record = *known; + for (auto const &name : known->parts) + { + auto *part{simulation::Paths.find(name)}; + if (part == nullptr || part->m_editorremoved || part->eType != tt_Switch || part->m_paths.size() < 2) + return std::nullopt; + slip.switches.push_back(part); + } + 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 &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{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 * 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) + for (auto const &path : slip.parts[i]->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const &position{path.points[index]}; + if (glm::distance(plan_of(position), crossing) > inside) + continue; + for (auto const &[other, point] : editor_track::connected_points(*slip.parts[i], position)) + if (false == other->m_editorremoved && std::find(slip.parts.begin(), slip.parts.end(), other) == slip.parts.end()) + slip.parts.push_back(other); + } + // its ends, each with the way into the slip there + std::vector> ends; + for (auto const *part : slip.parts) + for (auto const &path : part->m_paths) + for (auto const atend : {false, true}) + { + auto const &position{path.points[atend ? segment_data::point::end : segment_data::point::start]}; + if (glm::distance(plan_of(position), crossing) <= inside || + std::any_of(ends.begin(), ends.end(), [&](auto const &End) { return glm::distance(End.first, position) < geometry::same_point; })) + continue; + auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const into{plan_of(control != glm::dvec3{} ? control : path.points[atend ? segment_data::point::start : segment_data::point::end] - position)}; + if (glm::length(into) < 1e-9) + return std::nullopt; + ends.emplace_back(position, glm::normalize(into)); + } + if (ends.size() != 4) + return std::nullopt; + // A where its first switch starts, B on along its first track, D where its last switch starts, C the other one + auto const at = [&](glm::dvec2 const &Point) { + return static_cast(std::min_element(ends.begin(), ends.end(), [&](auto const &One, auto const &Other) { return glm::distance(plan_of(One.first), Point) < glm::distance(plan_of(Other.first), Point); }) - + ends.begin()); + }; + auto const ia{at(a)}; + auto const id{at(d)}; + if (ia == id) + return std::nullopt; + std::vector others; + for (std::size_t i = 0; i < 4; ++i) + if (i != ia && i != id) + others.push_back(i); + if (others.size() != 2) + return std::nullopt; + // of the other two the one farther on along the first track + auto const ahead = [&](std::size_t const Index) { return glm::dot(plan_of(ends[Index].first) - crossing, da); }; + auto const ib{ahead(others[0]) >= ahead(others[1]) ? others[0] : others[1]}; + auto const ic{ib == others[0] ? others[1] : others[0]}; + std::size_t const order[]{ia, ib, ic, id}; + for (std::size_t i = 0; i < 4; ++i) + { + slip.ends[i] = ends[order[i]].first; + slip.into[i] = ends[order[i]].second; + } + return slip; +} + +std::array editor_mode::slip_throughs(slip_set const &Slip) +{ + // from end to end, a straight where the slip is one, else heading out of the ends as the slip does + auto const through = [&](std::size_t const From, std::size_t const To) { + segment_data path; + path.points[segment_data::point::start] = Slip.ends[From]; + path.points[segment_data::point::end] = Slip.ends[To]; + auto const chord{plan_of(Slip.ends[To] - Slip.ends[From])}; + auto const length{glm::length(chord)}; + if (length > 1e-6 && std::abs(geometry::cross(Slip.into[From], chord / length)) + std::abs(geometry::cross(Slip.into[To], chord / length)) > 1e-5) + { + auto const handle{length / 3.0}; + auto const rise{Slip.ends[To].y - Slip.ends[From].y}; + path.points[segment_data::point::control1] = {Slip.into[From].x * handle, rise / 3.0, Slip.into[From].y * handle}; + path.points[segment_data::point::control2] = {Slip.into[To].x * handle, -rise / 3.0, Slip.into[To].y * handle}; + path.radius = static_cast(Slip.record.main_radius); + } + return path; + }; + return {through(0, 1), through(2, 3)}; +} + std::vector editor_mode::double_paths(double_switch const &Set) { auto link{Set.link->m_paths.front()}; @@ -369,6 +491,8 @@ void editor_mode::record_slip(std::vector const &Parts, std::string co if (first == nullptr || first->m_paths.empty()) return; switch_store::place(entry, *first); + // a slip bent into a curve, as a switch is + entry.main_radius = switch_store::main_radius_of(*first); switch_store::put(entry, alignment_store::layer_of(*first)); } diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index a176a4b8..651f06dc 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -52,10 +52,19 @@ bool editor_mode::route_active() const void editor_mode::route_recommend(alignment::vertex &Vertex) const { - auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, m_route.design.norms)}; + auto sharpest{Vertex.radius}; + if (Vertex.compound) + for (auto const &arc : Vertex.arcs) + sharpest = std::min(sharpest, arc.radius); + auto const recommended{alignment::recommend(m_route.design.speed, sharpest, laying_norms())}; Vertex.cant = recommended.cant; + // the cants of the arcs in proportion to their curvature, as the norms recommend them + Vertex.first_cant = -1.0; + for (auto &arc : Vertex.arcs) + arc.cant = -1.0; Vertex.transition_in = recommended.transition; Vertex.transition_out = recommended.transition; + alignment::recommend_between(Vertex, m_route.design.speed, laying_norms()); } void editor_mode::route_bind_ends() @@ -70,6 +79,8 @@ void editor_mode::route_bind_ends() design.end_grade = grade_of(chain.end_direction); design.start_radius = chain.start_radius; design.end_radius = chain.end_radius; + design.start_turn = chain.start_turn; + design.end_turn = chain.end_turn; design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance)); design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance)); } @@ -83,6 +94,10 @@ void editor_mode::route_reset() route.vertex = -1; route.grip = -1; route.result = {}; + route.trace = {}; + route.clicks.clear(); + route.click_states.clear(); + route.measured = -1; if (route.from == nullptr || route.to == nullptr) return; if (false == editor_track::find_chain(route.from, route.to, route.chain, route.error, true)) @@ -146,6 +161,7 @@ bool editor_mode::route_from_curve(TTrack &Track) if (curve.reversals == 0) { design.vertices = {vertex}; + route_curved_ends(); } else { @@ -154,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()) @@ -181,6 +255,9 @@ bool editor_mode::route_load(TTrack &Track) if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track))) return false; route_read_stored(Track); + // the points clicked along the line when it was laid, wherever they're kept + m_route.clicks = alignment_store::clicks_along(m_route.result, 3.0); + m_route.measured = -1; return true; } @@ -259,6 +336,11 @@ void editor_mode::route_take_stored(int const Index) design.norms = stored.design.norms; design.transition_pieces = stored.design.transition_pieces; design.arc_piece_angle = stored.design.arc_piece_angle; + // a curve laid going on from the curve of the track adjoining keeps going on from it, its radius the one which begins it there + design.start_curved = stored.design.start_curved; + design.end_curved = stored.design.end_curved; + design.start_cant = stored.design.start_cant; + design.end_cant = stored.design.end_cant; route.designed = static_cast(design.vertices.size()); route.origin = stored.origin; route.vertex = -1; @@ -279,7 +361,7 @@ void editor_mode::render_route_stored() ImGui::EndTable(); } auto const &records{alignment_store::records()}; - if (route.departed < 0 || route.departed >= static_cast(records.size())) + if (route.departed < 0 || route.departed >= static_cast(records.size()) || false == scene::Layers.valid(records[route.departed].layer)) return; auto const &layer{scene::Layers.layer(records[route.departed].layer).name}; ImGui::PushTextWrapPos(0.0f); @@ -307,7 +389,7 @@ void editor_mode::route_store() auto &route{m_route}; if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) return; - alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front())); + alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front()), "designed", 0.0, route.clicks); route.departed = -1; route.departure = 0.0; } @@ -381,7 +463,7 @@ int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *R if (count == 1) { ++Skipped; - reason = (Chain.tracks.front()->name().empty() ? std::string{STR_C("(no name)")} : Chain.tracks.front()->name()) + ": " + reason; + reason = name_of(*Chain.tracks.front()) + ": " + reason; WriteLog("Editor: digitizing - " + reason, logtype::generic); if (Reason != nullptr && Reason->empty()) *Reason = reason; @@ -404,7 +486,7 @@ int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *R bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string *Reason, TTrack **Culprit) { - if (Chain.tracks.empty()) + if (Chain.tracks.empty() || scene::Layers.empty()) return false; auto const name = [](TTrack const *Track) { return Track == nullptr || Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; auto const nearest = [&](glm::dvec2 const &Point) { @@ -434,6 +516,8 @@ bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string * design.end_grade = grade_of(Chain.end_direction); design.start_radius = Chain.start_radius; design.end_radius = Chain.end_radius; + design.start_turn = Chain.start_turn; + design.end_turn = Chain.end_turn; auto const preferred{m_route.design.shape}; auto const other{preferred == alignment::transition_shape::clothoid ? alignment::transition_shape::cubic_parabola : alignment::transition_shape::clothoid}; alignment::result result; @@ -511,12 +595,12 @@ bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string * for (auto const index : covered) { auto const &old{records[index]}; - auto const start{alignment_store::chainage_on(result, old.design.start)}; - auto const end{alignment_store::chainage_on(result, old.design.end)}; + auto const start{alignment_store::chainage_on(result, alignment_store::kept_start(old))}; + auto const end{alignment_store::chainage_on(result, alignment_store::kept_end(old))}; if (false == start.has_value() || false == end.has_value()) return false; if (index == *covered.begin() && *end > *start) - origin = old.origin - *start; + origin = old.origin + old.from - *start; } auto const layer{covered.empty() ? alignment_store::layer_of(*Chain.tracks.front()) : records[*covered.begin()].layer}; for (auto index{covered.rbegin()}; index != covered.rend(); ++index) @@ -566,16 +650,25 @@ 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; } void editor_mode::route_apply() +{ + m_route.error.clear(); + route_lay(); + if (false == m_route.error.empty()) + refused(m_route.error); +} + +void editor_mode::route_lay() { auto &route{m_route}; route.status.clear(); + m_infra_carried.clear(); if (false == route.result.valid || route.chain.tracks.empty()) return; editor_track::chain chain; @@ -633,16 +726,23 @@ void editor_mode::route_apply() std::vector> states; std::vector created; std::vector removed; + // the designs of the outer tracks come down with the lifts taken back, an undo puts them back + profile_library_load(); + auto library{m_profiles}; + bool lifted{false}; for (auto *switchtrack : switches) if (auto const crossover{crossover_of(*switchtrack)}) { TTrack *first{nullptr}; TTrack *second{nullptr}; + lifted = lifted || std::abs(crossover->record.lift) > 0.002; take_out_crossover(*crossover, states, created, removed, first, second); } switch_store::follow(); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); records_to_history(); + if (lifted) + profiles_to_history(std::move(library)); show_track_tab(tab); // the line between the same points again: taking the lift back may have cut its paths auto const end_at = [&](glm::dvec3 const &Point) -> TTrack * { @@ -722,8 +822,8 @@ void editor_mode::route_apply() to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); std::string error; auto const found{editor_track::find_chain(from, to, chain, error, true)}; - auto const startoff{found ? plan_distance(chain.start, route.result.start) : 0.0}; - auto const endoff{found ? plan_distance(chain.end, route.result.end) : 0.0}; + auto const startoff{found && startextension > 0.0 ? plan_distance(chain.start, route.result.start) : 0.0}; + auto const endoff{found && endextension > 0.0 ? plan_distance(chain.end, route.result.end) : 0.0}; if (false == found || startoff > 5.0 || endoff > 5.0) { auto const startrun{editor_track::straight_beyond(before, false, m_straights.tolerance)}; @@ -746,9 +846,10 @@ void editor_mode::route_apply() std::vector relaid; if (chain.switches.empty()) { - auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + std::vector intervals; + auto pieces{alignment::pieces_at(route.result, route.design, chain.tracks.size(), editor_track::chain_joints(chain), intervals)}; editor_track::keep_heights(chain, pieces); - relaid = editor_track::relay(chain, pieces, &to); + relaid = editor_track::relay(chain, pieces, &to, &intervals); } else if (false == route_relay_through_switches(chain, states, relaid, to)) { @@ -779,6 +880,7 @@ void editor_mode::route_apply() route.status = added > 0 ? format(STR_P("Re-laid %zu path, added %zu", "Re-laid %zu paths, added %zu", chain.tracks.size()), chain.tracks.size(), added) : format(STR_P("Re-laid %zu path", "Re-laid %zu paths", chain.tracks.size()), chain.tracks.size()); if (startextension > 0.0 || endextension > 0.0) route.status += format(STR_C(", the curve took over %.2f / %.2f m of the adjoining straights"), startextension, endextension); + route.status += infra_carried_note(); // the crossovers taken out before which fit the lines now if (false == route.relaying && false == route.crossovers_out.empty()) { @@ -805,7 +907,7 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, { auto &route{m_route}; auto const &result{route.result}; - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; + auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const plan = [](glm::dvec3 const &Vector) { return glm::normalize(plan_of(Vector)); }; struct placement { @@ -830,14 +932,14 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, } auto const oldlength{before}; std::vector placements; - std::vector breaks; + std::vector ends; // of the switches along the new line for (std::size_t k = 0; k < Chain.switches.size(); ++k) { auto const &passage{Chain.switches[k]}; auto const &main{passage.track->m_paths.front()}; auto const length{bezier{main}.plan_length()}; auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])}; - if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01)) + if (from < 0.0 || from + length > result.length || (false == ends.empty() && from < ends.back() - 0.01)) { route.error = format(STR_C("The switch %s doesn't fit on the designed line"), name(passage.track).c_str()); return false; @@ -860,22 +962,50 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, return false; } placements.push_back({passage.track, from, length, entry, angle}); - breaks.push_back(from); - breaks.push_back(from + length); + ends.push_back(from); + ends.push_back(from + length); } - // the regular parts between the switches, one interval each, the switches taking the ones between + // the regular parts between the switches, cut at the joints of their paths set square onto the new line where they follow one + // another along it, else one interval each; the switches taking the ones between auto const parts{editor_track::chain_parts(Chain)}; + std::vector breaks; std::vector counts; - std::vector partof; + std::vector> owners; // of each interval: the part, -1 for none, and its path, -1 for any std::size_t part{0}; for (std::size_t k = 0; k <= Chain.switches.size(); ++k) { auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before}; auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before}; - counts.push_back(last - first); - partof.push_back(last > first ? static_cast(part++) : -1); + if (last > first && part < parts.size()) + { + auto const inner{alignment::breaks_at(result, editor_track::chain_joints(parts[part]), k == 0 ? 0.0 : ends[2 * k - 1], k == Chain.switches.size() ? result.length : ends[2 * k])}; + if (last - first > 1 && inner.size() + 1 == last - first) + for (std::size_t j = 0; j < last - first; ++j) + { + if (j > 0) + breaks.push_back(inner[j - 1]); + counts.push_back(1); + owners.emplace_back(static_cast(part), static_cast(j)); + } + else + { + counts.push_back(last - first); + owners.emplace_back(static_cast(part), -1); + } + ++part; + } + else + { + counts.push_back(last - first); + owners.emplace_back(-1, -1); + } if (k < Chain.switches.size()) + { + breaks.push_back(ends[2 * k]); + breaks.push_back(ends[2 * k + 1]); counts.push_back(1); + owners.emplace_back(-1, -1); + } } if (part != parts.size()) { @@ -885,9 +1015,15 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector intervals; auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)}; std::vector> groups(parts.size()); - for (std::size_t i = 0; i < pieces.size(); ++i) - if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0) - groups[partof[intervals[i] / 2]].push_back(pieces[i]); + std::vector> paths(parts.size()); // of each piece of a part, its path, where its joints cut the line + for (std::size_t i = 0; i < pieces.size() && i < intervals.size(); ++i) + if (intervals[i] < owners.size() && owners[intervals[i]].first >= 0) + { + auto const &[owner, path]{owners[intervals[i]]}; + groups[owner].push_back(pieces[i]); + if (path >= 0) + paths[owner].push_back(static_cast(path)); + } for (std::size_t i = 0; i < parts.size(); ++i) if (groups[i].size() < parts[i].tracks.size()) { @@ -924,7 +1060,7 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, auto group{groups[i]}; editor_track::keep_heights(parts[i], group); TTrack *last{nullptr}; - auto const added{editor_track::relay(parts[i], group, &last)}; + auto const added{editor_track::relay(parts[i], group, &last, paths[i].size() == group.size() ? &paths[i] : nullptr)}; if (last != nullptr && parts[i].tracks.back() == Chain.tracks.back()) Last = last; Created.insert(Created.end(), added.begin(), added.end()); @@ -988,6 +1124,126 @@ bool editor_mode::route_hit(int &Vertex, int &Grip) const return hit >= 0 || griphit >= 0; } +void editor_mode::route_show_curve(double const From, double const To) +{ + auto const &result{m_route.result}; + if (result.elements.empty() || To <= From) + return; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{std::numeric_limits::lowest()}; + for (int i = 0; i <= 32; ++i) + { + auto const point{alignment::evaluate(result, From + (To - From) * i / 32.0).position}; + low = glm::min(low, point); + high = glm::max(high, point); + } + auto const along{plan_of(alignment::evaluate(result, (From + To) * 0.5).direction)}; + auto const flat{glm::length(along) > 1e-9 ? glm::normalize(along) : glm::dvec2{0.0, 1.0}}; + // from above, leaning a little along the line, the whole curve in sight + auto const look{glm::normalize(glm::dvec3{flat.x * 0.45, -1.0, flat.y * 0.45})}; + auto const extent{std::max(high.x - low.x, high.z - low.z)}; + fly_to((low + high) * 0.5, look, std::clamp(extent * 1.2, 60.0, 3000.0)); +} + +std::vector editor_mode::route_focused() const +{ + using part = route_design::curve_focus::part; + auto const &focus{m_route.focus}; + auto const &result{m_route.result}; + std::vector own; + if (focus.vertex < 0) + return own; + for (std::size_t i = 0; i < result.elements.size(); ++i) + if (result.elements[i].vertex == focus.vertex && result.elements[i].length > 1e-6) + own.push_back(i); + if (own.empty() || focus.what == part::whole) + return own; + // 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) + if (curve.vertex == focus.vertex) + radii = curve.radii.size() > 1 ? curve.radii : std::vector{curve.radius}; + std::vector found; + for (auto const i : own) + { + auto const [start, end] {alignment::end_curvatures(result.elements[i])}; + auto const arc{static_cast(focus.arc)}; + switch (focus.what) + { + case part::entry: if (i == own.front() && start == 0.0 && end > 0.0) found.push_back(i); break; + case part::exit: if (i == own.back() && end == 0.0 && start > 0.0) found.push_back(i); break; + case part::arc: if (arc < radii.size() && radii[arc] > 0.0 && same(start, 1.0 / radii[arc]) && same(end, start)) found.push_back(i); break; + case part::between: + if (arc >= 1 && arc < radii.size() && radii[arc] > 0.0 && radii[arc - 1] > 0.0 && same(start, 1.0 / radii[arc - 1]) && same(end, 1.0 / radii[arc])) + found.push_back(i); + break; + default: break; + } + } + // a part of no length, the arc of two transition curves meeting say, shows the whole curve + return found.empty() ? own : found; +} + +void editor_mode::route_measure_clicks() +{ + auto &route{m_route}; + if (route.measured == route.revision) + return; + route.measured = route.revision; + route.click_states.clear(); + auto const &result{route.result}; + if (route.clicks.empty() || false == result.valid) + return; + std::vector plan; + for (auto const &click : route.clicks) + plan.push_back(plan_of(click)); + auto const projections{alignment::project(result, plan)}; + auto const count{static_cast(route.design.vertices.size())}; + for (std::size_t i = 0; i < plan.size() && i < projections.size(); ++i) + { + route_design::click_state state; + state.offset = projections[i].offset; + // the curve nearest along the line, as the points traced in the plan go to their curves + for (int k = 0; k < count && k < static_cast(result.vertex_chainages.size()); ++k) + if (state.vertex < 0 || std::abs(result.vertex_chainages[k] - projections[i].chainage) < std::abs(result.vertex_chainages[state.vertex] - projections[i].chainage)) + state.vertex = k; + for (int k = 0; k < count && false == state.corner; ++k) + state.corner = plan_distance(route.clicks[i], route_vertex_position(k)) < 0.05; + route.click_states.push_back(state); + } +} + +std::vector editor_mode::route_clicks_of(int const Vertex) const +{ + auto const &route{m_route}; + std::vector points; + for (std::size_t i = 0; i < route.click_states.size() && i < route.clicks.size(); ++i) + { + auto const &state{route.click_states[i]}; + // a point clicked far off the line belongs to none of its curves + if (state.vertex == Vertex && false == state.corner && std::abs(state.offset) <= 3.0) + points.push_back(plan_of(route.clicks[i])); + } + return points; +} + +bool editor_mode::route_hold_clicks(int const Vertex) +{ + auto &route{m_route}; + route_measure_clicks(); + auto const points{route_clicks_of(Vertex)}; + if (points.size() < 3 || Vertex < 0 || Vertex >= static_cast(route.design.vertices.size())) + return false; + auto design{route.design}; + design.norms.checked = true; + auto const fit{alignment::refit_radii(design, static_cast(Vertex), points)}; + if (false == fit.valid) + return false; + route.design.vertices[Vertex] = fit.fitted; + return true; +} + void editor_mode::draw_route_overlay() const { auto const &route{m_route}; @@ -1035,6 +1291,76 @@ void editor_mode::draw_route_overlay() const previous = next; } } + // the cant ramps: their ends across the track, the ones moved off the transitions brighter, with how the cant goes along them + for (auto const &ramp : result.ramps) + { + if (ramp.cant < 0.5) + continue; + auto const colour{ramp.own ? IM_COL32(255, 170, 40, 255) : IM_COL32(255, 170, 40, 120)}; + for (auto const end : {ramp.from, ramp.to}) + { + auto const station{alignment::evaluate(result, end)}; + glm::dvec3 const across{-station.direction.z * 2.0, 0.0, station.direction.x * 2.0}; + projection.line(drawlist, station.position - across, station.position + across, colour, 3.0f); + } + ImVec2 screen; + if (ramp.own && projection.project(alignment::evaluate(result, (ramp.from + ramp.to) * 0.5).position, screen)) + drawlist->AddText({screen.x + 8.0f, screen.y + 6.0f}, colour, format(STR_C("ramp %.0f m, %.0f mm"), ramp.to - ramp.from, ramp.cant).c_str()); + } + // the part of the curve whose fields are pointed at or edited in the panel, lit up and named + if (auto const focused{route_focused()}; false == focused.empty()) + { + using part = route_design::curve_focus::part; + auto const &focus{route.focus}; + for (auto const index : focused) + { + auto const &e{result.elements[index]}; + int const steps = std::clamp(static_cast(e.length / 2.0), 1, 400); + auto previous{alignment::evaluate(result, e.chainage).position}; + for (int j = 1; j <= steps; ++j) + { + auto const next{alignment::evaluate(result, e.chainage + e.length * j / steps).position}; + projection.line(drawlist, previous, next, IM_COL32(60, 230, 255, 120), 11.0f); + projection.line(drawlist, previous, next, IM_COL32(200, 250, 255, 255), 3.0f); + previous = next; + } + } + auto const &first{result.elements[focused.front()]}; + auto const &last{result.elements[focused.back()]}; + alignment::curve_report const *report{nullptr}; + for (auto const &curve : result.curves) + if (curve.vertex == focus.vertex) + report = &curve; + std::string name; + if (report != nullptr) + { + auto const compound{report->radii.size() > 1}; + auto const arc{static_cast(focus.arc)}; + switch (focus.what) + { + case part::entry: name = format(STR_C("W%d transition in %.0f m"), focus.vertex + 1, first.length); break; + case part::exit: name = format(STR_C("W%d transition out %.0f m"), focus.vertex + 1, last.length); break; + case part::between: name = format(STR_C("W%d transition R%d-R%d %.0f m"), focus.vertex + 1, focus.arc, focus.arc + 1, first.length); break; + case part::arc: + if (compound && arc < report->radii.size()) + name = format(STR_C("W%d R%d %.0f m cant %.0f mm"), focus.vertex + 1, focus.arc + 1, report->radii[arc], arc < report->cants.size() ? report->cants[arc] : report->cant); + else + name = format(STR_C("W%d R %.0f m cant %.0f mm"), focus.vertex + 1, report->radius, report->cant); + break; + default: name = format("W%d ", focus.vertex + 1) + curve_label(*report, design.norms); break; + } + } + ImVec2 middle; + if (false == name.empty() && projection.project(alignment::evaluate(result, (first.chainage + last.chainage + last.length) * 0.5).position, middle)) + { + auto const size{ImGui::GetFontSize() * 1.25f}; + auto const text{ImGui::GetFont()->CalcTextSizeA(size, FLT_MAX, 0.0f, name.c_str())}; + ImVec2 const corner{middle.x - text.x * 0.5f, middle.y - text.y - 22.0f}; + drawlist->AddRectFilled(ImVec2(corner.x - 6.0f, corner.y - 3.0f), ImVec2(corner.x + text.x + 6.0f, corner.y + text.y + 3.0f), IM_COL32(10, 20, 30, 210), 4.0f); + drawlist->AddText(ImGui::GetFont(), size, corner, IM_COL32(200, 250, 255, 255), name.c_str()); + drawlist->AddLine(ImVec2(middle.x, corner.y + text.y + 3.0f), middle, IM_COL32(200, 250, 255, 200), 1.5f); + } + } ImVec2 pointed; if (route.band.at >= 0.0 && projection.project(alignment::evaluate(result, std::min(route.band.at, result.length)).position, pointed)) { @@ -1078,6 +1404,172 @@ void editor_mode::draw_route_overlay() const } drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); } + // the points clicked when the line was laid, in the colours of their departure; how far each is, along the curve in focus + for (std::size_t i = 0; i < route.clicks.size() && i < route.click_states.size(); ++i) + { + auto const &state{route.click_states[i]}; + ImVec2 screen; + if (state.corner || std::abs(state.offset) > 3.0 || false == projection.project(route.clicks[i], screen)) + continue; + auto const departure{std::abs(state.offset)}; + auto const colour{departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255)}; + drawlist->AddCircleFilled(screen, 4.5f, IM_COL32(0, 0, 0, 200), 12); + drawlist->AddCircleFilled(screen, 3.0f, colour, 12); + if (state.vertex == route.focus.vertex) + drawlist->AddText(ImVec2(screen.x + 7.0f, screen.y - 7.0f), colour, format("%.0f cm", departure * 100.0).c_str()); + } + if (route.trace.points.empty()) + return; + std::vector plan; + for (auto const &point : route.trace.points) + plan.push_back(plan_of(point)); + auto const projections{alignment::project(result, plan)}; + auto const hit{route_trace_hit()}; + for (std::size_t i = 0; i < route.trace.points.size(); ++i) + { + ImVec2 screen; + if (false == projection.project(route.trace.points[i], screen)) + continue; + auto const departure{i < projections.size() ? std::abs(projections[i].offset) : 0.0}; + auto const colour{departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255)}; + drawlist->AddCircle(screen, 5.0f, IM_COL32(0, 0, 0, 200), 12, 3.5f); + drawlist->AddCircle(screen, 5.0f, colour, 12, 2.0f); + if (static_cast(i) == hit) + drawlist->AddCircle(screen, 10.0f, overlay_color::highlight, 16, 2.0f); + if (i < projections.size()) + drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 7.0f), colour, format("%.0f cm", departure * 100.0).c_str()); + } +} + +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) + 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"}})) + route_trace_fit(); + if (trace.points.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("Click points along the middle of the track on the photo, spread over the whole curve; a few on the straights at its ends help too")); + ImGui::PopTextWrapPos(); + } + else + { + auto const colour = [](double const Departure) { return Departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : Departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255); }; + metric_tiles({{format("%zu", trace.points.size()), STR_C("points"), ImGui::GetColorU32(ImGuiCol_Text)}, + {format("%.0f cm", trace.rms * 100.0), STR_C("departure"), colour(trace.rms)}, + {format("%.0f cm", trace.worst * 100.0), STR_C("at most"), colour(trace.worst)}}); + } + if (main_button(STR_C("Apply Enter"), ImVec2(-1.0f, 0.0f))) + route_trace_end(true); + if (ImGui::Button(STR_C("Drop Esc"))) + route_trace_end(false); + ImGui::SameLine(); + ImGui::BeginDisabled(trace.points.empty()); + if (ImGui::Button(STR_C("Take out the last point Backspace"))) + { + trace.points.pop_back(); + route_trace_fit(); + } + ImGui::EndDisabled(); +} + +void editor_mode::route_trace_start() +{ + auto &trace{m_route.trace}; + trace.active = true; + trace.points.clear(); + trace.before = m_route.design; + trace.rms = 0.0; + trace.worst = 0.0; +} + +// every point goes to the curve nearest to it along the line, the curves without points keep their shape +void editor_mode::route_trace_fit() +{ + auto &route{m_route}; + auto &trace{route.trace}; + auto design{route.design}; + trace.rms = 0.0; + trace.worst = 0.0; + if (trace.points.empty() || design.vertices.empty() || false == route.result.valid) + return; + std::vector plan; + for (auto const &point : trace.points) + plan.push_back(plan_of(point)); + auto const projections{alignment::project(route.result, plan)}; + std::vector> owned(design.vertices.size()); + for (std::size_t i = 0; i < plan.size(); ++i) + { + std::size_t nearest{0}; + for (std::size_t k = 1; k < route.result.vertex_chainages.size() && k < owned.size(); ++k) + if (std::abs(route.result.vertex_chainages[k] - projections[i].chainage) < std::abs(route.result.vertex_chainages[nearest] - projections[i].chainage)) + nearest = k; + owned[nearest].push_back(plan[i]); + } + for (std::size_t k = 0; k < owned.size(); ++k) + { + if (owned[k].empty()) + continue; + auto const fit{alignment::fit_trace(design, k, owned[k], trace.radii)}; + if (fit.valid) + design.vertices[k] = fit.fitted; + } + route.design = design; + route_update(); + double sum{0.0}; + for (auto const &projection : alignment::project(route.result, plan)) + { + sum += projection.offset * projection.offset; + trace.worst = std::max(trace.worst, std::abs(projection.offset)); + } + trace.rms = std::sqrt(sum / static_cast(plan.size())); +} + +void editor_mode::route_trace_end(bool const Keep) +{ + auto &trace{m_route.trace}; + if (false == trace.active) + return; + auto const traced{false == trace.points.empty()}; + if (false == Keep) + { + m_route.design = trace.before; + route_update(); + } + trace = {}; + if (Keep && traced) + route_apply(); +} + +// the photo under the cursor where the depth finds it, else the level of the design there +glm::dvec3 editor_mode::route_trace_cursor() const +{ + if (GfxRenderer->Mouse_Hit()) + return cursor_ground(); + auto const &result{m_route.result}; + auto point{cursor_level(alignment::elevation(result, result.length * 0.5))}; + if (auto const projections{alignment::project(result, {plan_of(point)})}; false == projections.empty()) + point = cursor_level(alignment::elevation(result, projections.front().chainage)); + return point; +} + +int editor_mode::route_trace_hit() const +{ + screen_projection const projection; + float best{kHandleRadius * kHandleRadius}; + int hit{-1}; + for (int i = 0; i < static_cast(m_route.trace.points.size()); ++i) + if (auto const distance{projection.mouse_distance2(m_route.trace.points[i])}; distance < best) + { + best = distance; + hit = i; + } + return hit; } void editor_mode::render_route_gizmo() @@ -1102,6 +1594,8 @@ void editor_mode::render_route_gizmo() } return; } + if (false == m_route_dragging) + m_route_drag_from = route.design; m_route_dragging = true; if (grip) { @@ -1136,8 +1630,6 @@ void editor_mode::render_route_gizmo() void editor_mode::render_route_ui() { auto &route{m_route}; - render_norms_choice(); - ImGui::Separator(); if (false == route.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); if (route.chain.tracks.empty()) @@ -1156,6 +1648,7 @@ void editor_mode::render_route_ui() } item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); auto changed{render_route_band()}; + render_route_trace(); render_route_stored(); changed |= render_route_vertices(); if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) @@ -1218,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); @@ -1257,9 +1743,49 @@ bool editor_mode::render_route_band() auto const mouse{ImGui::GetIO().MousePos}; auto const count{static_cast(result.elements.size())}; - // the element under the mouse and what of it a drag changes: near the ends of a plateau its transitions, also the ones of no length yet + // what of its vertex each element is: an arc, the arcs of a compound curve counted from the first one, which are elements of an even + // curvature as an arc is, or a transition, in from the straight, out to it, or between two arcs, which goes on from the arc before it + struct role + { + int part{0}; + int arc{0}; + }; + std::vector roles(static_cast(count)); + std::vector arcs(design.vertices.size(), 0); + { + int vertex{-1}; + int arc{-1}; + double last{0.0}; + for (int i = 0; i < count; ++i) + { + auto const &element{result.elements[i]}; + if (element.vertex != vertex) + { + vertex = element.vertex; + arc = -1; + last = 0.0; + } + if (element.vertex < 0 || element.vertex >= static_cast(design.vertices.size()) || element.kind == alignment::element_kind::straight) + continue; + auto const [from, to]{ends(element)}; + if (element.kind == alignment::element_kind::arc || (std::abs(from - to) <= 1e-6 * std::abs(from) && std::abs(from) > 1e-9)) + { + // the next arc where the curvature isn't the one of the arc before, an arc cut in parts staying one + if (arc < 0 || std::abs(std::abs(from) - last) > 1e-6 * std::abs(from)) + ++arc; + last = std::abs(from); + roles[i] = {1, arc}; + arcs[element.vertex] = std::max(arcs[element.vertex], arc + 1); + } + else + roles[i] = std::abs(from) <= 1e-9 ? role{2, 0} : std::abs(to) <= 1e-9 ? role{3, 0} : role{4, arc + 1}; + } + } + // the element under the mouse and what of it a drag changes: near the ends of a plateau the transitions either side of it, also the + // ones of no length yet int under{-1}; int part{0}; + int arc{0}; if (hovered && false == dragging) { for (int i = 0; i < count; ++i) @@ -1268,24 +1794,34 @@ bool editor_mode::render_route_band() if (mouse.x < x_of(element.chainage) || mouse.x >= x_of(element.chainage + element.length)) continue; under = i; - auto const from{x_of(element.chainage)}; - auto const to{x_of(element.chainage + element.length)}; - auto const edge{std::min(std::round(font * 0.5f), (to - from) * 0.25f)}; - switch (element.kind) + part = roles[i].part; + arc = roles[i].arc; + if (part == 1) { - case alignment::element_kind::arc: part = mouse.x < from + edge ? 2 : mouse.x >= to - edge ? 3 : 1; break; - case alignment::element_kind::transition_in: part = 2; break; - case alignment::element_kind::transition_out: part = 3; break; - default: break; + auto const from{x_of(element.chainage)}; + auto const to{x_of(element.chainage + element.length)}; + auto const edge{std::min(std::round(font * 0.5f), (to - from) * 0.25f)}; + if (mouse.x < from + edge) + part = arc == 0 ? 2 : 4; + else if (mouse.x >= to - edge) + { + part = arc + 1 >= arcs[element.vertex] ? 3 : 4; + arc += 1; + } } - if (element.vertex < 0 || element.vertex >= static_cast(design.vertices.size())) - part = 0; break; } } - auto const value_of = [&](int const Vertex, int const Part) -> double & { + auto const value_of = [&](int const Vertex, int const Part, int const Arc) -> double & { auto &vertex{design.vertices[Vertex]}; - return Part == 1 ? vertex.radius : Part == 2 ? vertex.transition_in : vertex.transition_out; + auto const later{Arc >= 1 && Arc <= static_cast(vertex.arcs.size())}; + switch (Part) + { + case 1: return later ? vertex.arcs[Arc - 1].radius : vertex.radius; + case 2: return vertex.transition_in; + case 3: return vertex.transition_out; + default: return later ? vertex.arcs[Arc - 1].transition : vertex.transition_in; + } }; auto const rounded_radius = [](double const Radius) { auto const step{Radius < 1000.0 ? 1.0 : 10.0}; @@ -1296,14 +1832,15 @@ bool editor_mode::render_route_band() { band.vertex = result.elements[under].vertex; band.part = part; - band.value = value_of(band.vertex, part); + band.arc = arc; + band.value = value_of(band.vertex, part, arc); band.press = mouse; band.metres = metres; band.curvature = curvature; } if (band.vertex >= 0 && band.vertex < static_cast(design.vertices.size()) && active) { - auto &value{value_of(band.vertex, band.part)}; + auto &value{value_of(band.vertex, band.part, band.arc)}; double wanted{value}; if (band.part == 1) { @@ -1317,6 +1854,7 @@ bool editor_mode::render_route_band() } else { + // the transition in grows to the left, from where it starts; the others to the right auto const shift{(mouse.x - band.press.x) / std::max(1e-6f, metres)}; wanted = std::max(0.0, std::round(band.value + (band.part == 2 ? -shift : shift))); } @@ -1336,7 +1874,7 @@ bool editor_mode::render_route_band() ImGui::SetMouseCursor(part == 1 ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); if (auto const wheel{ImGui::GetIO().MouseWheel}; wheel != 0.0f) { - auto &value{value_of(result.elements[under].vertex, part)}; + auto &value{value_of(result.elements[under].vertex, part, arc)}; value = part == 1 ? rounded_radius(value * (wheel > 0.0f ? 0.95 : 1.0 / 0.95)) : std::max(0.0, std::round(value / 5.0) * 5.0 + (wheel > 0.0f ? 5.0 : -5.0)); changed = true; } @@ -1423,10 +1961,10 @@ bool editor_mode::render_route_band() if (false == dragging) list->AddLine(ImVec2(mouse.x, plot_top), ImVec2(mouse.x, plot_bottom), IM_COL32(255, 255, 255, 60)); } - if (dragging && band.vertex < static_cast(design.vertices.size())) + if (dragging && band.vertex >= 0 && band.vertex < static_cast(design.vertices.size())) { - auto const &vertex{design.vertices[band.vertex]}; - auto const text{band.part == 1 ? format("R %.0f m", vertex.radius) : format("%.0f m", band.part == 2 ? vertex.transition_in : vertex.transition_out)}; + auto const value{value_of(band.vertex, band.part, band.arc)}; + auto const text{band.part == 1 ? format("R %.0f m", value) : format("%.0f m", value)}; auto const size{ImGui::CalcTextSize(text.c_str())}; ImVec2 const at{std::clamp(mouse.x + pad, top.x, bottom.x - size.x - pad), std::clamp(mouse.y - size.y - pad, top.y, bottom.y - size.y)}; list->AddRectFilled(ImVec2(at.x - 3.0f, at.y - 1.0f), ImVec2(at.x + size.x + 3.0f, at.y + size.y + 1.0f), IM_COL32(0, 0, 0, 170), 3.0f); @@ -1438,12 +1976,18 @@ bool editor_mode::render_route_band() if (part != 0 && false == dragging && ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) { - auto const &vertex{design.vertices[result.elements[under].vertex]}; + auto const index{result.elements[under].vertex}; + auto const &vertex{design.vertices[index]}; + auto const value{value_of(index, part, arc)}; ImGui::BeginTooltip(); - if (part == 1) - ImGui::Text(STR_C("Curve %d: R %.0f m, cant %.0f mm"), result.elements[under].vertex + 1, vertex.radius, vertex.cant); + if (part == 1 && arcs[index] > 1) + ImGui::Text(STR_C("Curve %d, arc R%d: %.0f m"), index + 1, arc + 1, value); + else if (part == 1) + ImGui::Text(STR_C("Curve %d: R %.0f m, cant %.0f mm"), index + 1, value, vertex.cant); + else if (part == 4) + ImGui::Text(STR_C("Curve %d, transition between R%d and R%d: %.0f m"), index + 1, arc, arc + 1, value); else - ImGui::Text(part == 2 ? STR_C("Transition into curve %d: %.0f m") : STR_C("Transition out of curve %d: %.0f m"), result.elements[under].vertex + 1, part == 2 ? vertex.transition_in : vertex.transition_out); + ImGui::Text(part == 2 ? STR_C("Transition into curve %d: %.0f m") : STR_C("Transition out of curve %d: %.0f m"), index + 1, value); gesture_rows({{"Drag", part == 1 ? "up or down: the radius" : "sideways: the length"}, {"Wheel", part == 1 ? "the radius by 5%" : "the length by 5 m"}}); ImGui::EndTooltip(); } @@ -1500,6 +2044,20 @@ bool editor_mode::render_route_vertices() { bool changed{false}; auto const count{static_cast(m_route.design.vertices.size())}; + m_route.focus = {}; + route_measure_clicks(); + if (false == m_route.clicks.empty()) + { + ImGui::Checkbox(STR_C("Keep to the points clicked"), &m_route.hold); + item_tooltip("The scenery keeps the points clicked when the line was laid: a curve whose transitions or cants are changed gets its radii fitted to them again, so it stays where it was seen on the photo"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Fit all to them"))) + { + for (int i = 0; i < count; ++i) + changed |= route_hold_clicks(i); + } + item_tooltip("The radii of every curve fitted again to the points clicked along it, the transitions and the cants as they are"); + } for (int i = 0; i < count; ++i) { ImGui::PushID(i); @@ -1514,15 +2072,81 @@ bool editor_mode::render_route_vertex(int const Index) auto &route{m_route}; auto &design{route.design}; auto &vertex{design.vertices[Index]}; - auto const count{static_cast(design.vertices.size())}; bool changed{false}; - if (count > 1) - ImGui::SeparatorText(format(STR_C("Curve %d of the S-curve"), Index + 1).c_str()); + // a transition or a cant changed, the edit done: the curve fitted to the points clicked again, its radii given by them + bool settled{false}; + using part = route_design::curve_focus::part; + // the field just drawn shows its part of the curve in the view while the mouse is over it or it's being edited + auto const mark = [&](part const What, int const Arc) { + if (ImGui::IsItemActive()) + route.focus = {Index, What, Arc, true}; + else if (false == route.focus.held && ImGui::IsItemHovered()) + route.focus = {Index, What, Arc, false}; + }; + ImGui::BeginGroup(); + // named as the vertex is in the view, W1, W2..., with where it lies along the line + { + auto from{-1.0}; + auto to{-1.0}; + for (auto const &e : route.result.elements) + if (e.vertex == Index) + { + from = from < 0.0 ? e.chainage : from; + to = e.chainage + e.length; + } + auto title{format(STR_C("Curve W%d"), Index + 1)}; + if (from >= 0.0) + title += format(" km %s - %s", profile::format_chainage(route.origin + from).c_str(), profile::format_chainage(route.origin + to).c_str()); + // the vertex picked in the view stands out + auto const picked{route.vertex == Index || route.grip == Index}; + if (picked) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(overlay_color::highlight)); + ImGui::SeparatorText(title.c_str()); + if (picked) + ImGui::PopStyleColor(); + mark(part::whole, 0); + if (from >= 0.0) + { + if (ImGui::SmallButton(STR_C("Show in the view"))) + route_show_curve(from, to); + item_tooltip("The camera flies over the curve, which is lit up in the view while the mouse is over its fields"); + mark(part::whole, 0); + } + // how far the curve is from the points clicked along it when it was laid + std::size_t clicked{0}; + double sum{0.0}; + double worst{0.0}; + for (auto const &state : route.click_states) + if (state.vertex == Index && false == state.corner && std::abs(state.offset) <= 3.0) + { + ++clicked; + sum += state.offset * state.offset; + worst = std::max(worst, std::abs(state.offset)); + } + if (clicked > 0) + { + ImGui::SameLine(); + auto const colour{worst <= 0.1 ? ImVec4(0.35f, 0.86f, 0.43f, 1.0f) : worst <= 0.3 ? ImVec4(1.0f, 0.78f, 0.24f, 1.0f) : ImVec4(1.0f, 0.35f, 0.27f, 1.0f)}; + ImGui::TextColored(colour, STR_C("points clicked %zu: %.0f cm on average, at most %.0f cm"), clicked, std::sqrt(sum / static_cast(clicked)) * 100.0, worst * 100.0); + item_tooltip("The points clicked along the curve when it was laid, kept in the scenery, and how far the curve is from them now"); + mark(part::whole, 0); + if (clicked >= 3) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Fit to them"))) + changed |= route_hold_clicks(Index); + item_tooltip("The radii of the curve and their shares fitted again to the points clicked, the transitions and the cants as they are"); + } + } + } // two fields in a row, the name and the unit inside them, a bar in the colour of the element in the band and in the view auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; - auto const field = [&](char const *Id, double &Value, float const Speed, char const *Format, ImU32 const Colour, char const *Tooltip) { + auto const field = [&](char const *Id, double &Value, float const Speed, char const *Format, ImU32 const Colour, char const *Tooltip, part const What) { ImGui::SetNextItemWidth(half); auto const edited{ImGui::DragScalar(Id, ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format)}; + // the radius is the user's own, the curve no longer fitted after it + settled |= What != part::arc && ImGui::IsItemDeactivatedAfterEdit(); + mark(What, 0); auto const low{ImGui::GetItemRectMin()}; auto const high{ImGui::GetItemRectMax()}; ImGui::GetWindowDrawList()->AddRectFilled(low, ImVec2(low.x + 3.0f, high.y), Colour, 2.0f, ImDrawFlags_RoundCornersLeft); @@ -1530,29 +2154,77 @@ bool editor_mode::render_route_vertex(int const Index) return edited; }; ImU32 const transition{IM_COL32(255, 140, 30, 255)}; - changed |= field("##radius", vertex.radius, 5.0f, vertex.compound ? STR_C("R1 %.0f m") : STR_C("R %.0f m"), overlay_color::grip, "Radius of the arc: drag sideways, or a double click to type it"); + // the first curve, or the last one, may go on from the curve of the track adjoining at that end: it begins right there, its radius + // the one which begins it there, its entry, or exit, the transition between the two curves + auto const count{static_cast(design.vertices.size())}; + for (auto const atend : {false, true}) + { + auto const radius{atend ? design.end_radius : design.start_radius}; + if ((atend ? Index + 1 != count : Index != 0) || radius <= 0.0 || radius >= 10000.0 || (atend && count == 1 && design.start_curved)) + continue; + auto &curved{atend ? design.end_curved : design.start_curved}; + changed |= ImGui::Checkbox((format(atend ? STR_C("Goes into the curve of the track at the end (R %.0f m)") : STR_C("Goes on from the curve of the track at the start (R %.0f m)"), radius) + (atend ? "##curvedend" : "##curvedstart")).c_str(), &curved); + item_tooltip("The curve begins right at the end of the fragment, at the curvature of the track adjoining there, where it turns the same way; its radius is the one " + "which begins it there, the transition at that end goes between the two curves"); + } + changed |= field("##radius", vertex.radius, 5.0f, vertex.compound ? STR_C("R1 %.0f m") : STR_C("R %.0f m"), overlay_color::grip, "Radius of the arc: drag sideways, or a double click to type it", part::arc); ImGui::SameLine(); - changed |= field("##cant", vertex.cant, 1.0f, STR_C("cant %.0f mm"), IM_COL32(240, 170, 60, 255), "Height of the outer rail above the inner one"); - changed |= field("##entry", vertex.transition_in, 1.0f, STR_C("entry %.0f m"), transition, "Length of the transition curve leading into the arc"); + changed |= field("##cant", vertex.cant, 1.0f, STR_C("cant %.0f mm"), IM_COL32(240, 170, 60, 255), + vertex.compound ? "Height of the outer rail above the inner one on the sharpest arc; the arcs with no cant of their own get it in proportion to their curvature" + : "Height of the outer rail above the inner one", + part::whole); + changed |= field("##entry", vertex.transition_in, 1.0f, STR_C("entry %.0f m"), transition, "Length of the transition curve leading into the arc", part::entry); ImGui::SameLine(); - changed |= field("##exit", vertex.transition_out, 1.0f, STR_C("exit %.0f m"), transition, "Length of the transition curve leading out of the arc"); + changed |= field("##exit", vertex.transition_out, 1.0f, STR_C("exit %.0f m"), transition, "Length of the transition curve leading out of the arc", part::exit); ImGui::PushItemWidth(110.0f); - if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound)) + if (ImGui::Checkbox(STR_C("Two transition curves, no arc"), &vertex.spirals)) + { + changed = true; + if (vertex.spirals) + vertex.compound = false; + } + item_tooltip("The bend eased the way the lines are drawn: two transition curves meeting at the radius, with no arc between them and no cant; their lengths follow from the radius and the angle"); + if (false == vertex.spirals && ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound)) { changed = true; if (vertex.compound && vertex.arcs.empty()) vertex.arcs.push_back({std::max(10.0, std::round(vertex.radius * 0.6 / 10.0) * 10.0), 0.0, 1.0}); } - item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions"); + if (false == vertex.spirals) + item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions"); if (vertex.compound) { auto total{std::max(0.0, vertex.share)}; for (auto const &arc : vertex.arcs) total += std::max(0.0, arc.share); auto const percent = [&](double const Share) { return total > 0.0 ? 100.0 * std::max(0.0, Share) / total : 0.0; }; + auto const cants{alignment::compound_cants(vertex)}; + // the cant of one arc: the one in proportion to the curvature until it's given its own + auto const arc_cant = [&](int const Arc, double &Cant) { + auto value{static_cast(Arc) < cants.size() ? cants[Arc] : std::max(0.0, Cant)}; + auto edited{ImGui::DragScalar(format(STR_C("Cant of R%d (mm)"), Arc + 1).c_str(), ImGuiDataType_Double, &value, 1.0f, nullptr, nullptr, "%.0f")}; + settled |= ImGui::IsItemDeactivatedAfterEdit(); + mark(part::arc, Arc); + if (edited) + Cant = std::max(0.0, value); + item_tooltip("The cant of this arc, ramped along the transition curves to the arcs either side of it; until it's set here, the cant of the curve in proportion to the curvature of the arc"); + ImGui::SameLine(); + if (Cant < 0.0) + ImGui::TextDisabled("%s", STR_C("auto")); + else if (ImGui::SmallButton(STR_C("auto"))) + { + Cant = -1.0; + edited = true; + settled = true; + } + item_tooltip("The cant of the arc in proportion to its curvature again"); + return edited; + }; changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), 1).c_str(), ImGuiDataType_Double, &vertex.share, 0.01f, nullptr, nullptr, "%.2f"); + mark(part::arc, 0); ImGui::SameLine(); ImGui::TextDisabled("%.0f%%", percent(vertex.share)); + changed |= arc_cant(0, vertex.first_cant); int removed{-1}; for (int k = 0; k < static_cast(vertex.arcs.size()); ++k) { @@ -1560,14 +2232,20 @@ bool editor_mode::render_route_vertex(int const Index) ImGui::PushID(k); ImGui::Separator(); changed |= ImGui::DragScalar(format(STR_C("Radius R%d (m)"), k + 2).c_str(), ImGuiDataType_Double, &arc.radius, 5.0f, nullptr, nullptr, "%.0f"); + mark(part::arc, k + 1); ImGui::SameLine(); if (ImGui::SmallButton("x")) removed = k; item_tooltip("Takes this radius out of the curve"); + mark(part::arc, k + 1); changed |= ImGui::DragScalar(format(STR_C("Transition R%d-R%d (m)"), k + 1, k + 2).c_str(), ImGuiDataType_Double, &arc.transition, 1.0f, nullptr, nullptr, "%.0f"); + settled |= ImGui::IsItemDeactivatedAfterEdit(); + mark(part::between, k + 1); changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f"); + mark(part::arc, k + 1); ImGui::SameLine(); ImGui::TextDisabled("%.0f%%", percent(arc.share)); + changed |= arc_cant(k + 1, arc.cant); arc.radius = std::max(1.0, arc.radius); arc.transition = std::max(0.0, arc.transition); arc.share = std::max(0.0, arc.share); @@ -1595,19 +2273,74 @@ bool editor_mode::render_route_vertex(int const Index) vertex.transition_in = std::max(0.0, vertex.transition_in); vertex.transition_out = std::max(0.0, vertex.transition_out); vertex.cant = std::max(0.0, vertex.cant); - if (false == m_norms_free) { - if (ImGui::SmallButton(STR_C("Apply recommended for V"))) - { - route_recommend(vertex); + // the transitions follow the radii, which follow the points clicked: a few rounds settle them + auto const reshape = [&](auto const &Shape) { + for (int pass = 0; pass < 3; ++pass) + { + Shape(); + if (false == route.hold || false == route_hold_clicks(Index)) + break; + } changed = true; - } + }; + if (ImGui::SmallButton(STR_C("Apply recommended for V"))) + reshape([&] { route_recommend(vertex); }); if (ImGui::IsItemHovered()) { auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; ImGui::SetTooltip("%s\n%s", STR_C("Cant and transitions recommended for the design speed and the radius"), format(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition).c_str()); } + if (false == vertex.spirals) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Transitions for these cants"))) + reshape([&] { alignment::norm_transitions(vertex, design.speed, design.norms); }); + item_tooltip("The transition curves as long as the norms ask for at these radii and cants, the cants staying: a cant lowered asks for longer ones"); + mark(part::whole, 0); + } + } + // the cant ramps at the ends of the curve: along its transitions, or where they're put, off a switch laid on a transition say + if (false == vertex.spirals && ImGui::TreeNode(STR_C("Cant ramps"))) + { + std::vector ramps; + for (auto const &ramp : route.result.ramps) + if (ramp.vertex == Index) + ramps.push_back(ramp); + auto const ramp = [&](char const *Id, char const *Label, double &Offset, double &Length, std::size_t const Which, part const What) { + ImGui::PushID(Id); + auto along{Length < 0.0}; + if (ImGui::Checkbox(Label, &along)) + { + // let go of the transition where it lay along it, to be moved from there + Length = along ? -1.0 : Which < ramps.size() ? ramps[Which].to - ramps[Which].from : 0.0; + Offset = 0.0; + changed = true; + } + mark(What, 0); + item_tooltip("The norms recommend the cant ramp along the transition curve; off it, it may lie on the straight before the curve or on the arc, a switch on the transition keeping the same cant all along"); + if (false == along) + { + ImGui::SetNextItemWidth(half); + changed |= ImGui::DragScalar("##offset", ImGuiDataType_Double, &Offset, 0.5f, nullptr, nullptr, STR_C("into the curve %.1f m")); + mark(What, 0); + item_tooltip("How far the outer end of the ramp lies into the curve from its end, below 0 out along the straight before it"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(half); + changed |= ImGui::DragScalar("##length", ImGuiDataType_Double, &Length, 0.5f, nullptr, nullptr, STR_C("length %.1f m")); + mark(What, 0); + item_tooltip("The length of the ramp, the cant rising evenly along it"); + Length = std::max(0.0, Length); + } + if (Which < ramps.size() && ramps[Which].to > ramps[Which].from) + ImGui::TextDisabled(STR_C("km %s - %s, %.2f mm/m"), profile::format_chainage(route.origin + ramps[Which].from).c_str(), profile::format_chainage(route.origin + ramps[Which].to).c_str(), + ramps[Which].cant / (ramps[Which].to - ramps[Which].from)); + ImGui::PopID(); + }; + ramp("in", STR_C("Ramp in along the transition curve"), vertex.ramp_in_offset, vertex.ramp_in_length, 0, part::entry); + ramp("out", STR_C("Ramp out along the transition curve"), vertex.ramp_out_offset, vertex.ramp_out_length, 1, part::exit); + ImGui::TreePop(); } if (ImGui::TreeNode(STR_C("Details"))) { @@ -1622,6 +2355,11 @@ bool editor_mode::render_route_vertex(int const Index) item_tooltip("For the loops: the curve turns by more than a half circle"); ImGui::TreePop(); } + ImGui::EndGroup(); + if (route.focus.vertex != Index && false == route.focus.held && ImGui::IsItemHovered()) + route.focus = {Index, part::whole, 0, false}; + if (settled && route.hold) + changed |= route_hold_clicks(Index); return changed; } @@ -1657,14 +2395,11 @@ void editor_mode::render_route_result() if (result.valid) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) + if (main_button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) { route_apply(); first_step_done(first_step::plan_apply); } - ImGui::PopStyleColor(2); } else { diff --git a/application/editormodeselection.cpp b/application/editormodeselection.cpp index 732bd721..42d3d5d5 100644 --- a/application/editormodeselection.cpp +++ b/application/editormodeselection.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" @@ -271,7 +272,7 @@ void editor_mode::group_selection() { if (reason.empty()) reason = STR("it's a part of a scenery template"); - ui()->set_status(format(STR_C("\"%s\" can't be grouped: %s"), node->name().empty() ? STR_C("(no name)") : node->name().c_str(), reason.c_str()), true); + ui()->set_status(format(STR_C("\"%s\" can't be grouped: %s"), name_of(*node).c_str(), reason.c_str()), true); return; } } @@ -334,7 +335,7 @@ void editor_mode::render_context_menu() if (count > 1) ImGui::TextDisabled(STR_C("%zu models selected"), count); else if (selected) - ImGui::TextDisabled("%s", m_node->name().empty() ? STR_C("(no name)") : m_node->name().c_str()); + ImGui::TextDisabled("%s", name_of(*m_node).c_str()); else ImGui::TextDisabled("%s", STR_C("Nothing selected")); ImGui::Separator(); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index e163b632..fc96c829 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -14,6 +14,8 @@ http://mozilla.org/MPL/2.0/. #include "application/application.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" +#include "application/editorgestures.h" +#include "imgui/imgui.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" @@ -54,6 +56,9 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sleepers, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &editor_mode::selftest_props, &editor_mode::selftest_monkey}) if (auto const result{(this->*group)(command, words)}; result != selftest_result::unhandled) return result; + for (auto *tool : m_tools) + if (auto const result{tool->selftest(command, words)}; result != selftest_result::unhandled) + return result; return selftest_result::unhandled; } @@ -62,12 +67,15 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // camera from the first point, looking at the second // select // selectat the path nearest the point, within 3 m, as a click on it +// trackends logs the ends of the first path of the track against the ground under them +// kbreak K on the track nearest the point: split there, a break of the grade at the height of the ground // blade [name] logs the blades of each switch cut from the rails: tip, 50 mm head, full head, heel, drop of the head, open flangeway, the gauge lines apart against x^2/2R, vertices in both positions // frog [name] logs where the inner rails of each switch cross: distance on both paths, 1:n, throat, nose, check rails // guards [name] logs each guard rail and what each of its ends and breaks is cut along; a fault when a cut is not along a rail or a glued end not on the guard rail of the other track // railtime lays out the rails of every track, as when its geometry is made, and logs how long it took // throw <0|1> sets every switch to the path and moves its blades there at once // crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose +// switchtimbers the model of the timbers set on all the switches and crossings, as the buttons of the switch tool do it // frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings // geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too // sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, @@ -121,9 +129,28 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // terraincursor logs the point of the ground under the cursor, found along the ray (over holes too) // terraintool the tool the left button works on the terrain, with a brush of 3 m // 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 +// layspeed the speed set for the laying, and whether it goes also on from a track with its own +// laymode , layradius , laytrace as the panel of the laying sets them +// endcurves logs every free end of the rails: where, the way out and the curvature its track goes on with +// curvejoint logs the paths meeting at the end nearest the point, the curvature of each and its jump from the ones before to the ones after +// laywalk ... a line of known curves on from the free end nearest the point, +// the points along it clicked in the mode of a curve, from the end or towards it (see the command) +// laybends ... the bends of straights clicked from the free end nearest the point +// layp ... the points clicked in the mode set, on the ground, a free end within 3 m reached +// layreport