mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 05:09:20 +02:00
feat(editor): track editor driven by what the cursor is over: the outline and a label show what a click or a drag does, snapping to free ends on the screen, handles and ends dragged directly, no rotation gizmo; laying from a free end the same in every mode, straight with Shift, the curve of a switch going on past its diverging end; height 0 where there's no terrain; placed switch leaves the switch mode, double slips listed apart, a drive of the switch placed with it by the //$e switch description of its template; compound curves of any number of radii; dragged end of a straight fits the curve beside it; name of a path, followed by the memory cells and the includes taking it; turntable placed from a template described as one, tracks led out of it and the tracks around fitted to it, its positions written to the include; infrastructure bound to the path whose events read its memory cell; terrain made where the route has none and shaped to the formation under the ballast; models along the track over the selected path and on along the joined ones while dragged, rarely used settings folded; vehicles and trainsets placed on any path, switches included, facing the way the arrow shows; joined paths spread into the selection; New scenery in the File menu starts the editor again with the wizard; ortho zoom towards the cursor; scripted self tests; Polish translation
This commit is contained in:
@@ -10,6 +10,12 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "stdafx.h"
|
||||
#include "application/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#ifndef _WIN32
|
||||
#include <spawn.h>
|
||||
#include <unistd.h>
|
||||
extern char **environ;
|
||||
#endif
|
||||
#include "application/editorprojection.h"
|
||||
|
||||
#include "application/application.h"
|
||||
#include "editor/editorSettings.hpp"
|
||||
@@ -477,10 +483,12 @@ editor_mode::editor_mode() {
|
||||
ui()->set_fill_options([this]() { render_area_fill(); });
|
||||
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
|
||||
ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); });
|
||||
ui()->set_new_scenery([this]() { m_newscenery_asked = true; });
|
||||
ui()->set_menu_options([this]() {
|
||||
render_object_menu();
|
||||
render_track_menu();
|
||||
render_road_menu();
|
||||
render_map_menu();
|
||||
});
|
||||
m_roadtool.settings.normalize();
|
||||
road_select(nullptr);
|
||||
@@ -893,6 +901,20 @@ void editor_mode::undo_last()
|
||||
EditorSnapshot snap = m_history.back();
|
||||
m_history.pop_back();
|
||||
|
||||
if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty())
|
||||
{
|
||||
restore_sweeps(snap);
|
||||
g_redo.push_back(std::move(snap));
|
||||
return;
|
||||
}
|
||||
|
||||
if (false == snap.instances.empty() || false == snap.directives.empty())
|
||||
{
|
||||
restore_includes(snap);
|
||||
g_redo.push_back(std::move(snap));
|
||||
return;
|
||||
}
|
||||
|
||||
if (snap.instance != 0)
|
||||
{
|
||||
restore_include(snap);
|
||||
@@ -997,6 +1019,20 @@ void editor_mode::redo_last()
|
||||
EditorSnapshot snap = g_redo.back();
|
||||
g_redo.pop_back();
|
||||
|
||||
if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty())
|
||||
{
|
||||
restore_sweeps(snap);
|
||||
m_history.push_back(std::move(snap));
|
||||
return;
|
||||
}
|
||||
|
||||
if (false == snap.instances.empty() || false == snap.directives.empty())
|
||||
{
|
||||
restore_includes(snap);
|
||||
m_history.push_back(std::move(snap));
|
||||
return;
|
||||
}
|
||||
|
||||
if (snap.instance != 0)
|
||||
{
|
||||
restore_include(snap);
|
||||
@@ -1100,6 +1136,14 @@ void editor_mode::redo_last()
|
||||
|
||||
bool editor_mode::update()
|
||||
{
|
||||
selftest_step();
|
||||
if (m_vehicle.leave)
|
||||
{
|
||||
m_vehicle.leave = false;
|
||||
Application.pop_mode();
|
||||
return true;
|
||||
}
|
||||
|
||||
Timer::UpdateTimers(true);
|
||||
|
||||
simulation::State.update_clocks();
|
||||
@@ -1256,13 +1300,36 @@ bool editor_mode::update()
|
||||
draw_straights_overlay();
|
||||
if (ui()->mode() == nodebank_panel::TRACK)
|
||||
{
|
||||
if (m_track_box.active && false == ImGui::GetIO().MouseDown[0])
|
||||
finish_track_box();
|
||||
update_track_hover();
|
||||
point_drag_update();
|
||||
if (m_track_tab == track_tab::turntable)
|
||||
draw_turntable_overlay();
|
||||
if (m_track_tab == track_tab::lineside)
|
||||
{
|
||||
draw_parallel_preview();
|
||||
draw_hekto_overlay();
|
||||
draw_fouling_overlay();
|
||||
draw_vehicle_marker();
|
||||
sweep_drag();
|
||||
draw_sweep_overlay();
|
||||
}
|
||||
render_track_context();
|
||||
update_build_tools();
|
||||
update_track_intent();
|
||||
draw_track_hover();
|
||||
draw_track_set();
|
||||
draw_track_spread();
|
||||
draw_build_overlay();
|
||||
draw_track_intent();
|
||||
speed_step();
|
||||
joints_step();
|
||||
draw_track_hints();
|
||||
draw_profile_overlay();
|
||||
draw_infra_overlay();
|
||||
draw_speed_overlay();
|
||||
draw_joints_overlay();
|
||||
}
|
||||
update_gauge();
|
||||
render_gauge_window();
|
||||
@@ -1276,6 +1343,9 @@ bool editor_mode::update()
|
||||
if (ui()->mode() == nodebank_panel::FILL)
|
||||
draw_area_fill_outline();
|
||||
|
||||
render_orthophoto_window();
|
||||
render_new_scenery_popup();
|
||||
|
||||
// --- ImGui: Editor Settings & History windows ---
|
||||
if(m_settings_open)
|
||||
render_settings();
|
||||
@@ -1318,11 +1388,6 @@ void editor_mode::render_settings()
|
||||
render_terrain_ui();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Orthophoto"))
|
||||
{
|
||||
render_orthophoto_ui();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
@@ -1517,7 +1582,13 @@ void editor_mode::load_orthophoto_settings()
|
||||
config.in_scene = stored.in_scene;
|
||||
config.north = 0.0;
|
||||
config.east = 0.0;
|
||||
EditorSettings.orthophoto_origin(scenery, config.north, config.east);
|
||||
m_georeference_read = false;
|
||||
read_georeference();
|
||||
if (false == std::as_const(EditorSettings).orthophoto_origin(scenery, config.north, config.east) && m_georeference)
|
||||
{
|
||||
config.north = m_georeference_origin.x;
|
||||
config.east = m_georeference_origin.y;
|
||||
}
|
||||
m_orthophoto.settings(config);
|
||||
m_orthophoto_origin_edit = {config.north, config.east};
|
||||
}
|
||||
@@ -1567,6 +1638,21 @@ void editor_mode::render_orthophoto_ui()
|
||||
config.east = m_orthophoto_origin_edit.y;
|
||||
changed = persist = true;
|
||||
}
|
||||
if (m_georeference)
|
||||
{
|
||||
ImGui::TextDisabled("%s", m_georeference_line.c_str());
|
||||
ImGui::TextDisabled(STR_C("in the scenery file: Y (easting) %.0f m, X (northing) %.0f m"), m_georeference_origin.y, m_georeference_origin.x);
|
||||
if (config.north != m_georeference_origin.x || config.east != m_georeference_origin.y)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("Use it")))
|
||||
{
|
||||
config.north = m_orthophoto_origin_edit.x = m_georeference_origin.x;
|
||||
config.east = m_orthophoto_origin_edit.y = m_georeference_origin.y;
|
||||
changed = persist = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.north == 0.0 && config.east == 0.0)
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), "Enter the origin of this scenery first");
|
||||
else
|
||||
@@ -2103,13 +2189,13 @@ void editor_mode::commit_terrain()
|
||||
m_streamer.flush(); // save resident edited chunks to disk
|
||||
}
|
||||
|
||||
void editor_mode::save()
|
||||
bool editor_mode::save()
|
||||
{
|
||||
if (scene::Layers.empty())
|
||||
{
|
||||
// without the sources tracked during the load there's no telling where the changes belong
|
||||
ui()->set_status("The scenery wasn't opened for editing. Start the simulator with: -edit <scenery file>", true);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// terrain made in the editor is kept in files of its own, the scenery only needs the directive which loads it
|
||||
@@ -2137,6 +2223,7 @@ void editor_mode::save()
|
||||
}
|
||||
ui()->set_status(result.message, false == result.success);
|
||||
WriteLog("Editor: " + result.message, logtype::generic);
|
||||
return result.success;
|
||||
}
|
||||
|
||||
void editor_mode::toggle_include(scene::instance_handle const Instance)
|
||||
@@ -2175,6 +2262,33 @@ void editor_mode::restore_include(EditorSnapshot &Snapshot)
|
||||
select_include(scene::Layers.removable(Snapshot.instance) ? Snapshot.instance : 0);
|
||||
}
|
||||
|
||||
void editor_mode::restore_includes(EditorSnapshot &Snapshot)
|
||||
{
|
||||
std::size_t gone{0};
|
||||
for (auto const instance : Snapshot.instances)
|
||||
{
|
||||
if (false == scene::Layers.tracked(instance) || scene::Layers.instance(instance).dead)
|
||||
{
|
||||
++gone;
|
||||
continue;
|
||||
}
|
||||
scene::Layers.removed(instance, false == scene::Layers.instance(instance).removed);
|
||||
}
|
||||
for (auto &entry : Snapshot.directives)
|
||||
{
|
||||
auto current = scene::Layers.directive(entry.first);
|
||||
if (current.empty() || scene::Layers.instance(entry.first).dead)
|
||||
{
|
||||
++gone;
|
||||
continue;
|
||||
}
|
||||
set_include_directive(entry.first, entry.second);
|
||||
entry.second = std::move(current);
|
||||
}
|
||||
select_include(0);
|
||||
ui()->set_status(gone > 0 ? std::to_string(gone) + " of the includes are gone from the saved scenery, they can't be taken back." : std::to_string(Snapshot.instances.size() + Snapshot.directives.size()) + " includes restored.", gone > 0);
|
||||
}
|
||||
|
||||
void editor_mode::select_include(scene::instance_handle const Instance)
|
||||
{
|
||||
m_instance = (scene::Layers.tracked(Instance) ? Instance : 0);
|
||||
@@ -2541,6 +2655,146 @@ void editor_mode::drop_model_instances()
|
||||
ui()->set_status(status);
|
||||
}
|
||||
|
||||
void editor_mode::read_georeference()
|
||||
{
|
||||
if (m_georeference_read)
|
||||
return;
|
||||
m_georeference_read = true;
|
||||
m_georeference = false;
|
||||
m_georeference_line.clear();
|
||||
// the name of the scenery is kept in lower case, the file may have capitals in its name
|
||||
std::filesystem::path file{Global.asCurrentSceneryPath + Global.SceneryFile};
|
||||
std::error_code error;
|
||||
if (false == std::filesystem::exists(file, error))
|
||||
{
|
||||
for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error))
|
||||
{
|
||||
auto name{entry.path().filename().string()};
|
||||
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
|
||||
if (name == Global.SceneryFile)
|
||||
{
|
||||
file = entry.path();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::ifstream stream(file);
|
||||
std::string line;
|
||||
for (int count = 0; count < 200 && std::getline(stream, line); ++count)
|
||||
{
|
||||
auto const mark = line.find("//$g");
|
||||
if (mark == std::string::npos)
|
||||
continue;
|
||||
// Rainsted writes the origin of the scenery as: //$g <system> <easting> <northing>, in kilometres
|
||||
std::istringstream words(line.substr(mark + 4));
|
||||
std::string system;
|
||||
double east{0.0}, north{0.0};
|
||||
if (false == static_cast<bool>(words >> system >> east >> north) || system.find("1992") == std::string::npos)
|
||||
continue;
|
||||
auto const scale = (east < 10000.0 && north < 10000.0) ? 1000.0 : 1.0;
|
||||
m_georeference_origin = {north * scale, east * scale};
|
||||
m_georeference_line = line.substr(mark);
|
||||
if (false == m_georeference_line.empty() && m_georeference_line.back() == '\r')
|
||||
m_georeference_line.pop_back();
|
||||
m_georeference = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::render_map_menu()
|
||||
{
|
||||
if (false == ImGui::BeginMenu(STR_C("Map")))
|
||||
return;
|
||||
bool enabled = m_orthophoto.enabled();
|
||||
if (ImGui::MenuItem(STR_C("Orthophoto"), nullptr, enabled))
|
||||
m_orthophoto.enabled(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);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
void editor_mode::render_new_scenery_popup()
|
||||
{
|
||||
if (m_newscenery_asked)
|
||||
{
|
||||
ImGui::OpenPopup(STR_C("New scenery##restart"));
|
||||
m_newscenery_asked = false;
|
||||
}
|
||||
if (false == ImGui::BeginPopupModal(STR_C("New scenery##restart"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
|
||||
return;
|
||||
ImGui::TextUnformatted(STR_C("The editor starts again, with the wizard of a new scenery."));
|
||||
auto const editsession{false == scene::Layers.empty()};
|
||||
if (false == m_history.empty())
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "%s", STR_C("What isn't saved of this scenery is lost."));
|
||||
if (editsession && ImGui::Button(STR_C("Save and start the wizard")))
|
||||
{
|
||||
if (save() && false == restart_for_new_scenery())
|
||||
ui()->set_status(STR_C("The editor couldn't be started again"), true);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (editsession)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(STR_C("Start the wizard")))
|
||||
{
|
||||
if (false == restart_for_new_scenery())
|
||||
ui()->set_status(STR_C("The editor couldn't be started again"), true);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(STR_C("Cancel")))
|
||||
ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
bool editor_mode::restart_for_new_scenery()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
_putenv("EU07_EDITOR_SELFTEST=");
|
||||
wchar_t path[MAX_PATH];
|
||||
if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0)
|
||||
return false;
|
||||
std::wstring command{L"\"" + std::wstring{path} + L"\" -edit"};
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process{};
|
||||
if (false == CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, &process))
|
||||
return false;
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
#else
|
||||
unsetenv("EU07_EDITOR_SELFTEST");
|
||||
char path[4096];
|
||||
auto const size{readlink("/proc/self/exe", path, sizeof(path) - 1)};
|
||||
if (size <= 0)
|
||||
return false;
|
||||
path[size] = '\0';
|
||||
char edit[]{"-edit"};
|
||||
char *arguments[]{path, edit, nullptr};
|
||||
posix_spawnattr_t attributes;
|
||||
posix_spawnattr_init(&attributes);
|
||||
posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSID);
|
||||
pid_t child;
|
||||
auto const result{posix_spawn(&child, path, nullptr, &attributes, arguments, environ)};
|
||||
posix_spawnattr_destroy(&attributes);
|
||||
if (result != 0)
|
||||
return false;
|
||||
#endif
|
||||
WriteLog("Editor: started again for a new scenery");
|
||||
Application.queue_quit(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void editor_mode::render_orthophoto_window()
|
||||
{
|
||||
if (false == m_orthophoto_window)
|
||||
return;
|
||||
ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver);
|
||||
if (ImGui::Begin(STR_C("Orthophoto##window"), &m_orthophoto_window, ImGuiWindowFlags_AlwaysAutoResize))
|
||||
render_orthophoto_ui();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void editor_mode::render_object_menu()
|
||||
{
|
||||
if (ImGui::BeginMenu("Objects"))
|
||||
@@ -2899,6 +3153,12 @@ void editor_mode::render_gizmo()
|
||||
return;
|
||||
}
|
||||
|
||||
if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2)
|
||||
{
|
||||
render_track_set_gizmo();
|
||||
return;
|
||||
}
|
||||
|
||||
if (straights_active())
|
||||
{
|
||||
render_straight_gizmo();
|
||||
@@ -2920,7 +3180,7 @@ void editor_mode::render_gizmo()
|
||||
if (selected_track())
|
||||
{
|
||||
// in the track modes the whole path is moved only while selecting, or the switch in its mode
|
||||
if (ui()->mode() != nodebank_panel::TRACK || m_track_tab == track_tab::path || m_track_tab == track_tab::turnout)
|
||||
if (ui()->mode() != nodebank_panel::TRACK || (m_track_tab == track_tab::path && false == m_point_drag.active) || (m_track_tab == track_tab::turnout && selected_track()->eType == tt_Switch))
|
||||
render_track_gizmo();
|
||||
else
|
||||
m_track_gizmo_using = false;
|
||||
@@ -3153,7 +3413,16 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset)
|
||||
{
|
||||
if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse)
|
||||
return;
|
||||
Global.EditorOrthoExtent = std::clamp(Global.EditorOrthoExtent * static_cast<float>(std::pow(0.85, Yoffset)), 5.0f, 5000.0f);
|
||||
auto const before{Global.EditorOrthoExtent};
|
||||
auto const after{std::clamp(before * static_cast<float>(std::pow(0.85, Yoffset)), 5.0f, 5000.0f)};
|
||||
screen_projection const projection;
|
||||
glm::dvec3 origin, direction;
|
||||
projection.ray(ImGui::GetIO().MousePos, origin, direction);
|
||||
auto const scale{static_cast<double>(after) / static_cast<double>(before)};
|
||||
Camera.Pos.x = origin.x + (Camera.Pos.x - origin.x) * scale;
|
||||
Camera.Pos.z = origin.z + (Camera.Pos.z - origin.z) * scale;
|
||||
Global.EditorOrthoExtent = after;
|
||||
Global.pCamera = Camera;
|
||||
}
|
||||
|
||||
void editor_mode::update_camera(double const Deltatime)
|
||||
@@ -3286,6 +3555,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
if (!anyModifier && m_userinterface->on_key(Key, Action))
|
||||
return;
|
||||
|
||||
if (ui()->mode() == nodebank_panel::TRACK && is_press(Action) && (Mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)) == 0 && typed_key(Key))
|
||||
return;
|
||||
|
||||
// gizmo transform shortcuts (Q/W/E/R) — only when the camera isn't being flown (RMB up).
|
||||
// handled before the camera keyboard step because Q/W/E are also the fly-mode movement keys,
|
||||
// which would otherwise consume them.
|
||||
@@ -3316,6 +3588,16 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
return;
|
||||
}
|
||||
|
||||
if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_F && ui()->mode() == nodebank_panel::TRACK)
|
||||
{
|
||||
if (is_press(Action))
|
||||
{
|
||||
show_track_tab(m_track_tab);
|
||||
m_track_search.focus = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// then internal input handling
|
||||
if (m_input.keyboard.key(Key, Action))
|
||||
return;
|
||||
@@ -3324,7 +3606,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
return;
|
||||
|
||||
// shortcuts: undo/redo. not in the middle of a drag, it would go on from the state which was undone
|
||||
if (Global.ctrlState && (Key == GLFW_KEY_Z || Key == GLFW_KEY_Y) && is_press(Action) && (m_track_gizmo_using || m_straights.dragging || m_extend.active || m_switch.placing))
|
||||
if (Global.ctrlState && (Key == GLFW_KEY_Z || Key == GLFW_KEY_Y) && is_press(Action) && (m_track_gizmo_using || m_track_set_using || m_straights.dragging || m_extend.active || m_switch.placing || m_point_drag.active || m_handle_drag.active))
|
||||
return;
|
||||
if (Global.ctrlState && Key == GLFW_KEY_Z && is_press(Action))
|
||||
{
|
||||
@@ -3401,6 +3683,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
ui()->set_node(nullptr);
|
||||
simulation::State.delete_model(model);
|
||||
}
|
||||
else if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2)
|
||||
{
|
||||
track_set_delete();
|
||||
}
|
||||
else if (selected_track() != nullptr)
|
||||
{
|
||||
delete_selected_track();
|
||||
@@ -3423,8 +3709,16 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
cancel_track_tools();
|
||||
m_straights.tool = 0;
|
||||
}
|
||||
else
|
||||
else if (m_track_tab == track_tab::path && false == m_track_set.empty())
|
||||
m_track_set.clear();
|
||||
else if (m_track_tab != track_tab::path)
|
||||
show_track_tab(track_tab::path);
|
||||
else if (selected_track() != nullptr)
|
||||
{
|
||||
m_node = nullptr;
|
||||
m_track_point = {};
|
||||
ui()->set_node(nullptr);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -3443,7 +3737,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
case GLFW_KEY_ENTER:
|
||||
case GLFW_KEY_KP_ENTER:
|
||||
if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active)
|
||||
lay_finish({});
|
||||
lay_enter();
|
||||
break;
|
||||
|
||||
case GLFW_KEY_K:
|
||||
@@ -3452,16 +3746,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
|
||||
break;
|
||||
}
|
||||
if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && selected_track() != nullptr)
|
||||
{
|
||||
auto *track{selected_track()};
|
||||
auto const parameter{editor_track::nearest_parameter(*track, Global.pCamera.Pos + GfxRenderer->Mouse_Position())};
|
||||
auto const before{editor_track::capture(*track)};
|
||||
if (auto *created{editor_track::split_path(*track, parameter)})
|
||||
{
|
||||
push_track_snapshot({{track, before}}, {created});
|
||||
straight_refresh();
|
||||
}
|
||||
}
|
||||
split_track_at(*selected_track(), Global.pCamera.Pos + GfxRenderer->Mouse_Position());
|
||||
break;
|
||||
|
||||
case GLFW_KEY_F:
|
||||
@@ -3563,45 +3848,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
|
||||
if (mode == nodebank_panel::TRACK)
|
||||
{
|
||||
if (m_lay.active)
|
||||
{
|
||||
GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) {
|
||||
if (viewport_click())
|
||||
lay_click();
|
||||
});
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
if (start_extend() || start_switch_placement())
|
||||
{
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
if ((Mods & GLFW_MOD_ALT) != 0 && m_track_tab == track_tab::straights)
|
||||
{
|
||||
GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) {
|
||||
if (viewport_click() && dynamic_cast<TTrack *>(node) != nullptr)
|
||||
toggle_straight_set(*dynamic_cast<TTrack *>(node));
|
||||
});
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
if ((Mods & (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT)) != 0 && m_track_tab == track_tab::straights && false == current_straight().tracks.empty())
|
||||
{
|
||||
if ((Mods & GLFW_MOD_CONTROL) != 0)
|
||||
start_straight_gesture(1);
|
||||
else
|
||||
add_detour_point();
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
if (false == ImGuizmo::IsOver() && false == pick_route_vertex() && false == place_straight_tool() && false == pick_straight_handle() && false == pick_track_handle())
|
||||
{
|
||||
GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) {
|
||||
if (viewport_click())
|
||||
select_track(node);
|
||||
});
|
||||
}
|
||||
track_press(track_intent_at(Mods));
|
||||
m_input.mouse.button(Button, Action);
|
||||
return;
|
||||
}
|
||||
@@ -3700,12 +3947,18 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
if (is_release(Action))
|
||||
{
|
||||
mouseHold = false;
|
||||
finish_track_box();
|
||||
point_drag_finish();
|
||||
m_handle_drag = {};
|
||||
sweep_release();
|
||||
if (m_straights.tool_mouse)
|
||||
finish_straight_gesture();
|
||||
if (m_extend.active)
|
||||
finish_extend();
|
||||
if (m_switch.placing)
|
||||
finish_switch_placement();
|
||||
if (m_turntable.placing)
|
||||
turntable_finish_placement();
|
||||
}
|
||||
|
||||
m_dragging = false;
|
||||
@@ -3713,6 +3966,13 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
|
||||
}
|
||||
else if (Button == GLFW_MOUSE_BUTTON_RIGHT)
|
||||
{
|
||||
if (ui()->mode() == nodebank_panel::TRACK)
|
||||
{
|
||||
if (is_press(Action))
|
||||
track_context_press();
|
||||
else if (is_release(Action))
|
||||
track_context_release();
|
||||
}
|
||||
// game-engine style look: hide & grab the cursor while flying, restore it on release
|
||||
Application.set_cursor(is_press(Action) ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "editor/editorProfile.hpp"
|
||||
#include "editor/editorInfra.hpp"
|
||||
#include "editor/editorGauge.hpp"
|
||||
#include "world/Sweep.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
@@ -36,6 +37,12 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
class TAnimModel;
|
||||
class TTrack;
|
||||
namespace ui
|
||||
{
|
||||
class vehicles_bank;
|
||||
struct vehicle_desc;
|
||||
struct skin_set;
|
||||
}
|
||||
|
||||
class editor_mode : public application_mode, private editor_track::observer
|
||||
{
|
||||
@@ -51,6 +58,19 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
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, 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
|
||||
bool fixed{false};
|
||||
};
|
||||
// constructors
|
||||
editor_mode();
|
||||
~editor_mode() override;
|
||||
@@ -120,6 +140,10 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes
|
||||
scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it
|
||||
scene::instance_handle instance{0}; // include of a scenery template which was placed or removed, instead of a node
|
||||
std::vector<scene::instance_handle> instances; // includes placed or removed together
|
||||
std::vector<std::pair<scene::instance_handle, std::string>> directives; // includes changed together, with the directives to go back to
|
||||
std::vector<std::pair<sweep_node *, sweep_node::state>> sweeps; // models along curves changed, with the definitions to go back to
|
||||
std::vector<sweep_node *> sweeps_toggled; // models along curves made or removed: undo flips whether they're there
|
||||
std::vector<std::pair<TTrack *, editor_track::state>> tracks;
|
||||
std::vector<TTrack *> created;
|
||||
std::vector<TTrack *> removed;
|
||||
@@ -218,7 +242,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
// commits authored terrain to disk and enables streaming
|
||||
void commit_terrain();
|
||||
// writes the changes to the files of scenery opened for editing (Ctrl+S)
|
||||
void save();
|
||||
bool save();
|
||||
// places include of specified scenery template at the cursor, in the active layer
|
||||
void place_include(std::string const &File, int RotationMode, float FixedRotation);
|
||||
// undo and redo of placing or removing an include: brings removed include back, or removes it
|
||||
@@ -231,6 +255,8 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void set_include_directive(scene::instance_handle Instance, std::string const &Directive);
|
||||
// undo and redo of an include snapshot
|
||||
void restore_include(EditorSnapshot &Snapshot);
|
||||
// undo and redo of the includes placed, removed or changed together
|
||||
void restore_includes(EditorSnapshot &Snapshot);
|
||||
// gizmo for the selected include, limited to what the parameters of its template can express
|
||||
void render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos);
|
||||
// exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture
|
||||
@@ -269,6 +295,18 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
|
||||
// geoportal orthophoto layer drawn under the other viewport overlays
|
||||
void render_orthophoto_ui();
|
||||
void render_map_menu();
|
||||
void render_orthophoto_window();
|
||||
bool m_newscenery_asked{false};
|
||||
void render_new_scenery_popup();
|
||||
bool restart_for_new_scenery();
|
||||
bool m_orthophoto_window{false};
|
||||
// origin of the scenery in PUWG 1992 the scenery file gives in its //$g line, if it does
|
||||
bool m_georeference_read{false};
|
||||
bool m_georeference{false};
|
||||
glm::dvec2 m_georeference_origin{0.0}; // northing, easting
|
||||
std::string m_georeference_line;
|
||||
void read_georeference();
|
||||
void draw_orthophoto();
|
||||
// picks up the stored settings and the origin of the current scenery
|
||||
void load_orthophoto_settings();
|
||||
@@ -321,7 +359,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
TTrack *selected_track() const;
|
||||
void render_path_ui();
|
||||
void draw_track_overlay() const;
|
||||
bool pick_track_handle();
|
||||
editor_track::point_ref track_handle_hit() const;
|
||||
void select_track(scene::basic_node *Node);
|
||||
void render_track_gizmo();
|
||||
void commit_track_drag(bool const Force);
|
||||
@@ -432,7 +470,6 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
bool route_relay_through_switches(editor_track::chain const &Chain, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created);
|
||||
void route_recommend(alignment::vertex &Vertex) const;
|
||||
void draw_route_overlay() const;
|
||||
bool pick_route_vertex();
|
||||
void render_route_gizmo();
|
||||
glm::dvec3 route_vertex_position(int const Vertex) const;
|
||||
bool route_active() const;
|
||||
@@ -457,6 +494,8 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
bool fitting{false};
|
||||
editor_track::curve fit_before;
|
||||
editor_track::curve fit_after;
|
||||
bool fit_has_before{false};
|
||||
bool fit_has_after{false};
|
||||
std::vector<std::pair<TTrack *, editor_track::state>> drag_states;
|
||||
glm::dvec3 preview_start{0.0};
|
||||
glm::dvec3 preview_end{0.0};
|
||||
@@ -487,7 +526,6 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void draw_straights_overlay() const;
|
||||
editor_track::straight const ¤t_straight();
|
||||
bool straights_active();
|
||||
bool pick_straight_handle();
|
||||
void render_straight_gizmo();
|
||||
void straight_apply(editor_track::straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End);
|
||||
void straights_apply(std::vector<editor_track::straight> const &Lines, std::vector<std::pair<glm::dvec3, glm::dvec3>> const &Ends);
|
||||
@@ -498,7 +536,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
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<glm::dvec3(glm::dvec3 const &)> const &Tail, double const Radius);
|
||||
void straight_refresh();
|
||||
bool start_curve_fit(editor_track::straight const &Line);
|
||||
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();
|
||||
void apply_detour();
|
||||
@@ -531,9 +569,89 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
glm::dvec3 mouse{0.0};
|
||||
editor_track::straight line;
|
||||
std::string status;
|
||||
char name[64]{};
|
||||
struct drive_family
|
||||
{
|
||||
std::string name;
|
||||
std::array<std::string, 4> files;
|
||||
};
|
||||
std::vector<drive_family> drives;
|
||||
bool drives_scanned{false};
|
||||
int drive{-1};
|
||||
int drive_side{0};
|
||||
};
|
||||
switch_tool m_switch;
|
||||
void scan_switch_drives();
|
||||
std::string switch_name_for_new() const;
|
||||
bool place_switch_drive(TTrack &Switch);
|
||||
bool render_switch_drive_choice();
|
||||
int m_turnout_template{0};
|
||||
struct turntable_template
|
||||
{
|
||||
std::string file;
|
||||
std::string name;
|
||||
double length{0.0};
|
||||
int angles{0};
|
||||
int positions{0};
|
||||
int track{0};
|
||||
int yaw{0};
|
||||
std::vector<double> fixed;
|
||||
};
|
||||
struct turntable_fit
|
||||
{
|
||||
editor_track::snap_target end;
|
||||
double heading{0.0};
|
||||
double radius{0.0};
|
||||
double length{0.0};
|
||||
std::vector<segment_data> pieces;
|
||||
std::string issue;
|
||||
bool chosen{true};
|
||||
TTrack *replaces{nullptr};
|
||||
};
|
||||
struct turntable_tool
|
||||
{
|
||||
std::vector<turntable_template> templates;
|
||||
bool scanned{false};
|
||||
int chosen{-1};
|
||||
char name[64]{};
|
||||
bool placing{false};
|
||||
glm::dvec3 centre{0.0};
|
||||
double yaw{0.0};
|
||||
glm::vec2 pressed{0.f};
|
||||
editor_track::snap_target snap;
|
||||
TTrack *table{nullptr};
|
||||
scene::instance_handle include{0};
|
||||
int kind{-1};
|
||||
double length{0.0};
|
||||
double table_yaw{0.0};
|
||||
std::vector<double> exits;
|
||||
float reach{80.f};
|
||||
float radius{150.f};
|
||||
float stub{25.f};
|
||||
float step{0.f};
|
||||
std::vector<turntable_fit> fits;
|
||||
int hovered{-1};
|
||||
double heading{0.0};
|
||||
double exit_length{0.0};
|
||||
std::string status;
|
||||
std::string error;
|
||||
};
|
||||
turntable_tool m_turntable;
|
||||
void scan_turntables();
|
||||
std::string turntable_name_for_new() const;
|
||||
bool turntable_read(TTrack *Table);
|
||||
glm::dvec3 turntable_centre() const;
|
||||
std::vector<double> turntable_lines() const;
|
||||
double turntable_snap(double const Heading) const;
|
||||
void turntable_finish_placement();
|
||||
void turntable_place(glm::dvec3 const &Centre, double const Yaw);
|
||||
bool turntable_fit_end(editor_track::snap_target const &End, turntable_fit &Fit) const;
|
||||
void turntable_find_fits();
|
||||
void turntable_build(std::vector<turntable_fit const *> const &Fits);
|
||||
bool turntable_exit_at(glm::dvec3 const &Ground, double &Heading, double &Length) const;
|
||||
void turntable_store_exits();
|
||||
void render_turntable_ui();
|
||||
void draw_turntable_overlay() const;
|
||||
struct extend_tool
|
||||
{
|
||||
bool active{false};
|
||||
@@ -544,6 +662,9 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
glm::dvec3 mouse{0.0};
|
||||
int path{0};
|
||||
bool atend{true};
|
||||
editor_track::snap_target origin;
|
||||
editor_track::snap_target snap;
|
||||
glm::vec2 pressed{0.f};
|
||||
};
|
||||
extend_tool m_extend;
|
||||
// lays new track through the clicked points, with no path needed to start from
|
||||
@@ -570,18 +691,21 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void lay_click();
|
||||
void lay_finish(editor_track::snap_target const &End);
|
||||
void lay_cancel();
|
||||
void lay_enter();
|
||||
bool lay_heading(glm::dvec2 &Heading) const;
|
||||
std::vector<segment_data> lay_pieces(std::vector<glm::dvec3> const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const;
|
||||
void render_switch_ui();
|
||||
bool start_switch_placement();
|
||||
void finish_switch_placement();
|
||||
std::vector<segment_data> switch_preview() const;
|
||||
void insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side);
|
||||
void switch_placed(std::vector<TTrack *> const &Created);
|
||||
static std::vector<curve_sample> sample_chain(editor_track::chain const &Chain);
|
||||
static curve_sample sample_at(std::vector<curve_sample> const &Frame, double const Station);
|
||||
static double nearest_station(std::vector<curve_sample> const &Frame, glm::dvec3 const &Point);
|
||||
static std::vector<segment_data> curved_switch_paths(editor_track::switch_template const &Shape, std::vector<curve_sample> const &Frame, double const Station, int const Direction, int const Side);
|
||||
void insert_curved_switch(int const Direction, int const Side);
|
||||
bool place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, std::vector<TTrack *> &Removed);
|
||||
bool place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, std::vector<TTrack *> &Removed, std::string const &Name = {});
|
||||
struct crossover_plan
|
||||
{
|
||||
editor_track::straight target;
|
||||
@@ -597,7 +721,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape);
|
||||
bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape);
|
||||
std::vector<TTrack *> 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, double const Radius, TTrack &Style);
|
||||
bool start_extend();
|
||||
bool start_extend(editor_track::snap_target const &End);
|
||||
void finish_extend();
|
||||
std::vector<segment_data> extend_pieces() const;
|
||||
// the free end of the path as the start of a new track; false if it isn't free or has no direction
|
||||
@@ -614,7 +738,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void update_build_tools();
|
||||
void draw_build_overlay() 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, infra, lay };
|
||||
enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable };
|
||||
struct track_mode
|
||||
{
|
||||
char const *label;
|
||||
@@ -622,7 +746,10 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
track_tab tab;
|
||||
char const *tooltip;
|
||||
};
|
||||
static std::array<track_mode, 8> const &track_modes();
|
||||
static std::array<track_mode, 11> const &track_modes();
|
||||
// the objects along the track: a tab for each tool
|
||||
int m_lineside_tab{0};
|
||||
void render_lineside_ui();
|
||||
void render_track_menu();
|
||||
void arm_switch();
|
||||
bool m_track_window_open{false};
|
||||
@@ -630,11 +757,21 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void render_track_inspector();
|
||||
void render_track_modes(TTrack *Track);
|
||||
void show_track_tab(track_tab const Tab);
|
||||
bool track_analysis_tab() const { return m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::infra; }
|
||||
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
|
||||
std::string track_mode_name() const;
|
||||
void render_turnout_ui();
|
||||
struct track_name_edit
|
||||
{
|
||||
TTrack const *track{nullptr};
|
||||
std::string synced;
|
||||
char text[128]{};
|
||||
editor_track::name_uses uses;
|
||||
std::string status;
|
||||
};
|
||||
track_name_edit m_track_name;
|
||||
void render_track_name(TTrack &Track);
|
||||
void render_path_parameters(TTrack &Track);
|
||||
void render_straight_ui();
|
||||
track_tab m_track_tab{track_tab::path};
|
||||
@@ -651,6 +788,431 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void render_track_guide();
|
||||
std::string track_readout() const;
|
||||
void draw_track_hints();
|
||||
// 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};
|
||||
};
|
||||
track_hover m_hover;
|
||||
track_hover track_under_cursor() const;
|
||||
void update_track_hover();
|
||||
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 };
|
||||
kind what{kind::none};
|
||||
TTrack *track{nullptr};
|
||||
int path{0};
|
||||
editor_track::point_ref point;
|
||||
int index{-1};
|
||||
glm::dvec3 position{0.0};
|
||||
editor_track::snap_target snap;
|
||||
std::string action;
|
||||
};
|
||||
track_intent m_intent;
|
||||
track_intent track_intent_at(int const Mods);
|
||||
bool track_gesture_on() const;
|
||||
void update_track_intent();
|
||||
void track_press(track_intent const &Intent);
|
||||
void turntable_press(track_intent const &Intent);
|
||||
track_intent turntable_intent(glm::dvec3 const &Ground, bool const Shift);
|
||||
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<TTrack const *> const &Exclude) const;
|
||||
void draw_free_ends(TTrack const *Self, int const Category, std::vector<TTrack const *> const &Exclude) const;
|
||||
struct point_drag
|
||||
{
|
||||
bool active{false};
|
||||
bool moved{false};
|
||||
glm::vec2 from{0.f};
|
||||
double height{0.0};
|
||||
};
|
||||
point_drag m_point_drag;
|
||||
struct handle_drag
|
||||
{
|
||||
bool active{false};
|
||||
bool moved{false};
|
||||
glm::vec2 from{0.f};
|
||||
double height{0.0};
|
||||
glm::dvec3 offset{0.0};
|
||||
glm::dvec3 position{0.0};
|
||||
};
|
||||
handle_drag m_handle_drag;
|
||||
void handle_drag_start(glm::dvec3 const &Anchor);
|
||||
bool handle_drag_step(glm::dvec3 &Position);
|
||||
void point_drag_start(editor_track::point_ref const &Point);
|
||||
void point_drag_update();
|
||||
void point_drag_finish();
|
||||
bool route_hit(int &Vertex, int &Grip) const;
|
||||
int straight_handle_hit();
|
||||
bool switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point);
|
||||
glm::dvec3 cursor_ground() const;
|
||||
glm::dvec3 cursor_level(double const Height) const;
|
||||
bool sweep_grabs(glm::dvec3 const &Point);
|
||||
// menu of the path a click of the right mouse button lands on
|
||||
struct track_context
|
||||
{
|
||||
track_hover target;
|
||||
std::chrono::steady_clock::time_point pressed;
|
||||
glm::vec3 angle{0.f};
|
||||
glm::vec2 mouse{0.f};
|
||||
bool armed{false};
|
||||
bool pending{false};
|
||||
};
|
||||
track_context m_track_context;
|
||||
void track_context_press();
|
||||
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);
|
||||
// paths found by their names
|
||||
struct track_search
|
||||
{
|
||||
char text[64]{};
|
||||
std::string searched;
|
||||
std::vector<TTrack *> found;
|
||||
std::size_t total{0};
|
||||
bool focus{false};
|
||||
};
|
||||
track_search m_track_search;
|
||||
void render_track_search();
|
||||
// paths selected together: Shift+LMB adds or takes one out, Shift+drag adds those in the box
|
||||
std::vector<TTrack *> m_track_set;
|
||||
struct track_box
|
||||
{
|
||||
bool active{false};
|
||||
glm::vec2 from{0.f};
|
||||
glm::vec2 to{0.f};
|
||||
TTrack *hovered{nullptr};
|
||||
};
|
||||
track_box m_track_box;
|
||||
struct track_set_fields
|
||||
{
|
||||
double velocity{100.0};
|
||||
char rail[128]{};
|
||||
char trackbed[128]{};
|
||||
char circuits[256]{};
|
||||
int environment{0};
|
||||
};
|
||||
track_set_fields m_track_set_fields;
|
||||
glm::mat4 m_track_set_gizmo{1.0f};
|
||||
bool m_track_set_using{false};
|
||||
glm::dvec3 m_track_set_pivot{0.0};
|
||||
std::vector<std::pair<TTrack *, std::vector<editor_track::joint>>> m_track_set_joints;
|
||||
bool in_track_set(TTrack const *Track) const;
|
||||
void track_set_toggle(TTrack &Track);
|
||||
void track_set_prune();
|
||||
void start_track_box();
|
||||
void finish_track_box();
|
||||
void draw_track_set() const;
|
||||
void render_track_set_ui();
|
||||
void render_track_set_gizmo();
|
||||
void track_set_delete();
|
||||
struct track_spread
|
||||
{
|
||||
int stop{0};
|
||||
bool same_rails{false};
|
||||
bool same_bed{false};
|
||||
bool same_speed{false};
|
||||
float reach{0.f};
|
||||
TTrack const *from{nullptr};
|
||||
int signature{-1};
|
||||
std::vector<TTrack *> preview;
|
||||
double length{0.0};
|
||||
};
|
||||
mutable track_spread m_spread;
|
||||
std::vector<TTrack *> track_spread_from(TTrack &Start) const;
|
||||
std::vector<TTrack *> const &track_spread_preview(TTrack &Start) const;
|
||||
void track_spread_apply(TTrack &Start);
|
||||
void track_set_grow();
|
||||
void render_track_spread_ui();
|
||||
void draw_track_spread() const;
|
||||
// changes each path of the set which can be changed; false from Change: the path stays as it was
|
||||
void track_set_apply(std::function<bool(TTrack &)> const &Change, bool const Rebuild, char const *What);
|
||||
// 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};
|
||||
std::vector<segment_data> pieces;
|
||||
std::vector<TTrack *> styles;
|
||||
double length{0.0};
|
||||
std::string error;
|
||||
std::string status;
|
||||
};
|
||||
parallel_tool m_parallel;
|
||||
void parallel_update();
|
||||
void render_parallel_ui();
|
||||
void parallel_build();
|
||||
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<std::string> rest; // parameters after the rotation
|
||||
bool described{false};
|
||||
std::string track;
|
||||
};
|
||||
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<std::string> values;
|
||||
};
|
||||
// includes of the templates whose file names the test accepts, with the location in p2..p4 and the rotation in p5
|
||||
std::vector<standing_template> standing_templates(std::function<bool(std::string const &)> const &Accept) const;
|
||||
// places them in the active layer, as one step of the undo. returns: how many were placed
|
||||
std::size_t place_templates(std::vector<template_item> const &Items, std::string &Error);
|
||||
// moves them, as one step of the undo
|
||||
std::size_t move_templates(std::vector<std::pair<standing_template, template_item>> const &Moves);
|
||||
// takes them out, as one step of the undo
|
||||
std::size_t remove_templates(std::vector<scene::instance_handle> 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<editor_track::route_sample> samples;
|
||||
double origin{0.0}; // chainage of the point
|
||||
std::vector<hekto_post> posts;
|
||||
std::vector<hekto_found> 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{0}; // 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<fouling_point> points;
|
||||
std::vector<fouling_found> found;
|
||||
std::string status;
|
||||
std::string error;
|
||||
};
|
||||
fouling_tool m_fouling;
|
||||
void fouling_update();
|
||||
void fouling_place();
|
||||
void fouling_fix();
|
||||
void render_fouling_ui();
|
||||
void draw_fouling_overlay() const;
|
||||
|
||||
// a vehicle put on the selected path, to be driven: it isn't written to the scenery files
|
||||
struct vehicle_tool
|
||||
{
|
||||
bool open{false};
|
||||
bool expand{false};
|
||||
std::shared_ptr<ui::vehicles_bank> bank;
|
||||
char search[64]{};
|
||||
bool controllable_only{true};
|
||||
std::shared_ptr<ui::vehicle_desc> vehicle;
|
||||
std::shared_ptr<ui::skin_set> 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<ui::vehicle_desc> vehicle;
|
||||
std::shared_ptr<ui::skin_set> skin;
|
||||
bool turned{false};
|
||||
};
|
||||
std::vector<consist_vehicle> consist;
|
||||
int facing{1};
|
||||
};
|
||||
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;
|
||||
|
||||
// a model laid along the track: bent to follow it, or repeated along it
|
||||
struct sweep_tool
|
||||
{
|
||||
bool open{false};
|
||||
bool expand{false};
|
||||
char model[256]{};
|
||||
char skin[128]{"none"};
|
||||
char search[64]{};
|
||||
std::vector<std::string> models;
|
||||
bool scanned{false};
|
||||
// parameters of a template laid along the track, with their names
|
||||
std::string parameters_for;
|
||||
std::vector<std::array<char, 128>> parameters;
|
||||
std::vector<std::string> parameter_labels;
|
||||
std::vector<bool> parameter_placement; // filled in with zeros: the template is placed in its own space
|
||||
std::vector<std::vector<std::string>> parameter_choices; // variants of the textures a parameter is a part of the name of
|
||||
int length_mode{0}; // 0: one copy as long as the original, 1: the length given, 2: the whole stretch of the line
|
||||
double length{200.0};
|
||||
sweep_node::state settings;
|
||||
int axis{0}; // 0: the longer one of the model, 1: z, 2: x
|
||||
double model_length{0.0};
|
||||
std::string measured; // model the length is of
|
||||
// the curve: the axis of the track
|
||||
TTrack *curve_for{nullptr};
|
||||
std::size_t curve_history{0};
|
||||
struct piece
|
||||
{
|
||||
TTrack *track{nullptr};
|
||||
int path{0};
|
||||
bool forward{true};
|
||||
};
|
||||
std::vector<piece> curve_pieces;
|
||||
std::vector<segment_data> curve;
|
||||
double curve_length{0.0};
|
||||
std::vector<glm::dvec3> outline; // of the curve, for the preview
|
||||
std::vector<double> stations;
|
||||
// marking the stretch in the view: press where it starts, the distance from the track sets the offset, drag to the end
|
||||
bool marking{false};
|
||||
bool dragging{false};
|
||||
double anchor{0.0}; // station the drag started at
|
||||
sweep_node *edited{nullptr};
|
||||
std::string status;
|
||||
};
|
||||
sweep_tool m_sweep;
|
||||
// paths the models along curves were laid on, so they follow the changes of the paths
|
||||
struct sweep_link
|
||||
{
|
||||
sweep_node *sweep{nullptr};
|
||||
std::size_t piece{0};
|
||||
TTrack *track{nullptr};
|
||||
int path{0};
|
||||
bool reversed{false};
|
||||
};
|
||||
std::vector<sweep_link> m_sweep_links;
|
||||
void sweep_curve();
|
||||
void sweep_outline();
|
||||
bool sweep_extend(bool const Atend, glm::dvec3 const &Cursor);
|
||||
double sweep_station(glm::dvec3 const &Point, double &Lateral) const;
|
||||
void sweep_scan_models();
|
||||
void sweep_measure();
|
||||
sweep_node::state sweep_definition() const;
|
||||
void sweep_create();
|
||||
void sweep_apply();
|
||||
void sweep_delete(sweep_node &Sweep);
|
||||
void sweep_edit(sweep_node &Sweep);
|
||||
std::vector<sweep_node *> sweeps_near(TTrack const &Track) const;
|
||||
bool sweep_press();
|
||||
void sweep_drag();
|
||||
void sweep_release();
|
||||
// the same at a point of the world: Anywhere takes the press however far from the track it is
|
||||
bool sweep_press_at(glm::dvec3 const &Point, bool const Anywhere);
|
||||
void sweep_drag_at(glm::dvec3 const &Point);
|
||||
void render_sweep_ui();
|
||||
void draw_sweep_overlay() const;
|
||||
void restore_sweeps(EditorSnapshot &Snapshot);
|
||||
void sweeps_captured(TTrack const &Track);
|
||||
void sweeps_committed(std::vector<TTrack *> const &Tracks);
|
||||
void sweeps_split(TTrack &Original, TTrack &Created);
|
||||
|
||||
// script of editor actions given in the EU07_EDITOR_SELFTEST file, run a step at a time, for testing without a person
|
||||
struct selftest
|
||||
{
|
||||
bool loaded{false};
|
||||
std::vector<std::string> lines;
|
||||
std::size_t next{0};
|
||||
int wait{0};
|
||||
bool quitting{false};
|
||||
};
|
||||
selftest m_selftest;
|
||||
void selftest_step();
|
||||
// a value typed during a gesture, taken instead of the one the cursor gives
|
||||
std::string m_typed;
|
||||
// what the typed value sets in the gesture under way, nullptr when the gesture takes none
|
||||
char const *typed_meaning() const;
|
||||
std::vector<double> typed_values() const;
|
||||
bool typed_key(int const Key);
|
||||
void typed_extend();
|
||||
glm::dvec3 typed_lay(glm::dvec3 const &Mouse) const;
|
||||
void typed_turn();
|
||||
bool m_route_tab{true};
|
||||
|
||||
// vertical profile (grade line) along a route
|
||||
@@ -674,6 +1236,7 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
// the editor terrain led to the grade line: formation under the track, slopes down or up to the ground
|
||||
struct earthworks
|
||||
{
|
||||
bool from_ballast{true};
|
||||
double depth{0.7}; // m, formation below the top of the rail
|
||||
double half_width{3.5}; // m, from the axis of the route to the edge of the formation
|
||||
double slope{1.5}; // horizontal run of the slopes per metre of height
|
||||
@@ -681,6 +1244,9 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
double rounding{1.5}; // m, over which the slopes bend into the formation and into the ground
|
||||
// heights of the terrain before the last shaping
|
||||
std::vector<std::pair<editor_terrain *, std::vector<float>>> undo;
|
||||
bool generate{true};
|
||||
std::vector<std::pair<int, int>> generated;
|
||||
bool generated_streaming{false};
|
||||
} earthworks;
|
||||
double view_from{0.0};
|
||||
double view_to{100.0};
|
||||
@@ -736,7 +1302,9 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void profile_resample();
|
||||
void profile_recognize();
|
||||
void profile_fit_ground();
|
||||
static std::pair<double, double> profile_ballast(TTrack const &Track);
|
||||
void profile_shape_ground();
|
||||
std::pair<int, int> profile_generate_ground(std::vector<std::pair<double, double>> const &Beds, double const Reach);
|
||||
void profile_restore_ground();
|
||||
void render_profile_earthworks();
|
||||
void profile_check();
|
||||
@@ -762,6 +1330,13 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
std::string reason;
|
||||
bool chosen{false};
|
||||
bool bound{false};
|
||||
TTrack *reader{nullptr};
|
||||
std::string event; // of that path
|
||||
std::string cell;
|
||||
bool loose{false};
|
||||
TTrack *nearer{nullptr};
|
||||
glm::dvec3 nearer_foot{0.0};
|
||||
bool unread{false};
|
||||
};
|
||||
struct infra_window
|
||||
{
|
||||
@@ -774,6 +1349,11 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
std::vector<infra::rule> rules{infra::default_rules()};
|
||||
std::vector<TTrack *> tracks; // of the scope
|
||||
std::vector<infra_candidate> candidates;
|
||||
int filter{0};
|
||||
int read{0};
|
||||
int overruled{0};
|
||||
int unread{0};
|
||||
int elsewhere{0};
|
||||
int hovered{-1};
|
||||
std::string status;
|
||||
std::string error;
|
||||
@@ -827,6 +1407,36 @@ class editor_mode : public application_mode, private editor_track::observer
|
||||
void speed_apply(std::vector<int> const &Indices, bool const Branches);
|
||||
std::vector<int> speed_listed() const;
|
||||
void draw_speed_overlay() const;
|
||||
|
||||
// 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
|
||||
bool free_ends{false};
|
||||
std::vector<TTrack *> queue;
|
||||
std::size_t next{0};
|
||||
std::vector<joint_issue> 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);
|
||||
void joints_focus(int const Index);
|
||||
bool joint_fix(joint_issue &Issue, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Changed);
|
||||
void joints_fix(std::vector<int> const &Indices);
|
||||
void render_joints_body();
|
||||
void draw_joints_overlay() const;
|
||||
void infra_rebase();
|
||||
void render_infra_search();
|
||||
void render_infra_candidates();
|
||||
|
||||
@@ -43,6 +43,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
namespace
|
||||
{
|
||||
|
||||
using geometry::bezier;
|
||||
using geometry::plan_distance;
|
||||
using geometry::plan_of;
|
||||
|
||||
@@ -419,6 +420,159 @@ class track_index
|
||||
std::map<cell_key, std::vector<std::size_t>> m_cells;
|
||||
};
|
||||
|
||||
struct cell_reader
|
||||
{
|
||||
TTrack *track{nullptr};
|
||||
std::string event;
|
||||
std::string cell;
|
||||
bool loose{false};
|
||||
};
|
||||
|
||||
class event_links
|
||||
{
|
||||
public:
|
||||
event_links()
|
||||
{
|
||||
std::unordered_map<std::string, TTrack *> named;
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
if (track != nullptr && false == track->m_editorremoved && false == track->name().empty() && track->name() != "none")
|
||||
named.emplace(track->name(), track);
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
{
|
||||
if (track == nullptr || track->m_editorremoved)
|
||||
continue;
|
||||
for (auto const *sequence : {&track->m_events0, &track->m_events1, &track->m_events2, &track->m_events0all, &track->m_events1all, &track->m_events2all})
|
||||
for (auto const &entry : *sequence)
|
||||
read(track, entry.second != nullptr ? entry.second : simulation::Events.FindEvent(entry.first), entry.first.empty());
|
||||
}
|
||||
static std::set<std::string> const kinds{"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"};
|
||||
std::unordered_set<scene::basic_node const *> models(simulation::Instances.sequence().begin(), simulation::Instances.sequence().end());
|
||||
std::unordered_set<scene::basic_node const *> cells(simulation::Memory.sequence().begin(), simulation::Memory.sequence().end());
|
||||
for (auto *event : simulation::Events.sequence())
|
||||
{
|
||||
if (event == nullptr)
|
||||
continue;
|
||||
auto const &name{event->name()};
|
||||
auto const colon{name.rfind(':')};
|
||||
if (colon != std::string::npos && colon > 0 && kinds.count(name.substr(colon + 1)) > 0)
|
||||
{
|
||||
auto const track{named.find(name.substr(0, colon))};
|
||||
if (track != named.end())
|
||||
read(track->second, event, true);
|
||||
}
|
||||
auto const *multi{dynamic_cast<multi_event const *>(event)};
|
||||
if (multi == nullptr)
|
||||
continue;
|
||||
std::vector<scene::basic_node const *> shown;
|
||||
std::vector<TMemCell *> set;
|
||||
for (auto *child : multi->children())
|
||||
{
|
||||
auto const setter{dynamic_cast<updatevalues_event const *>(child) != nullptr || dynamic_cast<copyvalues_event const *>(child) != nullptr};
|
||||
for (auto *target : child->target_nodes())
|
||||
{
|
||||
if (models.count(target) > 0 && false == contains(shown, static_cast<scene::basic_node const *>(target)))
|
||||
shown.push_back(target);
|
||||
else if (setter && cells.count(target) > 0 && false == contains(set, static_cast<TMemCell *>(target)))
|
||||
set.push_back(static_cast<TMemCell *>(target));
|
||||
}
|
||||
}
|
||||
if (shown.size() > 4 || set.size() > 4)
|
||||
continue;
|
||||
for (auto const *model : shown)
|
||||
for (auto *cell : set)
|
||||
if (plan_distance(model->location(), cell->location()) <= kCellReach && false == contains(m_cells[model], cell))
|
||||
m_cells[model].push_back(cell);
|
||||
}
|
||||
for (auto *cell : simulation::Memory.sequence())
|
||||
{
|
||||
if (cell == nullptr)
|
||||
continue;
|
||||
if (cell->m_instance != 0)
|
||||
m_included[cell->m_instance].push_back(cell);
|
||||
if (false == cell->name().empty() && cell->name() != "none")
|
||||
m_named.emplace_back(cell->name(), cell);
|
||||
}
|
||||
std::sort(m_named.begin(), m_named.end(), [](auto const &A, auto const &B) { return A.first < B.first; });
|
||||
}
|
||||
std::vector<TMemCell *> cells_of(infra::binding const &Binding) const
|
||||
{
|
||||
std::vector<TMemCell *> result;
|
||||
switch (Binding.type)
|
||||
{
|
||||
case infra::kind::memcell: result.push_back(static_cast<TMemCell *>(Binding.node)); break;
|
||||
case infra::kind::include:
|
||||
{
|
||||
auto const found{m_included.find(Binding.include)};
|
||||
if (found != m_included.end())
|
||||
result = found->second;
|
||||
break;
|
||||
}
|
||||
case infra::kind::model:
|
||||
{
|
||||
auto const found{m_cells.find(Binding.node)};
|
||||
if (found != m_cells.end())
|
||||
result = found->second;
|
||||
auto const &name{Binding.node->name()};
|
||||
if (result.empty() && name.size() >= 3 && name != "none")
|
||||
for (auto entry{std::lower_bound(m_named.begin(), m_named.end(), name, [](auto const &Entry, std::string const &Name) { return Entry.first < Name; })};
|
||||
entry != m_named.end() && entry->first.compare(0, name.size(), name) == 0; ++entry)
|
||||
if (plan_distance(entry->second->location(), Binding.node->location()) <= kCellReach)
|
||||
result.push_back(entry->second);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::vector<cell_reader> readers_of(std::vector<TMemCell *> const &Cells) const
|
||||
{
|
||||
std::vector<cell_reader> result;
|
||||
for (auto const *cell : Cells)
|
||||
{
|
||||
auto const found{m_readers.find(cell)};
|
||||
if (found == m_readers.end())
|
||||
continue;
|
||||
for (auto const &reader : found->second)
|
||||
if (std::none_of(result.begin(), result.end(), [&](cell_reader const &Reader) { return Reader.track == reader.track; }))
|
||||
result.push_back(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr double kCellReach{60.0};
|
||||
void read(TTrack *Track, basic_event *Event, bool const Loose)
|
||||
{
|
||||
std::vector<basic_event *> pending{Event};
|
||||
std::unordered_set<basic_event *> seen;
|
||||
while (false == pending.empty())
|
||||
{
|
||||
auto *event{pending.back()};
|
||||
pending.pop_back();
|
||||
if (event == nullptr || false == seen.insert(event).second)
|
||||
continue;
|
||||
if (auto const *multi{dynamic_cast<multi_event const *>(event)})
|
||||
{
|
||||
auto const children{multi->children()};
|
||||
pending.insert(pending.end(), children.begin(), children.end());
|
||||
continue;
|
||||
}
|
||||
if (dynamic_cast<getvalues_event const *>(event) == nullptr)
|
||||
continue;
|
||||
auto const targets{event->target_nodes()};
|
||||
if (targets.empty())
|
||||
continue;
|
||||
auto &list{m_readers[targets.front()]};
|
||||
if (std::none_of(list.begin(), list.end(), [&](cell_reader const &Reader) { return Reader.track == Track; }))
|
||||
list.push_back({Track, Event->name(), targets.front()->name(), Loose});
|
||||
}
|
||||
}
|
||||
std::unordered_map<scene::basic_node const *, std::vector<cell_reader>> m_readers;
|
||||
std::unordered_map<scene::basic_node const *, std::vector<TMemCell *>> m_cells;
|
||||
std::unordered_map<scene::instance_handle, std::vector<TMemCell *>> m_included;
|
||||
std::vector<std::pair<std::string, TMemCell *>> m_named;
|
||||
};
|
||||
|
||||
// lookup of points by a grid of 1 m cells in the plan
|
||||
template <typename Type_>
|
||||
class point_grid
|
||||
@@ -711,8 +865,10 @@ void editor_mode::infra_recognize()
|
||||
state.error = (state.scope == 2 ? STR_C("Open the vertical profile of a route first") : state.scope == 3 ? STR_C("The scenery has no paths the editor can handle") : STR_C("Select a path first"));
|
||||
return;
|
||||
}
|
||||
state.read = state.overruled = state.unread = state.elsewhere = 0;
|
||||
track_index index;
|
||||
std::unordered_set<std::string> names;
|
||||
std::unordered_set<TTrack const *> const scope(state.tracks.begin(), state.tracks.end());
|
||||
for (auto *track : state.tracks)
|
||||
{
|
||||
index.add(*track);
|
||||
@@ -720,6 +876,18 @@ void editor_mode::infra_recognize()
|
||||
names.insert(track->name());
|
||||
}
|
||||
Live.refresh();
|
||||
event_links const links;
|
||||
std::unordered_map<TTrack *, std::vector<track_area>> threads;
|
||||
auto const thread_of = [&](TTrack *Reader) -> std::vector<track_area> const & {
|
||||
auto found{threads.find(Reader)};
|
||||
if (found != threads.end())
|
||||
return found->second;
|
||||
std::vector<track_area> areas;
|
||||
for (auto const &span : editor_track::run_route(*Reader, 250.0).spans)
|
||||
if (scope.count(span.track) > 0 && std::none_of(areas.begin(), areas.end(), [&](track_area const &Area) { return Area.track == span.track; }))
|
||||
areas.push_back(area_of(*span.track));
|
||||
return threads.emplace(Reader, std::move(areas)).first->second;
|
||||
};
|
||||
|
||||
auto const corridor{static_cast<double>(state.corridor)};
|
||||
auto const add = [&](infra::binding Binding, std::string const &Reason, double const Reach) {
|
||||
@@ -727,12 +895,57 @@ void editor_mode::infra_recognize()
|
||||
std::optional<double> yaw;
|
||||
if (false == object_points(Binding, points, yaw))
|
||||
return;
|
||||
for (auto const &point : points)
|
||||
infra_candidate candidate;
|
||||
auto const cells{points.size() == 1 ? links.cells_of(Binding) : std::vector<TMemCell *>{}};
|
||||
auto const readers{links.readers_of(cells)};
|
||||
infra::station geometric;
|
||||
auto const near{index.nearest(points.front(), Reach, geometric)};
|
||||
if (false == readers.empty())
|
||||
{
|
||||
auto best{-1.0};
|
||||
infra::station station;
|
||||
if (false == index.nearest(point, Reach, station))
|
||||
for (auto const &reader : readers)
|
||||
{
|
||||
infra::station at;
|
||||
auto const &thread{thread_of(reader.track)};
|
||||
if (false == nearest_beside(thread, points.front(), std::max(Reach, 25.0), at))
|
||||
continue;
|
||||
auto const distance{plan_distance(infra::frame_at(at).point, points.front())};
|
||||
if (best < 0.0 || distance < best)
|
||||
{
|
||||
best = distance;
|
||||
station = at;
|
||||
candidate.reader = reader.track;
|
||||
candidate.event = reader.event;
|
||||
candidate.cell = reader.cell;
|
||||
candidate.loose = reader.loose;
|
||||
}
|
||||
}
|
||||
if (candidate.reader == nullptr)
|
||||
{
|
||||
state.elsewhere += near ? 1 : 0;
|
||||
return;
|
||||
Binding.anchors.push_back(infra::make_anchor(station, point));
|
||||
}
|
||||
auto const &thread{thread_of(candidate.reader)};
|
||||
if (near && geometric.track != station.track && std::none_of(thread.begin(), thread.end(), [&](track_area const &Area) { return Area.track == geometric.track; }))
|
||||
{
|
||||
candidate.nearer = geometric.track;
|
||||
candidate.nearer_foot = infra::frame_at(geometric).point;
|
||||
}
|
||||
Binding.anchors.push_back(infra::make_anchor(station, points.front()));
|
||||
if (Binding.group == infra::category::other && Binding.type != infra::kind::memcell)
|
||||
Binding.group = infra::category::signal;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto const &point : points)
|
||||
{
|
||||
infra::station station;
|
||||
if (false == index.nearest(point, Reach, station))
|
||||
return;
|
||||
Binding.anchors.push_back(infra::make_anchor(station, point));
|
||||
}
|
||||
candidate.unread = (false == cells.empty() && Binding.type != infra::kind::memcell && Binding.group == infra::category::signal);
|
||||
}
|
||||
if (std::abs(Binding.anchors.front().height) > 40.0)
|
||||
return;
|
||||
@@ -740,12 +953,14 @@ void editor_mode::infra_recognize()
|
||||
if (Binding.turns)
|
||||
Binding.yaw = turn_of(Binding, *yaw);
|
||||
Binding.label = object_label(Binding);
|
||||
infra_candidate candidate;
|
||||
candidate.offset = Binding.anchors.front().offset;
|
||||
candidate.reason = Reason;
|
||||
candidate.bound = (infra_find(Binding) != nullptr);
|
||||
candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other);
|
||||
candidate.binding = std::move(Binding);
|
||||
state.read += candidate.reader != nullptr ? 1 : 0;
|
||||
state.overruled += candidate.nearer != nullptr ? 1 : 0;
|
||||
state.unread += candidate.unread ? 1 : 0;
|
||||
state.candidates.push_back(std::move(candidate));
|
||||
};
|
||||
auto const rule_reason = [](infra::category const Group, char const *What) { return Group == infra::category::other ? std::string{"nearby, no rule matches the "} + What : std::string{"the "} + What + " matches a rule"; };
|
||||
@@ -805,6 +1020,8 @@ void editor_mode::infra_recognize()
|
||||
}
|
||||
std::stable_sort(state.candidates.begin(), state.candidates.end(), [](infra_candidate const &A, infra_candidate const &B) { return A.binding.group < B.binding.group; });
|
||||
state.status = format(STR_C("%d object(s) found along %d path(s)"), static_cast<int>(state.candidates.size()), static_cast<int>(state.tracks.size()));
|
||||
if (state.elsewhere > 0)
|
||||
state.status += format(STR_C("; %d left out: they stand by the scope, but the paths which read them are elsewhere"), state.elsewhere);
|
||||
}
|
||||
|
||||
void editor_mode::infra_bind_chosen()
|
||||
@@ -910,6 +1127,7 @@ void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh
|
||||
|
||||
void editor_mode::track_captured(TTrack const &Track)
|
||||
{
|
||||
sweeps_captured(Track);
|
||||
if (m_infra_suspended || false == m_infra.follow)
|
||||
return;
|
||||
infra_load();
|
||||
@@ -947,6 +1165,7 @@ void editor_mode::track_captured(TTrack const &Track)
|
||||
|
||||
void editor_mode::tracks_committed(std::vector<TTrack *> const &Tracks)
|
||||
{
|
||||
sweeps_committed(Tracks);
|
||||
if (m_infra_suspended || false == m_infra.follow)
|
||||
return;
|
||||
infra_load();
|
||||
@@ -1031,6 +1250,7 @@ void editor_mode::track_retired(TTrack &)
|
||||
|
||||
void editor_mode::track_split(TTrack &Original, TTrack &Created, segment_data const &First)
|
||||
{
|
||||
sweeps_split(Original, Created);
|
||||
if (m_infra_suspended || Created.m_paths.empty())
|
||||
return;
|
||||
auto const length{infra::plan_length(First)};
|
||||
@@ -1185,9 +1405,9 @@ void editor_mode::infra_rebase()
|
||||
void editor_mode::render_infra_search()
|
||||
{
|
||||
auto &state{m_infra};
|
||||
static char const *const scopes[] = {"Selected path", "Line through the selected path", "Route of the vertical profile", "Whole scenery"};
|
||||
char const *const scopes[] = {STR_C("Selected path"), STR_C("Line through the selected path"), STR_C("Route of the vertical profile"), STR_C("Whole scenery")};
|
||||
ImGui::SetNextItemWidth(260.f);
|
||||
ImGui::Combo("Look along", &state.scope, scopes, IM_ARRAYSIZE(scopes));
|
||||
ImGui::Combo(STR_C("Look along"), &state.scope, scopes, IM_ARRAYSIZE(scopes));
|
||||
if (state.scope == 1)
|
||||
{
|
||||
ImGui::SetNextItemWidth(120.f);
|
||||
@@ -1224,77 +1444,140 @@ void editor_mode::render_infra_candidates()
|
||||
{
|
||||
auto &state{m_infra};
|
||||
auto &candidates{state.candidates};
|
||||
ImVec4 const readcolour{0.45f, 0.85f, 1.f, 1.f};
|
||||
ImVec4 const warncolour{1.f, 0.75f, 0.3f, 1.f};
|
||||
auto const shown = [&](infra_candidate const &Candidate) {
|
||||
switch (state.filter)
|
||||
{
|
||||
case 1: return Candidate.reader != nullptr;
|
||||
case 2: return Candidate.nearer != nullptr;
|
||||
case 3: return Candidate.unread;
|
||||
default: return true;
|
||||
}
|
||||
};
|
||||
auto const choose = [&](std::optional<infra::category> const Group, bool const Chosen) {
|
||||
for (auto &candidate : candidates)
|
||||
if (false == Group.has_value() || candidate.binding.group == *Group)
|
||||
if ((false == Group.has_value() || candidate.binding.group == *Group) && shown(candidate))
|
||||
candidate.chosen = Chosen && false == candidate.bound;
|
||||
};
|
||||
state.hovered = -1;
|
||||
if (false == candidates.empty())
|
||||
{
|
||||
int chosen{0};
|
||||
for (auto const &candidate : candidates)
|
||||
chosen += candidate.chosen ? 1 : 0;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(format(STR_C("Bind the chosen (%d)"), chosen).c_str()))
|
||||
infra_bind_chosen();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("All")))
|
||||
choose({}, true);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("None")))
|
||||
choose({}, false);
|
||||
if (candidates.empty())
|
||||
return;
|
||||
int chosen{0};
|
||||
for (auto const &candidate : candidates)
|
||||
chosen += candidate.chosen ? 1 : 0;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(format(STR_C("Bind the chosen (%d)"), chosen).c_str()))
|
||||
infra_bind_chosen();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("All")))
|
||||
choose({}, true);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("None")))
|
||||
choose({}, false);
|
||||
|
||||
ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true);
|
||||
for (auto const group : infra::categories())
|
||||
if (state.read > 0 || state.unread > 0)
|
||||
{
|
||||
ImGui::TextColored(readcolour, STR_C("%d read by the paths"), state.read);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(STR_C("The memory cell of the object is read by a getvalues event of a path, listed by the path, inside a multiple\n"
|
||||
"event or written loose as <path>:event<n>. The object is bound to the thread of that path, not to the nearest one"));
|
||||
if (state.overruled > 0)
|
||||
{
|
||||
auto const count{std::count_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })};
|
||||
if (count == 0)
|
||||
continue;
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(colour(group)));
|
||||
auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast<int>(count)).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)};
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton((std::string{STR_C("all##g")} + infra::name(group)).c_str()))
|
||||
choose(group, true);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton((std::string{STR_C("none##g")} + infra::name(group)).c_str()))
|
||||
choose(group, false);
|
||||
if (false == open)
|
||||
continue;
|
||||
// the candidates are sorted by the category
|
||||
auto const first{static_cast<int>(std::distance(candidates.begin(), std::find_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })))};
|
||||
ImGuiListClipper clipper(static_cast<int>(count));
|
||||
while (clipper.Step())
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
||||
{
|
||||
auto const i{static_cast<std::size_t>(first + row)};
|
||||
auto &candidate{candidates[i]};
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (candidate.bound)
|
||||
{
|
||||
ImGui::TextDisabled(STR_C("[bound]"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Checkbox("##chosen", &candidate.chosen);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", candidate.binding.label.c_str());
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
state.hovered = static_cast<int>(i);
|
||||
ImGui::SetTooltip(STR_C("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s"), infra::name(candidate.binding.type), candidate.binding.anchors.front().at.track->name().c_str(),
|
||||
std::abs(candidate.offset), candidate.offset >= 0.0 ? "right" : "left", candidate.binding.anchors.front().height, candidate.reason.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s %+.1f m", infra::name(candidate.binding.type), candidate.offset);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::TreePop();
|
||||
ImGui::TextColored(warncolour, STR_C("%d of them nearer to another path"), state.overruled);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(STR_C("By the distance alone these would go to the wrong thread. Check them in 3D: the dashed line goes to the nearer path"));
|
||||
}
|
||||
if (state.unread > 0)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(warncolour, STR_C("%d signal(s) not read by any path"), state.unread);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(STR_C("The signal has a memory cell, but no event of a path reads it: the vehicles won't see it.\nBound to the nearest path"));
|
||||
}
|
||||
static char const *const filters[] = {"All", "Read by a path", "Nearer to another path", "Not read"};
|
||||
for (int i = 0; i < IM_ARRAYSIZE(filters); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(STR_C(filters[i]), &state.filter, i);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true);
|
||||
std::vector<int> rows;
|
||||
for (auto const group : infra::categories())
|
||||
{
|
||||
rows.clear();
|
||||
for (int i = 0; i < static_cast<int>(candidates.size()); ++i)
|
||||
if (candidates[i].binding.group == group && shown(candidates[i]))
|
||||
rows.push_back(i);
|
||||
if (rows.empty())
|
||||
continue;
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(colour(group)));
|
||||
auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast<int>(rows.size())).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)};
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton((std::string{STR_C("all##g")} + infra::name(group)).c_str()))
|
||||
choose(group, true);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton((std::string{STR_C("none##g")} + infra::name(group)).c_str()))
|
||||
choose(group, false);
|
||||
if (false == open)
|
||||
continue;
|
||||
ImGuiListClipper clipper(static_cast<int>(rows.size()));
|
||||
while (clipper.Step())
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
||||
{
|
||||
auto const i{rows[row]};
|
||||
auto &candidate{candidates[i]};
|
||||
auto const &anchor{candidate.binding.anchors.front()};
|
||||
ImGui::PushID(i);
|
||||
if (candidate.bound)
|
||||
ImGui::TextDisabled(STR_C("[bound]"));
|
||||
else
|
||||
ImGui::Checkbox("##chosen", &candidate.chosen);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Selectable(candidate.binding.label.c_str(), false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(candidate.binding.label.c_str()).x, 0.f)) && anchor.at.track != nullptr)
|
||||
focus_track(*anchor.at.track, anchor.at.path, anchor.foot);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
state.hovered = i;
|
||||
std::string text{format(STR_C("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s"), infra::name(candidate.binding.type), anchor.at.track->name().c_str(), std::abs(candidate.offset),
|
||||
candidate.offset >= 0.0 ? "right" : "left", anchor.height, candidate.reason.c_str())};
|
||||
if (candidate.reader != nullptr)
|
||||
text += format(STR_C("\nRead by the path %s: event %s, getvalues of the memory cell %s"), candidate.reader->name().c_str(), candidate.event.c_str(), candidate.cell.c_str()) +
|
||||
(candidate.loose ? STR(" (written loose, the path takes it by the name)") : std::string{});
|
||||
if (candidate.nearer != nullptr)
|
||||
text += format(STR_C("\nThe path %s is nearer, but it doesn't read the object"), candidate.nearer->name().c_str());
|
||||
if (candidate.unread)
|
||||
text += STR("\nNo path reads the memory cell of the signal");
|
||||
text += STR("\nClick to look at it");
|
||||
ImGui::SetTooltip("%s", text.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s %+.1f m", infra::name(candidate.binding.type), candidate.offset);
|
||||
if (candidate.reader != nullptr)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(readcolour, STR_C("read by %s"), candidate.reader->name().c_str());
|
||||
}
|
||||
if (candidate.nearer != nullptr)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(warncolour, STR_C("(nearer: %s)"), candidate.nearer->name().c_str());
|
||||
}
|
||||
if (candidate.unread)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(warncolour, "%s", STR_C("not read"));
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void editor_mode::render_infra_bound()
|
||||
@@ -1369,11 +1652,53 @@ void editor_mode::draw_infra_overlay() const
|
||||
};
|
||||
for (auto const &binding : m_bindings)
|
||||
draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f);
|
||||
auto const object = [&](infra_candidate const &Candidate) {
|
||||
std::vector<glm::dvec3> points;
|
||||
std::optional<double> yaw;
|
||||
return object_points(Candidate.binding, points, yaw) ? points.front() : Candidate.binding.anchors.front().foot;
|
||||
};
|
||||
auto const dashed = [&](glm::dvec3 const &From, glm::dvec3 const &To, ImU32 const Colour) {
|
||||
auto const steps{std::clamp(static_cast<int>(glm::distance(From, To) / 0.5), 2, 40)};
|
||||
for (int k = 0; k < steps; k += 2)
|
||||
projection.line(drawlist, glm::mix(From, To, static_cast<double>(k) / steps), glm::mix(From, To, static_cast<double>(k + 1) / steps), Colour, 2.f);
|
||||
};
|
||||
for (int i = 0; i < static_cast<int>(state.candidates.size()); ++i)
|
||||
{
|
||||
auto const &candidate{state.candidates[i]};
|
||||
if (candidate.nearer != nullptr && alive(candidate.binding) && glm::distance(candidate.nearer_foot, camera) <= 2000.0)
|
||||
{
|
||||
auto const point{object(candidate)};
|
||||
ImVec2 screen;
|
||||
if (projection.project(point, screen))
|
||||
drawlist->AddCircle(screen, i == state.hovered ? 13.f : 8.f, IM_COL32(255, 190, 70, 230), 16, 2.f);
|
||||
if (i == state.hovered)
|
||||
dashed(point, candidate.nearer_foot, IM_COL32(255, 190, 70, 230));
|
||||
}
|
||||
if (candidate.bound)
|
||||
continue;
|
||||
draw(candidate.binding, colour(candidate.binding.group, candidate.chosen ? 255 : 110), i == state.hovered ? 9.f : (candidate.chosen ? 5.f : 3.f));
|
||||
}
|
||||
if (state.hovered >= 0 && state.hovered < static_cast<int>(state.candidates.size()))
|
||||
{
|
||||
auto const &candidate{state.candidates[state.hovered]};
|
||||
auto const outline = [&](TTrack const *Track, ImU32 const Colour, float const Width) {
|
||||
if (Track == nullptr || Track->m_editorremoved)
|
||||
return;
|
||||
for (auto const &path : Track->m_paths)
|
||||
{
|
||||
bezier const curve{path};
|
||||
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 3.0), 6, 64)};
|
||||
auto previous{curve.point(0.0)};
|
||||
for (int k = 1; k <= count; ++k)
|
||||
{
|
||||
auto const next{curve.point(static_cast<double>(k) / count)};
|
||||
projection.line(drawlist, previous, next, Colour, Width);
|
||||
previous = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
outline(candidate.binding.anchors.front().at.track, IM_COL32(255, 255, 255, 200), 3.f);
|
||||
if (candidate.reader != candidate.binding.anchors.front().at.track)
|
||||
outline(candidate.reader, IM_COL32(110, 215, 255, 220), 3.f);
|
||||
}
|
||||
}
|
||||
|
||||
521
application/editormodejoints.cpp
Normal file
521
application/editormodejoints.cpp
Normal file
@@ -0,0 +1,521 @@
|
||||
/*
|
||||
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/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#include "application/editorprojection.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
#include "editor/editorGeometry.hpp"
|
||||
|
||||
#include "scene/scene.h"
|
||||
#include "scene/scenelayers.h"
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "utilities/translation.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <tuple>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using geometry::grade_of;
|
||||
using geometry::plan_of;
|
||||
|
||||
double const kLinked{0.02}; // m, ends this close are joined by the simulation
|
||||
double const kOverlayRange{3000.0};
|
||||
double const kGauge{1.5}; // m between the rails, for the cant
|
||||
int const kRailCategory{1};
|
||||
|
||||
using kind = editor_mode::joint_issue::kind;
|
||||
|
||||
bool rail(TTrack const &Track)
|
||||
{
|
||||
return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && editor_track::is_supported(Track);
|
||||
}
|
||||
|
||||
bool at_start(editor_track::point_ref const &Point)
|
||||
{
|
||||
return Point.kind == editor_track::point_kind::start;
|
||||
}
|
||||
|
||||
// direction of the path at its end, into the path
|
||||
glm::dvec3 into(TTrack const &Track, editor_track::point_ref const &Point)
|
||||
{
|
||||
auto const &path{Track.m_paths[Point.path]};
|
||||
bool const start{at_start(Point)};
|
||||
auto const &point{path.points[start ? segment_data::point::start : segment_data::point::end]};
|
||||
auto const &control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
|
||||
glm::dvec3 const direction{control != glm::dvec3{} ? control : path.points[start ? segment_data::point::end : segment_data::point::start] - point};
|
||||
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec3{0.0, 0.0, 1.0};
|
||||
}
|
||||
|
||||
// roll of the path at its end, for the travel into the path, in degrees
|
||||
double roll_into(TTrack const &Track, editor_track::point_ref const &Point)
|
||||
{
|
||||
auto const &path{Track.m_paths[Point.path]};
|
||||
return at_start(Point) ? path.rolls[0] : -path.rolls[1];
|
||||
}
|
||||
|
||||
double plan_angle(glm::dvec3 const &A, glm::dvec3 const &B)
|
||||
{
|
||||
auto const a{plan_of(A)};
|
||||
auto const b{plan_of(B)};
|
||||
if (glm::length(a) < 1e-9 || glm::length(b) < 1e-9)
|
||||
return 0.0;
|
||||
return glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(a), glm::normalize(b)), -1.0, 1.0)));
|
||||
}
|
||||
|
||||
using end_key = std::tuple<TTrack const *, int, int>;
|
||||
|
||||
end_key key_of(TTrack const *Track, editor_track::point_ref const &Point)
|
||||
{
|
||||
return {Track, Point.path, static_cast<int>(Point.kind)};
|
||||
}
|
||||
|
||||
// pairs of the ends already looked at in the check under way
|
||||
std::set<std::pair<end_key, end_key>> reported;
|
||||
|
||||
char const *kind_name(kind const Kind)
|
||||
{
|
||||
switch (Kind)
|
||||
{
|
||||
case kind::gap: return "gap";
|
||||
case kind::step: return "step";
|
||||
case kind::kink: return "kink";
|
||||
case kind::cant: return "cant jump";
|
||||
case kind::grade: return "grade break";
|
||||
default: return "free end";
|
||||
}
|
||||
}
|
||||
|
||||
std::string value_text(editor_mode::joint_issue const &Issue)
|
||||
{
|
||||
switch (Issue.type)
|
||||
{
|
||||
case kind::gap: return format("%.3f m", Issue.value);
|
||||
case kind::step: return format("%.1f mm", Issue.value * 1000.0);
|
||||
case kind::kink: return format("%.3f deg", Issue.value);
|
||||
case kind::cant: return format("%.1f mm", Issue.value);
|
||||
case kind::grade: return format("%.1f %%o", Issue.value);
|
||||
default: return "-";
|
||||
}
|
||||
}
|
||||
|
||||
ImU32 kind_colour(kind const Kind)
|
||||
{
|
||||
switch (Kind)
|
||||
{
|
||||
case kind::gap: return IM_COL32(255, 70, 60, 235);
|
||||
case kind::step: return IM_COL32(255, 140, 50, 230);
|
||||
case kind::kink: return IM_COL32(255, 190, 60, 225);
|
||||
case kind::cant: return IM_COL32(200, 120, 255, 225);
|
||||
case kind::grade: return IM_COL32(90, 200, 255, 220);
|
||||
default: return IM_COL32(200, 200, 200, 200);
|
||||
}
|
||||
}
|
||||
|
||||
std::string name_of(TTrack const *Track)
|
||||
{
|
||||
if (Track == nullptr)
|
||||
return "-";
|
||||
return Track->name().empty() ? std::string{"(noname)"} : Track->name();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void editor_mode::joints_start()
|
||||
{
|
||||
auto &state{m_joints};
|
||||
state.queue.clear();
|
||||
state.next = 0;
|
||||
state.issues.clear();
|
||||
state.selected = -1;
|
||||
state.status.clear();
|
||||
reported.clear();
|
||||
glm::dvec3 const camera{Global.pCamera.Pos};
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
{
|
||||
if (track == nullptr || false == rail(*track))
|
||||
continue;
|
||||
if (state.scope == 0 && glm::distance(track->location(), camera) > state.reach)
|
||||
continue;
|
||||
state.queue.push_back(track);
|
||||
}
|
||||
state.history = m_history.size();
|
||||
}
|
||||
|
||||
void editor_mode::joints_check(TTrack &Track)
|
||||
{
|
||||
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; };
|
||||
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
|
||||
{
|
||||
for (auto const kind_of_end : {editor_track::point_kind::start, editor_track::point_kind::end})
|
||||
{
|
||||
editor_track::point_ref const point{i, kind_of_end};
|
||||
auto const position{editor_track::point_position(Track, point)};
|
||||
auto const connections{editor_track::connected_points(Track, position)};
|
||||
if (connections.empty())
|
||||
{
|
||||
joint_issue nearest;
|
||||
nearest.value = std::numeric_limits<double>::max();
|
||||
for (auto *section : simulation::Region->sections(position, static_cast<float>(state.gap)))
|
||||
for (auto const &cell : section->m_cells)
|
||||
for (auto *other : cell.m_directories.paths)
|
||||
{
|
||||
if (other == nullptr || other == &Track || false == rail(*other))
|
||||
continue;
|
||||
for (int k = 0; k < static_cast<int>(other->m_paths.size()); ++k)
|
||||
for (auto const theirkind : {editor_track::point_kind::start, editor_track::point_kind::end})
|
||||
{
|
||||
editor_track::point_ref const theirs{k, theirkind};
|
||||
auto const distance{glm::distance(position, editor_track::point_position(*other, theirs))};
|
||||
if (distance > state.gap || distance >= nearest.value)
|
||||
continue;
|
||||
nearest.track = &Track;
|
||||
nearest.point = point;
|
||||
nearest.other = other;
|
||||
nearest.theirs = theirs;
|
||||
nearest.value = distance;
|
||||
}
|
||||
}
|
||||
if (nearest.other != nullptr)
|
||||
{
|
||||
if (once(key_of(&Track, point), key_of(nearest.other, nearest.theirs)))
|
||||
{
|
||||
nearest.type = kind::gap;
|
||||
nearest.position = position;
|
||||
state.issues.push_back(nearest);
|
||||
}
|
||||
}
|
||||
else if (state.free_ends)
|
||||
{
|
||||
joint_issue open;
|
||||
open.type = kind::open;
|
||||
open.track = &Track;
|
||||
open.point = point;
|
||||
open.position = position;
|
||||
state.issues.push_back(open);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (auto const &connection : connections)
|
||||
{
|
||||
auto *other{connection.first};
|
||||
auto const &theirs{connection.second};
|
||||
if (false == rail(*other) || false == once(key_of(&Track, point), key_of(other, theirs)))
|
||||
continue;
|
||||
joint_issue issue;
|
||||
issue.track = &Track;
|
||||
issue.point = point;
|
||||
issue.other = other;
|
||||
issue.theirs = theirs;
|
||||
issue.position = position;
|
||||
auto const step{glm::distance(position, editor_track::point_position(*other, theirs))};
|
||||
auto const ours{into(Track, point)};
|
||||
auto const their{into(*other, theirs)};
|
||||
auto const angle{plan_angle(ours, -their)};
|
||||
auto const cant{kGauge * 1000.0 * std::abs(std::sin(glm::radians(roll_into(Track, point) + roll_into(*other, theirs))))};
|
||||
auto const grade{std::abs(grade_of(ours) + grade_of(their)) * 1000.0};
|
||||
auto const add = [&](kind const Kind, double const Value) {
|
||||
issue.type = Kind;
|
||||
issue.value = Value;
|
||||
state.issues.push_back(issue);
|
||||
};
|
||||
if (step > state.step)
|
||||
add(kind::step, step);
|
||||
if (angle > state.angle)
|
||||
add(kind::kink, angle);
|
||||
if (cant > state.cant)
|
||||
add(kind::cant, cant);
|
||||
if (grade > state.grade)
|
||||
add(kind::grade, grade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::joints_step()
|
||||
{
|
||||
auto &state{m_joints};
|
||||
if (state.next >= state.queue.size())
|
||||
return;
|
||||
auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)};
|
||||
while (state.next < state.queue.size() && std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
auto *track{state.queue[state.next++]};
|
||||
if (track->m_editorremoved)
|
||||
continue;
|
||||
joints_check(*track);
|
||||
}
|
||||
if (state.next < state.queue.size())
|
||||
return;
|
||||
std::stable_sort(state.issues.begin(), state.issues.end(), [](joint_issue const &A, joint_issue const &B) {
|
||||
if (A.type != B.type)
|
||||
return A.type < B.type;
|
||||
return A.value > B.value;
|
||||
});
|
||||
std::array<int, 6> counts{};
|
||||
for (auto const &issue : state.issues)
|
||||
++counts[static_cast<int>(issue.type)];
|
||||
state.status = format(STR_C("%zu paths checked: %d gaps, %d steps, %d kinks, %d cant jumps, %d grade breaks"), state.queue.size(), counts[0], counts[1], counts[2], counts[3], counts[4]);
|
||||
if (state.free_ends)
|
||||
state.status += format(STR_C(", %d free ends"), counts[5]);
|
||||
WriteLog("Editor: joints check - " + state.status, logtype::generic);
|
||||
}
|
||||
|
||||
void editor_mode::joints_focus(int const Index)
|
||||
{
|
||||
auto &state{m_joints};
|
||||
if (Index < 0 || Index >= static_cast<int>(state.issues.size()))
|
||||
return;
|
||||
state.selected = Index;
|
||||
auto const &issue{state.issues[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);
|
||||
}
|
||||
|
||||
bool editor_mode::joint_fix(joint_issue &Issue, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Changed)
|
||||
{
|
||||
if (Issue.fixed || Issue.type == kind::open || Issue.track == nullptr || Issue.other == nullptr || Issue.track->m_editorremoved || Issue.other->m_editorremoved)
|
||||
return false;
|
||||
auto const movable = [](TTrack const *Track) {
|
||||
std::string reason;
|
||||
return Track->eType == tt_Normal && editor_track::can_edit_geometry(*Track, reason) && scene::Layers.editable(Track);
|
||||
};
|
||||
TTrack *mover{nullptr};
|
||||
editor_track::point_ref moved;
|
||||
TTrack *fixed{nullptr};
|
||||
editor_track::point_ref held;
|
||||
if (movable(Issue.track))
|
||||
{
|
||||
mover = Issue.track;
|
||||
moved = Issue.point;
|
||||
fixed = Issue.other;
|
||||
held = Issue.theirs;
|
||||
}
|
||||
else if (movable(Issue.other) && (Issue.type != kind::gap || false == editor_track::is_connected(*Issue.other, Issue.theirs)))
|
||||
{
|
||||
mover = Issue.other;
|
||||
moved = Issue.theirs;
|
||||
fixed = Issue.track;
|
||||
held = Issue.point;
|
||||
}
|
||||
if (mover == nullptr)
|
||||
return false;
|
||||
if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == mover; }))
|
||||
States.emplace_back(mover, editor_track::capture(*mover));
|
||||
if (std::find(Changed.begin(), Changed.end(), mover) == Changed.end())
|
||||
Changed.push_back(mover);
|
||||
editor_track::snap_target target;
|
||||
target.track = fixed;
|
||||
target.point = held;
|
||||
target.position = editor_track::point_position(*fixed, held);
|
||||
target.direction = into(*fixed, held);
|
||||
switch (Issue.type)
|
||||
{
|
||||
case kind::gap:
|
||||
case kind::kink:
|
||||
case kind::grade:
|
||||
editor_track::snap_point(*mover, moved, target, true);
|
||||
break;
|
||||
case kind::step:
|
||||
editor_track::snap_point(*mover, moved, target, false);
|
||||
break;
|
||||
case kind::cant:
|
||||
{
|
||||
auto &path{mover->m_paths[moved.path]};
|
||||
auto const roll{static_cast<float>(roll_into(*fixed, held))};
|
||||
if (at_start(moved))
|
||||
path.rolls[0] = -roll;
|
||||
else
|
||||
path.rolls[1] = roll;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
Issue.fixed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void editor_mode::joints_fix(std::vector<int> const &Indices)
|
||||
{
|
||||
auto &state{m_joints};
|
||||
std::vector<std::pair<TTrack *, editor_track::state>> states;
|
||||
std::vector<TTrack *> changed;
|
||||
std::vector<joint_issue *> chosen;
|
||||
for (auto const index : Indices)
|
||||
if (index >= 0 && index < static_cast<int>(state.issues.size()))
|
||||
chosen.push_back(&state.issues[index]);
|
||||
std::size_t count{0};
|
||||
for (auto *issue : chosen)
|
||||
if (joint_fix(*issue, states, changed))
|
||||
++count;
|
||||
if (count == 0)
|
||||
{
|
||||
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));
|
||||
editor_track::commit(changed);
|
||||
for (auto *track : changed)
|
||||
track->mark_dirty();
|
||||
state.history = m_history.size();
|
||||
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()
|
||||
{
|
||||
auto &state{m_joints};
|
||||
ImGui::TextDisabled("%s", STR_C("Ends of the paths which almost meet, steps, kinks,\njumps of the cant and of the grade at the joints"));
|
||||
ImGui::RadioButton(STR_C("Around the camera"), &state.scope, 0);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(STR_C("Whole scenery"), &state.scope, 1);
|
||||
bool const running{state.next < state.queue.size()};
|
||||
if (running)
|
||||
{
|
||||
auto const progress{static_cast<float>(state.next) / static_cast<float>(state.queue.size())};
|
||||
ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Checking %zu / %zu"), state.next, state.queue.size()).c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("Cancel")))
|
||||
state.queue.resize(state.next);
|
||||
}
|
||||
else if (ImGui::Button(STR_C("Check")))
|
||||
{
|
||||
joints_start();
|
||||
}
|
||||
if (ImGui::TreeNode(STR_C("Limits")))
|
||||
{
|
||||
ImGui::PushItemWidth(100.0f);
|
||||
if (state.scope == 0)
|
||||
ImGui::InputDouble(STR_C("Around the camera (m)"), &state.reach, 0.0, 0.0, "%.0f");
|
||||
ImGui::InputDouble(STR_C("Gap up to (m)"), &state.gap, 0.0, 0.0, "%.2f");
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart"));
|
||||
ImGui::InputDouble(STR_C("Step (m)"), &state.step, 0.0, 0.0, "%.3f");
|
||||
ImGui::InputDouble(STR_C("Kink (deg)"), &state.angle, 0.0, 0.0, "%.2f");
|
||||
ImGui::InputDouble(STR_C("Cant jump (mm)"), &state.cant, 0.0, 0.0, "%.1f");
|
||||
ImGui::InputDouble(STR_C("Grade break (per mille)"), &state.grade, 0.0, 0.0, "%.1f");
|
||||
ImGui::Checkbox(STR_C("Free ends too"), &state.free_ends);
|
||||
ImGui::PopItemWidth();
|
||||
state.reach = std::clamp(state.reach, 50.0, 50000.0);
|
||||
state.gap = std::clamp(state.gap, 0.03, 20.0);
|
||||
state.step = std::max(0.001, state.step);
|
||||
state.angle = std::max(0.01, state.angle);
|
||||
state.cant = std::max(0.5, state.cant);
|
||||
state.grade = std::max(0.5, state.grade);
|
||||
ImGui::TextDisabled("%s", STR_C("Change the limits, then Check again"));
|
||||
ImGui::TreePop();
|
||||
}
|
||||
if (false == state.status.empty())
|
||||
ImGui::TextWrapped("%s", state.status.c_str());
|
||||
if (state.issues.empty())
|
||||
return;
|
||||
if (state.history != m_history.size())
|
||||
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);
|
||||
auto const filters{std::string{STR_C("All")} + '\0' + STR_C("gap") + '\0' + STR_C("step") + '\0' + STR_C("kink") + '\0' + STR_C("cant jump") + '\0' + STR_C("grade break") + '\0' + STR_C("free end") + '\0' + '\0'};
|
||||
if (ImGui::Combo("##jointfilter", &filter, filters.c_str()))
|
||||
state.filter = filter - 1;
|
||||
std::vector<int> listed;
|
||||
std::vector<int> fixable;
|
||||
for (int i = 0; i < static_cast<int>(state.issues.size()); ++i)
|
||||
{
|
||||
auto const &issue{state.issues[i]};
|
||||
if (state.filter >= 0 && static_cast<int>(issue.type) != state.filter)
|
||||
continue;
|
||||
listed.push_back(i);
|
||||
if (false == issue.fixed && issue.type != kind::open)
|
||||
fixable.push_back(i);
|
||||
}
|
||||
if (false == fixable.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(format(STR_C("Fix %zu listed"), fixable.size()).c_str()))
|
||||
joints_fix(fixable);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("Gaps: the free end goes to the other end, along it. Steps: the end goes to the other one.\n"
|
||||
"Kinks and grade breaks: the end is turned in line with the other path. Cant: the cant of the other path.\n"
|
||||
"The plain path moves, a switch stays as it is. One Ctrl+Z takes it all back"));
|
||||
}
|
||||
auto const namewidth{std::max(110.0f, ImGui::GetContentRegionAvail().x * 0.32f)};
|
||||
bool const chosen{state.selected >= 0 && state.selected < static_cast<int>(state.issues.size())};
|
||||
auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 2.0f : 0.0f};
|
||||
ImGui::BeginChild("jointresults", ImVec2(0.0f, -details), true);
|
||||
ImGui::Columns(4, "jointrows", false);
|
||||
ImGui::SetColumnWidth(0, 90.0f);
|
||||
ImGui::SetColumnWidth(1, 80.0f);
|
||||
ImGui::SetColumnWidth(2, namewidth);
|
||||
ImGuiListClipper clipper(static_cast<int>(listed.size()));
|
||||
while (clipper.Step())
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
||||
{
|
||||
auto const index{listed[row]};
|
||||
auto const &issue{state.issues[index]};
|
||||
auto const colour{ImGui::ColorConvertU32ToFloat4(kind_colour(issue.type))};
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, issue.fixed ? ImVec4(0.5f, 0.8f, 0.5f, 1.0f) : colour);
|
||||
if (ImGui::Selectable(format("%s##joint%d", STR_C(kind_name(issue.type)), index).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns))
|
||||
joints_focus(index);
|
||||
ImGui::PopStyleColor();
|
||||
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::NextColumn();
|
||||
ImGui::TextUnformatted(name_of(issue.other).c_str());
|
||||
ImGui::NextColumn();
|
||||
}
|
||||
ImGui::Columns(1);
|
||||
ImGui::EndChild();
|
||||
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());
|
||||
if (false == issue.fixed && issue.type != kind::open && ImGui::Button(STR_C("Fix this one")))
|
||||
joints_fix({state.selected});
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::draw_joints_overlay() const
|
||||
{
|
||||
auto const &state{m_joints};
|
||||
if (false == state.open || state.issues.empty())
|
||||
return;
|
||||
screen_projection const projection;
|
||||
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
||||
glm::dvec3 const camera{Global.pCamera.Pos};
|
||||
for (int i = 0; i < static_cast<int>(state.issues.size()); ++i)
|
||||
{
|
||||
auto const &issue{state.issues[i]};
|
||||
if (issue.fixed || (state.filter >= 0 && static_cast<int>(issue.type) != state.filter) || glm::distance(issue.position, camera) > kOverlayRange)
|
||||
continue;
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(issue.position, screen))
|
||||
continue;
|
||||
auto const colour{i == state.selected ? IM_COL32(255, 255, 80, 255) : kind_colour(issue.type)};
|
||||
drawlist->AddCircle(screen, 11.0f, colour, 16, 2.5f);
|
||||
auto const text{std::string{STR_C(kind_name(issue.type))} + " " + value_text(issue)};
|
||||
drawlist->AddText(ImVec2(screen.x + 13.0f, screen.y - 7.0f), colour, text.c_str());
|
||||
}
|
||||
}
|
||||
983
application/editormodelineside.cpp
Normal file
983
application/editormodelineside.cpp
Normal file
@@ -0,0 +1,983 @@
|
||||
/*
|
||||
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/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#include "application/editorprojection.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
#include "editor/editorGeometry.hpp"
|
||||
#include "editor/editorIncludeInfo.hpp"
|
||||
|
||||
#include "scene/scene.h"
|
||||
#include "scene/scenelayers.h"
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/utilities.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "utilities/translation.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using geometry::bezier;
|
||||
using geometry::plan_distance;
|
||||
using geometry::plan_of;
|
||||
|
||||
double const kSampleStep{2.0}; // m between the samples of the line
|
||||
double const kLineCorridor{15.0}; // m from the axis, posts farther away belong to another line
|
||||
double const kOverlayRange{3000.0};
|
||||
double const kFoulingTrace{250.0}; // m from the start of a switch the tracks are followed for
|
||||
double const kRailHeight{0.18}; // m from the points of a track to the heads of its rails
|
||||
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<char>(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)};
|
||||
if (text == "-0" || text.find_first_not_of("-0.") == std::string::npos)
|
||||
text.erase(0, text.front() == '-' ? 1 : 0);
|
||||
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<editor_track::route_sample> const &Samples, double const Chainage)
|
||||
{
|
||||
auto const found{std::upper_bound(Samples.begin(), Samples.end(), Chainage, [](double const Value, editor_track::route_sample const &Sample) { return Value < Sample.chainage; })};
|
||||
return found == Samples.begin() ? 0 : std::min<std::size_t>(std::distance(Samples.begin(), found) - 1, Samples.size() - 2);
|
||||
}
|
||||
|
||||
glm::dvec2 sampled_direction(std::vector<editor_track::route_sample> const &Samples, double const Chainage)
|
||||
{
|
||||
auto const index{sample_index(Samples, Chainage)};
|
||||
auto const &a{Samples[index]};
|
||||
auto const &b{Samples[index + 1]};
|
||||
auto const span{b.chainage - a.chainage};
|
||||
auto const f{span > 1e-9 ? std::clamp((Chainage - a.chainage) / span, 0.0, 1.0) : 0.0};
|
||||
auto const direction{a.direction * (1.0 - f) + b.direction * f};
|
||||
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0};
|
||||
}
|
||||
|
||||
// chainage of the point of the line nearest to the point, and how far from the line it is
|
||||
double project(std::vector<editor_track::route_sample> const &Samples, glm::dvec3 const &Point, double &Distance)
|
||||
{
|
||||
Distance = std::numeric_limits<double>::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 b{plan_of(Samples[i].position)};
|
||||
auto const ab{b - 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};
|
||||
auto const distance{glm::length(point - (a + ab * t))};
|
||||
if (distance < Distance)
|
||||
{
|
||||
Distance = distance;
|
||||
chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t;
|
||||
}
|
||||
}
|
||||
return chainage;
|
||||
}
|
||||
|
||||
// the digits after the rotation make the number of the post, the last one the hectometre
|
||||
bool hectometres_of(std::vector<std::string> 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<int>(digit);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string hekto_file(int const Hectometres, int const Style)
|
||||
{
|
||||
auto style{Style};
|
||||
if (style > 2)
|
||||
{
|
||||
auto const pick{LocalRandom(0.0, 1.0)};
|
||||
style = pick < 0.6 ? 0 : pick < 0.9 ? 1 : 2;
|
||||
}
|
||||
auto const size{Hectometres < 100 ? "1" : Hectometres < 1000 ? "10" : "100"};
|
||||
return std::string{"przytorowe/slupek_hekto_"} + kHektoStyles[style] + "_" + size + ".inc";
|
||||
}
|
||||
|
||||
std::vector<std::string> hekto_digits(int const Hectometres)
|
||||
{
|
||||
return {std::to_string((Hectometres / 1000) % 10), std::to_string((Hectometres / 100) % 10), std::to_string((Hectometres / 10) % 10), std::to_string(Hectometres % 10)};
|
||||
}
|
||||
|
||||
std::string km_text(int const Hectometres)
|
||||
{
|
||||
return format("%d.%d", Hectometres / 10, Hectometres % 10);
|
||||
}
|
||||
|
||||
// points along the track from the end of a path on, the way a train goes: through a switch along its main track
|
||||
std::vector<glm::dvec3> trace(TTrack &Track, int const Path, bool const Forward, double const Limit)
|
||||
{
|
||||
std::vector<glm::dvec3> points;
|
||||
auto *track{&Track};
|
||||
auto path{Path};
|
||||
auto forward{Forward};
|
||||
double length{0.0};
|
||||
for (int guard = 0; guard < 64 && track != nullptr && length < Limit; ++guard)
|
||||
{
|
||||
bezier const curve{track->m_paths[path]};
|
||||
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 1.0), 4, 400)};
|
||||
for (int k = points.empty() ? 0 : 1; k <= count; ++k)
|
||||
{
|
||||
auto const point{curve.point(forward ? static_cast<double>(k) / count : 1.0 - static_cast<double>(k) / count)};
|
||||
if (false == points.empty())
|
||||
length += plan_distance(points.back(), point);
|
||||
points.push_back(point);
|
||||
}
|
||||
auto const exit{points.back()};
|
||||
TTrack *next{nullptr};
|
||||
int nextpath{0};
|
||||
bool nextforward{true};
|
||||
for (auto const &connection : editor_track::connected_points(*track, exit))
|
||||
{
|
||||
auto *other{connection.first};
|
||||
if (other == track || (other->iCategoryFlag & 15) != kRailCategory)
|
||||
continue;
|
||||
bool const start{connection.second.kind == editor_track::point_kind::start};
|
||||
// entering a switch at its start the train takes the main track
|
||||
if (next == nullptr || (other == next && start && connection.second.path == 0))
|
||||
{
|
||||
next = other;
|
||||
nextpath = connection.second.path;
|
||||
nextforward = start;
|
||||
}
|
||||
}
|
||||
track = next;
|
||||
path = nextpath;
|
||||
forward = nextforward;
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// nearest point of the polyline
|
||||
glm::dvec3 nearest_on(std::vector<glm::dvec3> const &Line, glm::dvec3 const &Point, double &Distance)
|
||||
{
|
||||
Distance = std::numeric_limits<double>::max();
|
||||
glm::dvec3 result{Point};
|
||||
auto const point{plan_of(Point)};
|
||||
for (std::size_t i = 1; i < Line.size(); ++i)
|
||||
{
|
||||
auto const a{plan_of(Line[i - 1])};
|
||||
auto const ab{plan_of(Line[i]) - 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};
|
||||
auto const distance{glm::length(point - (a + ab * t))};
|
||||
if (distance < Distance)
|
||||
{
|
||||
Distance = distance;
|
||||
result = glm::mix(Line[i - 1], Line[i], t);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void draw_label(ImDrawList *Draw, ImVec2 const At, ImU32 const Colour, std::string const &Text)
|
||||
{
|
||||
auto const size{ImGui::CalcTextSize(Text.c_str())};
|
||||
Draw->AddRectFilled(ImVec2(At.x + 8.0f, At.y - 8.0f), ImVec2(At.x + 14.0f + size.x, At.y - 6.0f + size.y), IM_COL32(0, 0, 0, 150), 3.0f);
|
||||
Draw->AddText(ImVec2(At.x + 11.0f, At.y - 7.0f), Colour, Text.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<editor_mode::standing_template> editor_mode::standing_templates(std::function<bool(std::string const &)> const &Accept) const
|
||||
{
|
||||
std::vector<standing_template> result;
|
||||
for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle)
|
||||
{
|
||||
if (false == scene::Layers.tracked(handle))
|
||||
continue;
|
||||
auto const &included{scene::Layers.instance(handle)};
|
||||
if (included.removed || included.dead || included.file == nullptr)
|
||||
continue;
|
||||
auto const file{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;
|
||||
std::vector<std::string> values;
|
||||
if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || values.size() < 5)
|
||||
continue;
|
||||
glm::dvec3 location;
|
||||
double yaw;
|
||||
if (false == numeric(values[1], location.x) || false == numeric(values[2], location.y) || false == numeric(values[3], location.z) || false == numeric(values[4], yaw))
|
||||
continue;
|
||||
standing_template standing;
|
||||
standing.instance = handle;
|
||||
standing.file = file;
|
||||
standing.location = included.context.offset + location;
|
||||
standing.yaw = yaw;
|
||||
standing.values = std::move(values);
|
||||
result.push_back(std::move(standing));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::size_t editor_mode::place_templates(std::vector<template_item> const &Items, std::string &Error)
|
||||
{
|
||||
if (Items.empty())
|
||||
return 0;
|
||||
if (scene::Layers.empty())
|
||||
{
|
||||
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())};
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
auto const context{scene::Layers.layer(layer).context_insert()};
|
||||
if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f})
|
||||
{
|
||||
Error = STR_C("The file of the active layer would receive them with a rotation or a scale in effect, pick another layer");
|
||||
return 0;
|
||||
}
|
||||
EditorSnapshot snap;
|
||||
for (auto const &item : Items)
|
||||
{
|
||||
auto const local{context.to_local(item.location)};
|
||||
std::string directive;
|
||||
if (item.described)
|
||||
{
|
||||
include_info info;
|
||||
if (false == editor_includes::load(item.file, info, Error))
|
||||
continue;
|
||||
directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast<float>(item.yaw), item.track);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<std::string> values{"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);
|
||||
}
|
||||
auto const instance{scene::Layers.place(layer, item.file, directive)};
|
||||
if (instance == 0)
|
||||
continue;
|
||||
simulation::State.preview_include(directive, context, layer, instance);
|
||||
snap.instances.push_back(instance);
|
||||
}
|
||||
if (snap.instances.empty())
|
||||
{
|
||||
Error = STR_C("The active layer can't take them");
|
||||
return 0;
|
||||
}
|
||||
auto const count{snap.instances.size()};
|
||||
snap.node_name = Items.front().file;
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
std::size_t editor_mode::move_templates(std::vector<std::pair<standing_template, template_item>> const &Moves)
|
||||
{
|
||||
EditorSnapshot snap;
|
||||
for (auto const &[standing, item] : Moves)
|
||||
{
|
||||
if (false == scene::Layers.tracked(standing.instance) || false == scene::Layers.removable(standing.instance))
|
||||
continue;
|
||||
auto const &included{scene::Layers.instance(standing.instance)};
|
||||
auto values{standing.values};
|
||||
auto const local{item.location - included.context.offset};
|
||||
values[1] = number(local.x, 3);
|
||||
values[2] = number(local.y, 3);
|
||||
values[3] = number(local.z, 3);
|
||||
values[4] = number(item.yaw, 2);
|
||||
auto before{scene::Layers.directive(standing.instance)};
|
||||
set_include_directive(standing.instance, editor_includes::compose_directive(*included.file, values));
|
||||
snap.directives.emplace_back(standing.instance, std::move(before));
|
||||
}
|
||||
if (snap.directives.empty())
|
||||
return 0;
|
||||
auto const count{snap.directives.size()};
|
||||
snap.node_name = Moves.front().first.file;
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
std::size_t editor_mode::remove_templates(std::vector<scene::instance_handle> const &Instances)
|
||||
{
|
||||
EditorSnapshot snap;
|
||||
for (auto const instance : Instances)
|
||||
{
|
||||
if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance))
|
||||
continue;
|
||||
scene::Layers.removed(instance, true);
|
||||
snap.instances.push_back(instance);
|
||||
}
|
||||
if (snap.instances.empty())
|
||||
return 0;
|
||||
auto const count{snap.instances.size()};
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
select_include(0);
|
||||
return count;
|
||||
}
|
||||
|
||||
void editor_mode::hekto_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;
|
||||
tool.status.clear();
|
||||
}
|
||||
|
||||
void editor_mode::hekto_read_kilometrage()
|
||||
{
|
||||
auto &tool{m_hekto};
|
||||
auto const &found{tool.found};
|
||||
if (found.empty())
|
||||
return;
|
||||
if (found.size() == 1)
|
||||
{
|
||||
// one post doesn't tell the way the kilometrage grows: it's taken to grow along the longer part of the line past the post
|
||||
auto const length{tool.samples.empty() ? 0.0 : tool.samples.back().chainage};
|
||||
tool.growth = found.front().chainage > 0.5 * length ? -1 : 1;
|
||||
tool.km = found.front().hectometres / 10.0 + tool.growth * (tool.origin - found.front().chainage) / 1000.0;
|
||||
tool.status = format(STR_C("Kilometrage read from the post %s, check which way it grows"), km_text(found.front().hectometres).c_str());
|
||||
return;
|
||||
}
|
||||
// a straight line through the numbers of the posts against where they stand
|
||||
double sc{0.0}, sk{0.0}, scc{0.0}, sck{0.0};
|
||||
for (auto const &post : found)
|
||||
{
|
||||
auto const km{post.hectometres / 10.0};
|
||||
sc += post.chainage;
|
||||
sk += km;
|
||||
scc += post.chainage * post.chainage;
|
||||
sck += post.chainage * km;
|
||||
}
|
||||
auto const n{static_cast<double>(found.size())};
|
||||
auto const denominator{n * scc - sc * sc};
|
||||
if (std::abs(denominator) < 1e-9)
|
||||
return;
|
||||
auto const slope{(n * sck - sc * sk) / denominator};
|
||||
auto const intercept{(sk - slope * sc) / n};
|
||||
if (std::abs(std::abs(slope) * 1000.0 - 1.0) > 0.2)
|
||||
{
|
||||
tool.status = format(STR_C("The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them"), found.size());
|
||||
return;
|
||||
}
|
||||
tool.growth = slope > 0.0 ? 1 : -1;
|
||||
tool.km = std::round((intercept + slope * tool.origin) * 1000.0) / 1000.0;
|
||||
tool.status = format(STR_C("Kilometrage read from %zu posts standing along the line"), found.size());
|
||||
}
|
||||
|
||||
void editor_mode::hekto_update()
|
||||
{
|
||||
auto &tool{m_hekto};
|
||||
if (false == tool.dirty && tool.history == m_history.size())
|
||||
return;
|
||||
tool.dirty = false;
|
||||
tool.history = m_history.size();
|
||||
tool.posts.clear();
|
||||
tool.found.clear();
|
||||
tool.error.clear();
|
||||
tool.samples.clear();
|
||||
if (tool.track == nullptr || tool.track->m_editorremoved)
|
||||
{
|
||||
tool.track = nullptr;
|
||||
return;
|
||||
}
|
||||
tool.route = editor_track::run_route(*tool.track, tool.reach);
|
||||
if (tool.route.spans.empty())
|
||||
{
|
||||
tool.error = STR_C("No line through this path");
|
||||
return;
|
||||
}
|
||||
tool.samples = editor_track::sample_route(tool.route, kSampleStep);
|
||||
if (tool.samples.size() < 2)
|
||||
{
|
||||
tool.error = STR_C("No line through this path");
|
||||
return;
|
||||
}
|
||||
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; }))
|
||||
{
|
||||
hekto_found post;
|
||||
post.post = standing;
|
||||
if (false == hectometres_of(standing.values, post.hectometres))
|
||||
continue;
|
||||
post.chainage = project(tool.samples, standing.location, distance);
|
||||
if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length)
|
||||
continue;
|
||||
tool.found.push_back(post);
|
||||
}
|
||||
if (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<int>(std::ceil(std::min(km_at(0.0), km_at(length)) * 10.0 - 1e-6)))};
|
||||
auto const high{static_cast<int>(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<double>(tool.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 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<double>::max()};
|
||||
for (int i = 0; i < static_cast<int>(tool.found.size()); ++i)
|
||||
{
|
||||
auto &found{tool.found[i]};
|
||||
if (found.hectometres != hectometres)
|
||||
continue;
|
||||
found.outside = false;
|
||||
found.distance = plan_distance(found.post.location, post.position);
|
||||
if (found.distance < best)
|
||||
{
|
||||
best = found.distance;
|
||||
post.standing = i;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(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);
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::hekto_remove_duplicates()
|
||||
{
|
||||
auto &tool{m_hekto};
|
||||
hekto_update();
|
||||
std::vector<scene::instance_handle> instances;
|
||||
for (auto const &found : tool.found)
|
||||
if (found.duplicate)
|
||||
instances.push_back(found.post.instance);
|
||||
auto const removed{remove_templates(instances)};
|
||||
tool.status = format(STR_C("%zu duplicate hectometre posts removed, Ctrl+Z brings them back"), removed);
|
||||
WriteLog("Editor: " + tool.status);
|
||||
tool.dirty = true;
|
||||
}
|
||||
|
||||
void editor_mode::hekto_place()
|
||||
{
|
||||
auto &tool{m_hekto};
|
||||
hekto_update();
|
||||
std::vector<template_item> items;
|
||||
for (auto const &post : tool.posts)
|
||||
{
|
||||
if (post.standing >= 0)
|
||||
continue;
|
||||
template_item item;
|
||||
item.file = hekto_file(post.hectometres, tool.style);
|
||||
item.location = post.position;
|
||||
item.yaw = post.yaw;
|
||||
item.rest = hekto_digits(post.hectometres);
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
std::string error;
|
||||
auto const placed{place_templates(items, error)};
|
||||
tool.status = placed > 0 ? format(STR_C("%zu hectometre posts placed, Ctrl+Z takes them back"), placed) : error;
|
||||
WriteLog("Editor: " + tool.status, logtype::generic);
|
||||
tool.dirty = true;
|
||||
}
|
||||
|
||||
void editor_mode::hekto_fix()
|
||||
{
|
||||
auto &tool{m_hekto};
|
||||
hekto_update();
|
||||
std::vector<std::pair<standing_template, template_item>> moves;
|
||||
for (auto const &post : tool.posts)
|
||||
{
|
||||
if (post.standing < 0 || false == tool.found[post.standing].wrong)
|
||||
continue;
|
||||
template_item item;
|
||||
item.location = post.position;
|
||||
item.yaw = post.yaw;
|
||||
moves.emplace_back(tool.found[post.standing].post, item);
|
||||
}
|
||||
auto const moved{move_templates(moves)};
|
||||
tool.status = format(STR_C("%zu hectometre posts moved to their places, Ctrl+Z takes it back"), moved);
|
||||
WriteLog("Editor: " + tool.status, logtype::generic);
|
||||
tool.dirty = true;
|
||||
}
|
||||
|
||||
void editor_mode::render_hekto_ui()
|
||||
{
|
||||
auto &tool{m_hekto};
|
||||
auto *selected{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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
auto const changed = [&](bool const Changed) {
|
||||
if (Changed)
|
||||
tool.dirty = true;
|
||||
};
|
||||
ImGui::PushItemWidth(110.0f);
|
||||
changed(ImGui::InputDouble(STR_C("Kilometre at the point (km)"), &tool.km, 0.0, 0.0, "%.3f"));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("Read from the posts")))
|
||||
{
|
||||
tool.km_read = false;
|
||||
tool.dirty = true;
|
||||
}
|
||||
ImGui::TextDisabled("%s", STR_C("The kilometrage grows"));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("along the line"), &tool.growth, 1));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("against it"), &tool.growth, -1));
|
||||
ImGui::TextDisabled("%s", STR_C("Side, looking where it grows"));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("right"), &tool.side, 1));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("left"), &tool.side, -1));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("alternately"), &tool.side, 0));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("The even hectometres on the right, the odd ones on the left"));
|
||||
changed(ImGui::InputDouble(STR_C("From the axis (m)"), &tool.offset, 0.0, 0.0, "%.2f"));
|
||||
tool.offset = std::clamp(tool.offset, 1.5, 30.0);
|
||||
char const *styles[] = {"nowy", "stary", "zgnity", "mixed"};
|
||||
ImGui::Combo(STR_C("Posts"), &tool.style, styles, IM_ARRAYSIZE(styles));
|
||||
char const *turns[] = {"0", "90", "180", "270"};
|
||||
int turn{tool.turn / 90};
|
||||
if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns)))
|
||||
{
|
||||
tool.turn = turn * 90;
|
||||
tool.dirty = true;
|
||||
}
|
||||
changed(ImGui::InputDouble(STR_C("Along the line each way (m)"), &tool.reach, 0.0, 0.0, "%.0f"));
|
||||
tool.reach = std::clamp(tool.reach, 100.0, 100000.0);
|
||||
changed(ImGui::InputDouble(STR_C("A post standing off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f"));
|
||||
tool.tolerance = std::clamp(tool.tolerance, 0.5, 50.0);
|
||||
ImGui::PopItemWidth();
|
||||
hekto_update();
|
||||
if (false == tool.error.empty())
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str());
|
||||
return;
|
||||
}
|
||||
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; })};
|
||||
ImGui::Text(STR_C("Line %.0f m, km %s - %s: %d posts to place, %zu standing, %d of them wrong"), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, tool.posts.empty() ? "-" : km_text(std::min(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(),
|
||||
tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), static_cast<int>(missing), tool.found.size(), static_cast<int>(wrong));
|
||||
if (missing > 0 && ImGui::Button(format(STR_C("Place %d posts"), static_cast<int>(missing)).c_str()))
|
||||
hekto_place();
|
||||
if (duplicates > 0)
|
||||
{
|
||||
if (missing > 0)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(format(STR_C("Remove %d duplicate posts"), static_cast<int>(duplicates)).c_str()))
|
||||
hekto_remove_duplicates();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("Posts with a number another post nearer to its place already has"));
|
||||
}
|
||||
if (wrong > 0 || duplicates > 0)
|
||||
{
|
||||
if (wrong > 0)
|
||||
{
|
||||
if (missing > 0 || duplicates > 0)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(format(STR_C("Move %d wrong posts to their places"), static_cast<int>(wrong)).c_str()))
|
||||
hekto_fix();
|
||||
}
|
||||
ImGui::BeginChild("##hektowrong", ImVec2(0.0f, std::min(6, static_cast<int>(wrong + duplicates)) * ImGui::GetTextLineHeightWithSpacing() + 6.0f), true);
|
||||
for (auto const &post : tool.found)
|
||||
{
|
||||
if (false == post.wrong && false == post.duplicate)
|
||||
continue;
|
||||
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()))
|
||||
{
|
||||
glm::dvec3 const look{glm::normalize(glm::dvec3{-1.0, -0.8, -1.0})};
|
||||
m_focus_start_pos = Camera.Pos;
|
||||
m_focus_start_angle = Camera.Angle;
|
||||
m_focus_target_pos = post.post.location - look * 20.0;
|
||||
m_focus_target_angle = glm::vec3(static_cast<float>(std::asin(look.y)), static_cast<float>(std::atan2(-look.x, -look.z)), 0.0f);
|
||||
m_focus_active = true;
|
||||
m_focus_time = 0.0;
|
||||
m_focus_duration = 0.6;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
if (false == tool.status.empty())
|
||||
ImGui::TextWrapped("%s", tool.status.c_str());
|
||||
}
|
||||
|
||||
void editor_mode::draw_hekto_overlay() const
|
||||
{
|
||||
auto const &tool{m_hekto};
|
||||
if (false == tool.open || tool.track == nullptr)
|
||||
return;
|
||||
screen_projection const projection;
|
||||
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
||||
glm::dvec3 const camera{Global.pCamera.Pos};
|
||||
for (auto const &post : tool.posts)
|
||||
{
|
||||
if (glm::distance(post.position, camera) > kOverlayRange)
|
||||
continue;
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(post.position, screen))
|
||||
continue;
|
||||
bool const standing{post.standing >= 0 && false == tool.found[post.standing].wrong};
|
||||
auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)};
|
||||
drawlist->AddCircleFilled(screen, 4.0f, colour);
|
||||
draw_label(drawlist, screen, colour, km_text(post.hectometres));
|
||||
}
|
||||
for (auto const &post : tool.found)
|
||||
{
|
||||
if ((false == post.wrong && false == post.duplicate) || glm::distance(post.post.location, camera) > kOverlayRange)
|
||||
continue;
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(post.post.location, screen))
|
||||
continue;
|
||||
auto const colour{post.duplicate ? IM_COL32(255, 160, 40, 235) : IM_COL32(255, 70, 60, 235)};
|
||||
drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f);
|
||||
draw_label(drawlist, screen, colour, post.duplicate ? format(STR_C("km %s duplicate"), km_text(post.hectometres).c_str()) : format(STR_C("km %s, %.1f m from its place"), km_text(post.hectometres).c_str(), post.distance));
|
||||
if (post.duplicate)
|
||||
continue;
|
||||
for (auto const &target : tool.posts)
|
||||
if (target.hectometres == post.hectometres)
|
||||
projection.line(drawlist, post.post.location, target.position, IM_COL32(255, 70, 60, 160), 1.5f);
|
||||
}
|
||||
ImVec2 screen;
|
||||
if (projection.project(tool.point, screen))
|
||||
{
|
||||
drawlist->AddCircle(screen, 7.0f, IM_COL32(90, 200, 255, 235), 16, 2.5f);
|
||||
draw_label(drawlist, screen, IM_COL32(90, 200, 255, 235), format("km %.3f", tool.km));
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::fouling_update()
|
||||
{
|
||||
auto &tool{m_fouling};
|
||||
auto *selected{selected_track()};
|
||||
if (tool.scope == 0 && selected != tool.selected)
|
||||
tool.dirty = true;
|
||||
if (false == tool.dirty && tool.history == m_history.size())
|
||||
return;
|
||||
tool.dirty = false;
|
||||
tool.history = m_history.size();
|
||||
tool.selected = selected;
|
||||
tool.points.clear();
|
||||
tool.found.clear();
|
||||
tool.error.clear();
|
||||
std::vector<TTrack *> switches;
|
||||
glm::dvec3 const camera{Global.pCamera.Pos};
|
||||
if (tool.scope == 0)
|
||||
{
|
||||
if (selected == nullptr || selected->eType != tt_Switch)
|
||||
{
|
||||
tool.error = STR_C("Select a switch, or check around the camera");
|
||||
return;
|
||||
}
|
||||
switches.push_back(selected);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
if (track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved && (track->iCategoryFlag & 15) == kRailCategory && track->m_paths.size() >= 2 &&
|
||||
(tool.scope == 2 || glm::distance(track->location(), camera) <= tool.reach))
|
||||
switches.push_back(track);
|
||||
}
|
||||
for (auto *track : switches)
|
||||
{
|
||||
if (track->m_paths.size() < 2 || false == editor_track::is_supported(*track))
|
||||
continue;
|
||||
auto const main{trace(*track, 0, true, kFoulingTrace)};
|
||||
auto const branch{trace(*track, 1, true, kFoulingTrace)};
|
||||
if (main.size() < 2 || branch.size() < 2)
|
||||
continue;
|
||||
glm::dvec3 previous{branch.front()};
|
||||
double previousdistance{0.0};
|
||||
for (std::size_t i = 1; i < branch.size(); ++i)
|
||||
{
|
||||
double distance;
|
||||
nearest_on(main, branch[i], distance);
|
||||
if (distance < tool.spacing)
|
||||
{
|
||||
previous = branch[i];
|
||||
previousdistance = distance;
|
||||
continue;
|
||||
}
|
||||
auto const f{distance - previousdistance > 1e-9 ? (tool.spacing - previousdistance) / (distance - previousdistance) : 1.0};
|
||||
auto const onbranch{glm::mix(previous, branch[i], std::clamp(f, 0.0, 1.0))};
|
||||
double unused;
|
||||
auto const onmain{nearest_on(main, onbranch, unused)};
|
||||
fouling_point point;
|
||||
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});
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 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; })};
|
||||
for (auto const &marker : markers)
|
||||
{
|
||||
int nearest{-1};
|
||||
double best{std::numeric_limits<double>::max()};
|
||||
for (int i = 0; i < static_cast<int>(tool.points.size()); ++i)
|
||||
{
|
||||
auto const distance{plan_distance(marker.location, tool.points[i].position)};
|
||||
if (distance < best)
|
||||
{
|
||||
best = distance;
|
||||
nearest = i;
|
||||
}
|
||||
}
|
||||
if (nearest < 0 || best > 40.0)
|
||||
continue;
|
||||
auto &point{tool.points[nearest]};
|
||||
if (point.standing >= 0 && tool.found[point.standing].error <= best)
|
||||
continue;
|
||||
fouling_found found;
|
||||
found.marker = marker;
|
||||
found.point = nearest;
|
||||
found.error = best;
|
||||
found.height_error = marker.location.y - point.position.y;
|
||||
found.wrong = best > tool.tolerance || (tool.height_mode == 1 && std::abs(found.height_error) > tool.height_tolerance);
|
||||
if (point.standing >= 0)
|
||||
tool.found[point.standing] = found;
|
||||
else
|
||||
{
|
||||
point.standing = static_cast<int>(tool.found.size());
|
||||
tool.found.push_back(found);
|
||||
}
|
||||
}
|
||||
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; })};
|
||||
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<int>(missing), static_cast<int>(wrong));
|
||||
}
|
||||
|
||||
void editor_mode::fouling_place()
|
||||
{
|
||||
auto &tool{m_fouling};
|
||||
fouling_update();
|
||||
std::vector<template_item> items;
|
||||
for (auto const &point : tool.points)
|
||||
{
|
||||
if (point.standing >= 0)
|
||||
continue;
|
||||
template_item item;
|
||||
item.file = kFoulingModels[std::clamp(tool.model, 0, 3)];
|
||||
item.location = point.position;
|
||||
item.yaw = point.yaw;
|
||||
if (tool.model != 2)
|
||||
item.rest = {"0"};
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
std::string error;
|
||||
auto const placed{place_templates(items, error)};
|
||||
tool.status = placed > 0 ? format(STR_C("%zu fouling point markers placed, Ctrl+Z takes them back"), placed) : error;
|
||||
WriteLog("Editor: " + tool.status, logtype::generic);
|
||||
tool.dirty = true;
|
||||
}
|
||||
|
||||
void editor_mode::fouling_fix()
|
||||
{
|
||||
auto &tool{m_fouling};
|
||||
fouling_update();
|
||||
std::vector<std::pair<standing_template, template_item>> moves;
|
||||
for (auto const &found : tool.found)
|
||||
{
|
||||
if (false == found.wrong)
|
||||
continue;
|
||||
template_item item;
|
||||
auto const &point{tool.points[found.point]};
|
||||
if (found.error <= tool.tolerance)
|
||||
{
|
||||
// in its place, only too high or too low
|
||||
item.location = {found.marker.location.x, point.position.y, found.marker.location.z};
|
||||
item.yaw = found.marker.yaw;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.location = point.position;
|
||||
item.yaw = point.yaw;
|
||||
}
|
||||
moves.emplace_back(found.marker, item);
|
||||
}
|
||||
auto const moved{move_templates(moves)};
|
||||
tool.status = format(STR_C("%zu fouling point markers moved to their places, Ctrl+Z takes it back"), moved);
|
||||
WriteLog("Editor: " + tool.status, logtype::generic);
|
||||
tool.dirty = true;
|
||||
}
|
||||
|
||||
void editor_mode::render_fouling_ui()
|
||||
{
|
||||
auto &tool{m_fouling};
|
||||
ImGui::TextDisabled("%s", STR_C("W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them"));
|
||||
auto const changed = [&](bool const Changed) {
|
||||
if (Changed)
|
||||
tool.dirty = true;
|
||||
};
|
||||
changed(ImGui::RadioButton(STR_C("The selected switch"), &tool.scope, 0));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("Around the camera"), &tool.scope, 1));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("Whole scenery"), &tool.scope, 2));
|
||||
ImGui::PushItemWidth(110.0f);
|
||||
if (tool.scope == 1)
|
||||
changed(ImGui::InputDouble(STR_C("Around the camera (m)"), &tool.reach, 0.0, 0.0, "%.0f"));
|
||||
changed(ImGui::InputDouble(STR_C("Spacing of the axes (m)"), &tool.spacing, 0.0, 0.0, "%.2f"));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("3.5 m on the straight track; more on the curves, where the cars lean out"));
|
||||
changed(ImGui::InputDouble(STR_C("A marker off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f"));
|
||||
changed(ImGui::RadioButton(STR_C("On the ground"), &tool.height_mode, 0));
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::RadioButton(STR_C("Over the rail heads"), &tool.height_mode, 1));
|
||||
if (tool.height_mode == 1)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
changed(ImGui::InputDouble(STR_C("by (m)"), &tool.height, 0.0, 0.0, "%.3f"));
|
||||
changed(ImGui::InputDouble(STR_C("A marker higher or lower by more (m) is wrong"), &tool.height_tolerance, 0.0, 0.0, "%.3f"));
|
||||
tool.height_tolerance = std::clamp(tool.height_tolerance, 0.005, 1.0);
|
||||
}
|
||||
char const *models[] = {"w17", "w17_new", "w17_old", "w17_dwuteo"};
|
||||
ImGui::Combo(STR_C("Marker"), &tool.model, models, IM_ARRAYSIZE(models));
|
||||
char const *turns[] = {"0", "90", "180", "270"};
|
||||
int turn{tool.turn / 90};
|
||||
if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns)))
|
||||
{
|
||||
tool.turn = turn * 90;
|
||||
tool.dirty = true;
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
tool.reach = std::clamp(tool.reach, 50.0, 50000.0);
|
||||
tool.spacing = std::clamp(tool.spacing, 2.0, 10.0);
|
||||
tool.tolerance = std::clamp(tool.tolerance, 0.2, 20.0);
|
||||
fouling_update();
|
||||
if (false == tool.error.empty())
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str());
|
||||
return;
|
||||
}
|
||||
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; })};
|
||||
if (missing > 0 && ImGui::Button(format(STR_C("Place %d markers"), static_cast<int>(missing)).c_str()))
|
||||
fouling_place();
|
||||
if (wrong > 0)
|
||||
{
|
||||
if (missing > 0)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(format(STR_C("Move %d wrong markers to their places"), static_cast<int>(wrong)).c_str()))
|
||||
fouling_fix();
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::draw_fouling_overlay() const
|
||||
{
|
||||
auto const &tool{m_fouling};
|
||||
if (false == tool.open)
|
||||
return;
|
||||
screen_projection const projection;
|
||||
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
||||
glm::dvec3 const camera{Global.pCamera.Pos};
|
||||
for (auto const &point : tool.points)
|
||||
{
|
||||
if (glm::distance(point.position, camera) > kOverlayRange)
|
||||
continue;
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(point.position, screen))
|
||||
continue;
|
||||
bool const standing{point.standing >= 0 && false == tool.found[point.standing].wrong};
|
||||
auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)};
|
||||
drawlist->AddCircleFilled(screen, 4.0f, colour);
|
||||
draw_label(drawlist, screen, colour, "W17");
|
||||
}
|
||||
for (auto const &found : tool.found)
|
||||
{
|
||||
if (false == found.wrong || glm::distance(found.marker.location, camera) > kOverlayRange)
|
||||
continue;
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(found.marker.location, screen))
|
||||
continue;
|
||||
drawlist->AddCircle(screen, 9.0f, IM_COL32(255, 70, 60, 235), 16, 2.5f);
|
||||
draw_label(drawlist, screen, IM_COL32(255, 70, 60, 235), format(STR_C("W17 %.1f m off, %+.2f m high"), found.error, found.height_error));
|
||||
projection.line(drawlist, found.marker.location, tool.points[found.point].position, IM_COL32(255, 70, 60, 160), 1.5f);
|
||||
}
|
||||
}
|
||||
234
application/editormodeparallel.cpp
Normal file
234
application/editormodeparallel.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
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/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#include "application/editorprojection.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
#include "editor/editorGeometry.hpp"
|
||||
|
||||
#include "scene/scenelayers.h"
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "utilities/translation.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using geometry::bezier;
|
||||
using geometry::cross;
|
||||
using geometry::plan_of;
|
||||
|
||||
double const kParallelReach{5000.0}; // m each way along the line
|
||||
double const kSpacings[] = {3.5, 4.0, 4.5, 4.75, 5.0, 6.0};
|
||||
|
||||
segment_data reversed(segment_data const &Path)
|
||||
{
|
||||
segment_data result{Path};
|
||||
result.points[segment_data::point::start] = Path.points[segment_data::point::end];
|
||||
result.points[segment_data::point::end] = Path.points[segment_data::point::start];
|
||||
result.points[segment_data::point::control1] = Path.points[segment_data::point::control2];
|
||||
result.points[segment_data::point::control2] = Path.points[segment_data::point::control1];
|
||||
result.rolls = {-Path.rolls[1], -Path.rolls[0]};
|
||||
return result;
|
||||
}
|
||||
|
||||
// the control vectors as they're drawn, also for a straight given without them
|
||||
std::pair<glm::dvec3, glm::dvec3> controls(segment_data const &Path)
|
||||
{
|
||||
auto const &start{Path.points[segment_data::point::start]};
|
||||
auto const &end{Path.points[segment_data::point::end]};
|
||||
auto const &control1{Path.points[segment_data::point::control1]};
|
||||
auto const &control2{Path.points[segment_data::point::control2]};
|
||||
if (control1 == glm::dvec3{} && control2 == glm::dvec3{})
|
||||
return {(end - start) / 3.0, (start - end) / 3.0};
|
||||
return {control1, control2};
|
||||
}
|
||||
|
||||
// curvature in the plan at the start (false) or the end (true) of the path, 1/m, positive turning left
|
||||
double curvature(segment_data const &Path, bool const Atend)
|
||||
{
|
||||
auto const [control1, control2] = controls(Path);
|
||||
auto const p0{plan_of(Path.points[segment_data::point::start])};
|
||||
auto const p3{plan_of(Path.points[segment_data::point::end])};
|
||||
auto const p1{p0 + plan_of(control1)};
|
||||
auto const p2{p3 + plan_of(control2)};
|
||||
auto const first{Atend ? 3.0 * (p3 - p2) : 3.0 * (p1 - p0)};
|
||||
auto const second{Atend ? 6.0 * (p1 - 2.0 * p2 + p3) : 6.0 * (p0 - 2.0 * p1 + p2)};
|
||||
auto const speed{glm::length(first)};
|
||||
return speed > 1e-9 ? cross(first, second) / (speed * speed * speed) : 0.0;
|
||||
}
|
||||
|
||||
glm::dvec2 direction(segment_data const &Path, bool const Atend)
|
||||
{
|
||||
auto const [control1, control2] = controls(Path);
|
||||
auto const along{Atend ? -plan_of(control2) : plan_of(control1)};
|
||||
return glm::length(along) > 1e-9 ? glm::normalize(along) : glm::dvec2{0.0, 1.0};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void editor_mode::parallel_update()
|
||||
{
|
||||
auto &tool{m_parallel};
|
||||
auto *track{selected_track()};
|
||||
if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.size() == tool.built_history)
|
||||
return;
|
||||
tool.built_for = track;
|
||||
tool.built_spacing = tool.spacing;
|
||||
tool.built_side = tool.side;
|
||||
tool.built_scope = tool.scope;
|
||||
tool.built_history = m_history.size();
|
||||
tool.pieces.clear();
|
||||
tool.styles.clear();
|
||||
tool.length = 0.0;
|
||||
tool.error.clear();
|
||||
if (track == nullptr)
|
||||
return;
|
||||
if (track->eType != tt_Normal || false == editor_track::is_supported(*track))
|
||||
{
|
||||
tool.error = STR_C("A switch has no line of its own: select a plain path");
|
||||
return;
|
||||
}
|
||||
std::vector<std::pair<TTrack *, segment_data>> sources;
|
||||
if (tool.scope == 1)
|
||||
{
|
||||
editor_track::chain run;
|
||||
std::string error;
|
||||
if (editor_track::find_run(*track, kParallelReach, run, error))
|
||||
for (std::size_t i = 0; i < run.tracks.size(); ++i)
|
||||
sources.emplace_back(run.tracks[i], run.forward[i] ? run.tracks[i]->m_paths.front() : reversed(run.tracks[i]->m_paths.front()));
|
||||
}
|
||||
if (sources.empty())
|
||||
sources.emplace_back(track, track->m_paths.front());
|
||||
auto const offset{tool.spacing * tool.side};
|
||||
// the joints are moved once, along the mean direction of the paths meeting there, so the new paths meet too
|
||||
std::vector<glm::dvec3> joints;
|
||||
for (std::size_t k = 0; k <= sources.size(); ++k)
|
||||
{
|
||||
bool const first{k == 0};
|
||||
bool const last{k == sources.size()};
|
||||
auto const &before{sources[last ? k - 1 : (first ? 0 : k - 1)].second};
|
||||
auto const &after{sources[last ? k - 1 : k].second};
|
||||
auto const point{first ? after.points[segment_data::point::start] : before.points[segment_data::point::end]};
|
||||
glm::dvec2 heading{first ? direction(after, false) : last ? direction(before, true) : direction(before, true) + direction(after, false)};
|
||||
heading = glm::length(heading) > 1e-9 ? glm::normalize(heading) : glm::dvec2{0.0, 1.0};
|
||||
auto const roll{first ? after.rolls[0] : last ? before.rolls[1] : 0.5f * (before.rolls[1] + after.rolls[0])};
|
||||
glm::dvec2 const left{-heading.y, heading.x};
|
||||
auto const planar{plan_of(point) + left * offset};
|
||||
joints.emplace_back(planar.x, point.y - offset * std::tan(glm::radians(static_cast<double>(roll))), planar.y);
|
||||
}
|
||||
for (std::size_t i = 0; i < sources.size(); ++i)
|
||||
{
|
||||
auto const &source{sources[i].second};
|
||||
auto const [control1, control2] = controls(source);
|
||||
auto const startscale{1.0 - curvature(source, false) * offset};
|
||||
auto const endscale{1.0 - curvature(source, true) * offset};
|
||||
if (startscale < 0.05 || endscale < 0.05)
|
||||
{
|
||||
tool.pieces.clear();
|
||||
tool.styles.clear();
|
||||
tool.error = format(STR_C("The spacing is larger than the radius of %s"), sources[i].first->name().c_str());
|
||||
return;
|
||||
}
|
||||
segment_data piece;
|
||||
piece.points[segment_data::point::start] = joints[i];
|
||||
piece.points[segment_data::point::end] = joints[i + 1];
|
||||
piece.points[segment_data::point::control1] = control1 * startscale;
|
||||
piece.points[segment_data::point::control2] = control2 * endscale;
|
||||
piece.rolls = source.rolls;
|
||||
auto const middle{0.5 * (curvature(source, false) + curvature(source, true))};
|
||||
if (source.radius != 0.0f && std::abs(middle) > 1e-7)
|
||||
piece.radius = static_cast<float>(std::copysign(std::abs((1.0 - middle * offset) / middle), static_cast<double>(source.radius)));
|
||||
tool.length += bezier{piece}.plan_length();
|
||||
tool.pieces.push_back(piece);
|
||||
tool.styles.push_back(sources[i].first);
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::parallel_build()
|
||||
{
|
||||
auto &tool{m_parallel};
|
||||
parallel_update();
|
||||
if (tool.pieces.empty())
|
||||
return;
|
||||
std::vector<TTrack *> created;
|
||||
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);
|
||||
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")};
|
||||
WriteLog("Editor: " + tool.status, logtype::generic);
|
||||
tool.built_for = nullptr;
|
||||
}
|
||||
|
||||
void editor_mode::render_parallel_ui()
|
||||
{
|
||||
auto &tool{m_parallel};
|
||||
ImGui::TextDisabled("%s", STR_C("Second track alongside, curves included: radius R minus or plus the spacing,\nthe same transitions and cant, heights on the plane of the cant"));
|
||||
ImGui::RadioButton(STR_C("This path"), &tool.scope, 0);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(STR_C("The line, up to the switches"), &tool.scope, 1);
|
||||
ImGui::RadioButton(STR_C("Left"), &tool.side, -1);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(STR_C("Right"), &tool.side, 1);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", STR_C("(of the direction of the line, see the preview)"));
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
ImGui::InputDouble(STR_C("Spacing (m)"), &tool.spacing, 0.0, 0.0, "%.2f");
|
||||
tool.spacing = std::clamp(tool.spacing, 1.5, 100.0);
|
||||
for (auto const spacing : kSpacings)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(format("%.2f", spacing).c_str()))
|
||||
tool.spacing = spacing;
|
||||
}
|
||||
parallel_update();
|
||||
if (false == tool.error.empty())
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str());
|
||||
else if (false == tool.pieces.empty())
|
||||
{
|
||||
if (ImGui::Button(format(STR_C("Build %zu paths, %.1f m"), tool.pieces.size(), tool.length).c_str()))
|
||||
parallel_build();
|
||||
}
|
||||
if (false == tool.status.empty())
|
||||
ImGui::TextWrapped("%s", tool.status.c_str());
|
||||
}
|
||||
|
||||
void editor_mode::draw_parallel_preview() const
|
||||
{
|
||||
auto const &tool{m_parallel};
|
||||
if (false == tool.open || tool.pieces.empty() || selected_track() != tool.built_for)
|
||||
return;
|
||||
screen_projection const projection;
|
||||
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
||||
for (auto const &piece : tool.pieces)
|
||||
{
|
||||
bezier const curve{piece};
|
||||
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 2.0), 4, 128)};
|
||||
auto previous{curve.point(0.0)};
|
||||
for (int k = 1; k <= count; ++k)
|
||||
{
|
||||
auto const next{curve.point(static_cast<double>(k) / count)};
|
||||
projection.line(drawlist, previous, next, k % 2 ? IM_COL32(90, 255, 120, 230) : IM_COL32(90, 255, 120, 90), 3.0f);
|
||||
previous = next;
|
||||
}
|
||||
}
|
||||
ImVec2 screen;
|
||||
if (projection.project(tool.pieces.front().points[segment_data::point::start], screen))
|
||||
drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(90, 255, 120, 230));
|
||||
}
|
||||
@@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
namespace
|
||||
@@ -475,6 +476,122 @@ void editor_mode::profile_fit_ground()
|
||||
state.status = format(STR_C("Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m"), state.line.points.size(), fill, cut);
|
||||
}
|
||||
|
||||
std::pair<double, double> editor_mode::profile_ballast(TTrack const &Track)
|
||||
{
|
||||
auto const depth{std::max(0.0, static_cast<double>(Track.fTexHeight1 - Track.fTexHeightOffset))};
|
||||
auto const foot{0.5 * std::abs(Track.fTrackWidth) + std::abs(Track.fTexWidth) + std::abs(Track.fTexSlope)};
|
||||
return {depth, std::max(0.5, foot)};
|
||||
}
|
||||
|
||||
std::pair<int, int> editor_mode::profile_generate_ground(std::vector<std::pair<double, double>> const &Beds, double const Reach)
|
||||
{
|
||||
auto &state{m_profile};
|
||||
auto &works{state.earthworks};
|
||||
auto const &samples{state.samples};
|
||||
bool const streaming{m_streamer.active()};
|
||||
double const size{streaming ? m_streamer.cells() * m_streamer.cellsize() : chunk_grid_size()};
|
||||
int const cells{streaming ? m_streamer.cells() : std::clamp(m_terrain_cells, 1, 256)};
|
||||
if (size <= 0.0 || cells <= 0 || samples.empty())
|
||||
return {0, 0};
|
||||
double const cellsize{size / cells};
|
||||
std::set<std::pair<int, int>> keys;
|
||||
for (auto const &sample : samples)
|
||||
{
|
||||
auto const &p{sample.position};
|
||||
for (auto x = static_cast<int>(std::floor((p.x - Reach) / size)); x <= static_cast<int>(std::floor((p.x + Reach) / size)); ++x)
|
||||
for (auto z = static_cast<int>(std::floor((p.z - Reach) / size)); z <= static_cast<int>(std::floor((p.z + Reach) / size)); ++z)
|
||||
keys.insert({x, z});
|
||||
}
|
||||
auto const terrains{active_terrains()};
|
||||
std::string const texture{m_terrain_texture};
|
||||
int made{0}, skipped{0};
|
||||
std::size_t const side{static_cast<std::size_t>(cells) + 1};
|
||||
for (auto const &key : keys)
|
||||
{
|
||||
glm::dvec2 const low{key.first * size, key.second * size};
|
||||
glm::dvec2 const middle{low + glm::dvec2{size * 0.5}};
|
||||
if (streaming ? m_streamer.terrain_at(middle.x, middle.y) != nullptr : m_grid_chunks.count(key) > 0)
|
||||
continue;
|
||||
std::vector<std::size_t> near;
|
||||
for (std::size_t i = 0; i < samples.size(); ++i)
|
||||
if (std::abs(samples[i].position.x - middle.x) <= size * 0.5 + Reach && std::abs(samples[i].position.z - middle.y) <= size * 0.5 + Reach)
|
||||
near.push_back(i);
|
||||
if (near.empty())
|
||||
continue;
|
||||
std::vector<glm::dvec3> points;
|
||||
std::vector<std::size_t> nearest;
|
||||
points.reserve(side * side);
|
||||
nearest.reserve(side * side);
|
||||
for (std::size_t iz = 0; iz < side; ++iz)
|
||||
for (std::size_t ix = 0; ix < side; ++ix)
|
||||
{
|
||||
glm::dvec3 point{low.x + ix * cellsize, 0.0, low.y + iz * cellsize};
|
||||
auto best{near.front()};
|
||||
auto distance{std::numeric_limits<double>::max()};
|
||||
for (auto const i : near)
|
||||
{
|
||||
auto const d{plan_distance(point, samples[i].position)};
|
||||
if (d < distance)
|
||||
{
|
||||
distance = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
point.y = profile::elevation(state.line, samples[best].chainage);
|
||||
points.push_back(point);
|
||||
nearest.push_back(best);
|
||||
}
|
||||
std::vector<char> found;
|
||||
auto heights{ground_heights(points, true, &found)};
|
||||
std::size_t own{0};
|
||||
for (std::size_t i = 0; i < points.size(); ++i)
|
||||
if (found[i] != 0 && std::none_of(terrains.begin(), terrains.end(), [&](editor_terrain const *Terrain) { return Terrain->contains(points[i].x, points[i].z); }))
|
||||
++own;
|
||||
if (own * 20 > points.size())
|
||||
{
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
for (std::size_t i = 0; i < points.size(); ++i)
|
||||
{
|
||||
if (found[i] != 0)
|
||||
continue;
|
||||
auto const sample{nearest[i]};
|
||||
heights[i] = sample < state.terrain.size() && std::isfinite(state.terrain[sample]) ? state.terrain[sample] : profile::elevation(state.line, samples[sample].chainage) - Beds[sample].first;
|
||||
}
|
||||
auto const sampler = [&](double const X, double const Z, double &Height) {
|
||||
auto const ix{static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / cellsize), 0, cells))};
|
||||
auto const iz{static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / cellsize), 0, cells))};
|
||||
Height = heights[iz * side + ix];
|
||||
return true;
|
||||
};
|
||||
if (streaming)
|
||||
{
|
||||
m_streamer.add_chunk(key.first, key.second);
|
||||
auto *terrain{m_streamer.terrain_at(middle.x, middle.y)};
|
||||
if (terrain == nullptr)
|
||||
continue;
|
||||
terrain->reshape([&](double const X, double const Z, float &Height) {
|
||||
double height;
|
||||
sampler(X, Z, height);
|
||||
Height = static_cast<float>(height);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
auto chunk{std::make_unique<editor_terrain>()};
|
||||
if (false == chunk->create(glm::dvec3{middle.x, m_terrain_baseheight, middle.y}, cells, static_cast<float>(cellsize), texture, sampler))
|
||||
continue;
|
||||
m_grid_chunks[key] = std::move(chunk);
|
||||
}
|
||||
works.generated.push_back(key);
|
||||
works.generated_streaming = streaming;
|
||||
++made;
|
||||
}
|
||||
return {made, skipped};
|
||||
}
|
||||
|
||||
void editor_mode::profile_shape_ground()
|
||||
{
|
||||
auto &state{m_profile};
|
||||
@@ -483,11 +600,23 @@ void editor_mode::profile_shape_ground()
|
||||
if (samples.size() < 2 || state.line.points.size() < 2)
|
||||
return;
|
||||
auto const slope{std::max(0.1, works.slope)};
|
||||
auto const reach{works.half_width + works.reach};
|
||||
std::vector<std::pair<double, double>> beds(samples.size(), {works.depth, works.half_width});
|
||||
if (works.from_ballast)
|
||||
for (std::size_t i = 0; i < samples.size(); ++i)
|
||||
if (samples[i].span < state.route.spans.size())
|
||||
if (auto const *track{state.route.spans[samples[i].span].track}; track != nullptr)
|
||||
beds[i] = profile_ballast(*track);
|
||||
double widest{0.0};
|
||||
for (auto const &bed : beds)
|
||||
widest = std::max(widest, bed.second);
|
||||
auto const reach{widest + works.reach};
|
||||
works.undo.clear();
|
||||
works.generated.clear();
|
||||
auto const generated{works.generate ? profile_generate_ground(beds, reach) : std::pair<int, int>{0, 0}};
|
||||
auto const terrains{active_terrains()};
|
||||
if (terrains.empty())
|
||||
{
|
||||
state.error = STR_C("There's no editor terrain to shape: convert the scenery ground first");
|
||||
state.error = generated.second > 0 ? STR_C("The scenery has its own ground under the route: convert it to editor terrain first") : STR_C("There's no editor terrain to shape: convert the scenery ground first");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -509,7 +638,6 @@ void editor_mode::profile_shape_ground()
|
||||
high = glm::max(high, glm::dvec2{std::max(a.x, b.x), std::max(a.z, b.z)});
|
||||
}
|
||||
|
||||
works.undo.clear();
|
||||
std::size_t moved{0};
|
||||
double fill{0.0}, cut{0.0};
|
||||
for (auto *terrain : terrains)
|
||||
@@ -520,6 +648,7 @@ void editor_mode::profile_shape_ground()
|
||||
return false;
|
||||
double distance{reach};
|
||||
double chainage{-1.0};
|
||||
std::size_t nearest{0};
|
||||
auto const from{key(X - reach, Z - reach)};
|
||||
auto const to{key(X + reach, Z + reach)};
|
||||
for (auto x = from.first; x <= to.first; ++x)
|
||||
@@ -540,12 +669,14 @@ void editor_mode::profile_shape_ground()
|
||||
{
|
||||
distance = offset;
|
||||
chainage = samples[i].chainage + (samples[i + 1].chainage - samples[i].chainage) * t;
|
||||
nearest = t < 0.5 ? i : i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chainage < 0.0)
|
||||
return false;
|
||||
auto const formation{profile::elevation(state.line, chainage) - works.depth};
|
||||
auto const &bed{beds[nearest]};
|
||||
auto const formation{profile::elevation(state.line, chainage) - bed.first};
|
||||
auto const ground{static_cast<double>(Height)};
|
||||
// the bends at the edge of the formation and at the foot of the slope are rounded off over the given length
|
||||
auto const rounding{std::max(0.0, works.rounding)};
|
||||
@@ -555,7 +686,7 @@ void editor_mode::profile_shape_ground()
|
||||
auto const h{std::max(K - std::abs(A - B), 0.0) / K};
|
||||
return std::max(A, B) + h * h * K * 0.25;
|
||||
};
|
||||
auto const beyond{soft_max(0.0, distance - works.half_width, rounding)};
|
||||
auto const beyond{soft_max(0.0, distance - bed.second, rounding)};
|
||||
auto const bend{rounding / slope};
|
||||
auto const target{ground > formation ? -soft_max(-ground, -(formation + beyond / slope), bend) : soft_max(ground, formation - beyond / slope, bend)};
|
||||
if (std::abs(target - ground) < 0.005)
|
||||
@@ -569,14 +700,21 @@ void editor_mode::profile_shape_ground()
|
||||
if (shaped)
|
||||
works.undo.emplace_back(terrain, before);
|
||||
}
|
||||
auto const chunks = [&](std::string Text) {
|
||||
if (generated.first > 0)
|
||||
Text += format(STR_C("; %d chunk(s) of terrain made under the route"), generated.first);
|
||||
if (generated.second > 0)
|
||||
Text += format(STR_C("; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it"), generated.second);
|
||||
return Text;
|
||||
};
|
||||
if (moved == 0)
|
||||
{
|
||||
state.status = STR_C("The editor terrain doesn't reach the route, or it's shaped already");
|
||||
state.status = chunks(STR_C("The editor terrain doesn't reach the route, or it's shaped already"));
|
||||
return;
|
||||
}
|
||||
state.error.clear();
|
||||
profile_resample();
|
||||
state.status = format(STR_C("Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m"), moved, fill, cut);
|
||||
state.status = chunks(format(STR_C("Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m"), moved, fill, cut));
|
||||
WriteLog("Editor: vertical profile - " + state.status, logtype::generic);
|
||||
}
|
||||
|
||||
@@ -592,6 +730,15 @@ void editor_mode::profile_restore_ground()
|
||||
++restored;
|
||||
}
|
||||
state.earthworks.undo.clear();
|
||||
for (auto const &key : state.earthworks.generated)
|
||||
{
|
||||
if (state.earthworks.generated_streaming)
|
||||
m_streamer.remove_chunk(key.first, key.second);
|
||||
else
|
||||
remove_grid_chunk(key.first, key.second);
|
||||
++restored;
|
||||
}
|
||||
state.earthworks.generated.clear();
|
||||
profile_resample();
|
||||
state.status = restored > 0 ? STR_C("The terrain is back as it was before the shaping") : STR_C("The shaped terrain isn't loaded any more");
|
||||
}
|
||||
@@ -659,10 +806,27 @@ void editor_mode::render_profile_earthworks()
|
||||
render_ground_conversion();
|
||||
auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); };
|
||||
ImGui::PushItemWidth(90.0f);
|
||||
drag("Formation below the rail top (m)", works.depth, 0.01f, "%.2f");
|
||||
drag("Half width of the formation (m)", works.half_width, 0.05f, "%.2f");
|
||||
ImGui::Checkbox(STR_C("Make terrain where the route has none"), &works.generate);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them"));
|
||||
ImGui::SetTooltip("%s", STR_C("Chunks of editor terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead"));
|
||||
ImGui::Checkbox(STR_C("Formation under the ballast of the track"), &works.from_ballast);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below"));
|
||||
if (works.from_ballast)
|
||||
{
|
||||
if (false == state.route.spans.empty() && state.route.spans.front().track != nullptr)
|
||||
{
|
||||
auto const bed{profile_ballast(*state.route.spans.front().track)};
|
||||
ImGui::TextDisabled(STR_C("Ballast %.2f m deep, its foot %.2f m from the axis"), bed.first, bed.second);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
drag("Formation below the track (m)", works.depth, 0.01f, "%.2f");
|
||||
drag("Half width of the formation (m)", works.half_width, 0.05f, "%.2f");
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them"));
|
||||
}
|
||||
drag("Slopes 1 : n, n", works.slope, 0.05f, "%.2f");
|
||||
drag("Slopes reach at most (m)", works.reach, 1.0f, "%.0f");
|
||||
drag("Rounding of the edges (m)", works.rounding, 0.1f, "%.1f");
|
||||
@@ -678,13 +842,13 @@ void editor_mode::render_profile_earthworks()
|
||||
ImGui::TextDisabled("%s", STR_C("Correct the errors of the grade line first"));
|
||||
else if (ImGui::Button(STR_C("Shape the terrain")))
|
||||
profile_shape_ground();
|
||||
if (false == works.undo.empty())
|
||||
if (false == works.undo.empty() || false == works.generated.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(STR_C("Restore the terrain")))
|
||||
profile_restore_ground();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("Puts back the terrain as it was before the last shaping"));
|
||||
ImGui::SetTooltip("%s", STR_C("Puts back the terrain as it was before the last shaping, and takes away the terrain it made"));
|
||||
}
|
||||
if (false == active_terrains().empty())
|
||||
{
|
||||
|
||||
385
application/editormodeselftest.cpp
Normal file
385
application/editormodeselftest.cpp
Normal file
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
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/editormode.h"
|
||||
#include "application/application.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#include "application/editorprojection.h"
|
||||
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "world/Track.h"
|
||||
#include "vehicle/DynObj.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
// the script is a text file, one action a line:
|
||||
// wait <frames>
|
||||
// camera <x> <y> <z> <x> <y> <z> from the first point, looking at the second
|
||||
// select <track name>
|
||||
// sweep <key>=<value>... model, params (separated by commas), lateral, height, side (origin, near, far,
|
||||
// centre), top (origin, bottom, top, edge), mode (bend, repeat), step,
|
||||
// length (original, all, or metres), from, to, widen (0, 1), face (0, 1), tilt (0, 1)
|
||||
// spread <track> <stop 0..2> <reach> logs the paths the spread from the track takes
|
||||
// newscenery the editor starts again with the wizard of a new scenery
|
||||
// vehicles logs each vehicle: its head, facing, and what its couplers hold
|
||||
// screenshot
|
||||
// save
|
||||
// log <text>
|
||||
// quit
|
||||
void editor_mode::selftest_step()
|
||||
{
|
||||
auto &test{m_selftest};
|
||||
if (false == test.loaded)
|
||||
{
|
||||
test.loaded = true;
|
||||
auto const *file{std::getenv("EU07_EDITOR_SELFTEST")};
|
||||
if (file == nullptr)
|
||||
return;
|
||||
std::ifstream stream(file);
|
||||
std::string line;
|
||||
while (std::getline(stream, line))
|
||||
if (false == line.empty() && line.front() != '#')
|
||||
test.lines.push_back(line);
|
||||
WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file);
|
||||
}
|
||||
if (test.wait > 0)
|
||||
{
|
||||
--test.wait;
|
||||
return;
|
||||
}
|
||||
while (test.next < test.lines.size())
|
||||
{
|
||||
std::istringstream words(test.lines[test.next++]);
|
||||
std::string command;
|
||||
words >> command;
|
||||
if (command == "wait")
|
||||
{
|
||||
words >> test.wait;
|
||||
return;
|
||||
}
|
||||
if (command == "camera")
|
||||
{
|
||||
glm::dvec3 from, to;
|
||||
words >> from.x >> from.y >> from.z >> to.x >> to.y >> to.z;
|
||||
auto const look{glm::normalize(to - from)};
|
||||
Camera.Pos = from;
|
||||
Camera.Angle = glm::vec3(static_cast<float>(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast<float>(std::atan2(-look.x, -look.z)), 0.0f);
|
||||
Global.pCamera = Camera;
|
||||
m_focus_active = false;
|
||||
}
|
||||
else if (command == "select")
|
||||
{
|
||||
std::string name;
|
||||
words >> name;
|
||||
m_node = simulation::Paths.find(name);
|
||||
m_track_point = {};
|
||||
ui()->set_node(m_node);
|
||||
WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track"));
|
||||
}
|
||||
else if (command == "sweep")
|
||||
{
|
||||
std::map<std::string, std::string> values;
|
||||
std::string pair;
|
||||
while (words >> pair)
|
||||
{
|
||||
auto const equals{pair.find('=')};
|
||||
if (equals != std::string::npos)
|
||||
values[pair.substr(0, equals)] = pair.substr(equals + 1);
|
||||
}
|
||||
auto &tool{m_sweep};
|
||||
auto const get = [&](char const *Key, std::string const &Default) { auto const found{values.find(Key)}; return found != values.end() ? found->second : Default; };
|
||||
tool.edited = nullptr;
|
||||
tool.curve_for = nullptr;
|
||||
std::snprintf(tool.model, sizeof(tool.model), "%s", get("model", "").c_str());
|
||||
tool.parameters.clear();
|
||||
std::stringstream parameters(get("params", ""));
|
||||
std::string parameter;
|
||||
while (std::getline(parameters, parameter, ','))
|
||||
{
|
||||
tool.parameters.emplace_back();
|
||||
std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", parameter.c_str());
|
||||
}
|
||||
tool.parameters_for = tool.model;
|
||||
auto &settings{tool.settings};
|
||||
settings.lateral = std::stod(get("lateral", "0"));
|
||||
settings.height = std::stod(get("height", "0"));
|
||||
auto const side{get("side", "origin")};
|
||||
settings.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0;
|
||||
auto const top{get("top", "origin")};
|
||||
settings.height_anchor = top == "bottom" ? 1 : top == "top" ? 2 : top == "edge" ? 3 : 0;
|
||||
settings.bend = get("mode", "bend") != "repeat";
|
||||
settings.step = std::stod(get("step", "0"));
|
||||
settings.widen = get("widen", "0") == "1";
|
||||
settings.face = get("face", "1") == "1";
|
||||
settings.tilt = get("tilt", "0") == "1";
|
||||
settings.from = std::stod(get("from", "0"));
|
||||
settings.to = std::stod(get("to", "-1"));
|
||||
auto const length{get("length", "all")};
|
||||
tool.length_mode = length == "original" ? 0 : length == "all" ? 2 : 1;
|
||||
if (tool.length_mode == 1)
|
||||
tool.length = std::stod(length);
|
||||
tool.measured.clear();
|
||||
tool.open = true;
|
||||
sweep_curve();
|
||||
sweep_measure();
|
||||
if (get("create", "1") == "1")
|
||||
{
|
||||
sweep_create();
|
||||
WriteLog("SELFTEST sweep: " + tool.status);
|
||||
}
|
||||
}
|
||||
else if (command == "drag")
|
||||
{
|
||||
glm::dvec3 from{0.0}, to{0.0};
|
||||
words >> from.x >> from.z >> to.x >> to.z;
|
||||
m_sweep.open = true;
|
||||
if (false == sweep_press_at(from, false))
|
||||
{
|
||||
WriteLog("SELFTEST drag: the press wasn't taken");
|
||||
continue;
|
||||
}
|
||||
sweep_drag_at(to);
|
||||
sweep_release();
|
||||
WriteLog("SELFTEST drag: " + m_sweep.status);
|
||||
}
|
||||
else if (command == "probe")
|
||||
{
|
||||
// edge of the last laid model as it was made: its distance from the axis and the height of its top over the rail head
|
||||
std::string name;
|
||||
words >> name;
|
||||
auto *sweep{name.empty() ? m_sweep.edited : simulation::Sweeps.find(name)};
|
||||
if (sweep == nullptr)
|
||||
{
|
||||
WriteLog("SELFTEST probe: nothing laid");
|
||||
continue;
|
||||
}
|
||||
auto const &definition{sweep->definition()};
|
||||
double const sign{definition.lateral >= 0.0 ? 1.0 : -1.0};
|
||||
struct bin
|
||||
{
|
||||
double edge{std::numeric_limits<double>::max()};
|
||||
std::vector<std::pair<double, double>> points; // lateral, height over the axis
|
||||
};
|
||||
std::map<int, bin> bins;
|
||||
for (auto const &shape : sweep->create_shapes())
|
||||
for (auto const &vertex : shape.data().vertices)
|
||||
{
|
||||
double lateral;
|
||||
auto const station{sweep->project(vertex.position, lateral)};
|
||||
if (lateral * sign <= 0.0)
|
||||
continue;
|
||||
auto const axis{sweep->frame_at(station)};
|
||||
auto &entry{bins[static_cast<int>(station / 20.0)]};
|
||||
entry.edge = std::min(entry.edge, std::abs(lateral));
|
||||
entry.points.emplace_back(std::abs(lateral), vertex.position.y - axis.position.y);
|
||||
}
|
||||
WriteLog(format("SELFTEST probe %s: %zu stretches with the model, %.1f - %.1f m of %.1f m", sweep->name().c_str(), bins.size(), sweep->start(), sweep->end(), sweep->length()));
|
||||
for (auto const &[index, entry] : bins)
|
||||
{
|
||||
auto const station{index * 20.0 + 10.0};
|
||||
if (station < sweep->start() || station > sweep->end())
|
||||
continue;
|
||||
double top{-1000.0};
|
||||
for (auto const &point : entry.points)
|
||||
if (point.first < entry.edge + 0.15)
|
||||
top = std::max(top, point.second);
|
||||
auto const at{sweep->frame_at(station)};
|
||||
auto const head{0.18 + sweep_node::rail_head(at.roll, definition.lateral)};
|
||||
WriteLog(format("SELFTEST probe %s at %.0f m: edge %.3f m from the axis, its top %.3f m over the rail head (cant %.1f deg, gauge widening %.3f m, cant part %.3f m)", sweep->name().c_str(), station, entry.edge, top - head, at.roll,
|
||||
at.widening[definition.lateral >= 0.0 ? 1 : 0], std::max(0.0, at.cant[definition.lateral >= 0.0 ? 1 : 0])));
|
||||
}
|
||||
}
|
||||
else if (command == "fouling")
|
||||
{
|
||||
std::map<std::string, std::string> values;
|
||||
std::string pair;
|
||||
while (words >> pair)
|
||||
{
|
||||
auto const equals{pair.find('=')};
|
||||
if (equals != std::string::npos)
|
||||
values[pair.substr(0, equals)] = pair.substr(equals + 1);
|
||||
}
|
||||
auto &tool{m_fouling};
|
||||
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"] == "1")
|
||||
{
|
||||
fouling_place();
|
||||
tool.dirty = true;
|
||||
fouling_update();
|
||||
WriteLog("SELFTEST fouling after placing: " + tool.status);
|
||||
}
|
||||
if (values["fix"] == "1")
|
||||
{
|
||||
fouling_fix();
|
||||
tool.dirty = true;
|
||||
fouling_update();
|
||||
WriteLog("SELFTEST fouling after the fix: " + tool.status);
|
||||
}
|
||||
}
|
||||
else if (command == "hekto")
|
||||
{
|
||||
std::map<std::string, std::string> values;
|
||||
std::string pair;
|
||||
while (words >> pair)
|
||||
{
|
||||
auto const equals{pair.find('=')};
|
||||
if (equals != std::string::npos)
|
||||
values[pair.substr(0, equals)] = pair.substr(equals + 1);
|
||||
}
|
||||
auto *track{simulation::Paths.find(values["track"])};
|
||||
if (track == nullptr)
|
||||
{
|
||||
WriteLog("SELFTEST hekto: no such track");
|
||||
continue;
|
||||
}
|
||||
hekto_start(*track, track->m_paths.front().points[segment_data::point::start]);
|
||||
auto &tool{m_hekto};
|
||||
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; })};
|
||||
WriteLog(format("SELFTEST hekto: km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing. %s", tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast<int>(missing), tool.found.size(), 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) * tool.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["place"] == "1")
|
||||
{
|
||||
hekto_place();
|
||||
WriteLog("SELFTEST hekto: " + tool.status);
|
||||
}
|
||||
}
|
||||
else if (command == "tracks")
|
||||
{
|
||||
int tab{0};
|
||||
words >> tab;
|
||||
show_track_tab(track_tab::lineside);
|
||||
m_sweep.expand = tab == 0;
|
||||
m_hekto.expand = tab == 1;
|
||||
m_fouling.expand = tab == 2;
|
||||
m_parallel.expand = tab == 3;
|
||||
m_vehicle.expand = tab == 4;
|
||||
}
|
||||
else if (command == "curve")
|
||||
{
|
||||
sweep_curve();
|
||||
for (auto const &piece : m_sweep.curve)
|
||||
{
|
||||
auto const &a{piece.points[segment_data::point::start]};
|
||||
auto const &b{piece.points[segment_data::point::end]};
|
||||
WriteLog(format("SELFTEST curve piece %.1f %.1f -> %.1f %.1f", a.x, a.z, b.x, b.z));
|
||||
}
|
||||
if (false == m_sweep.outline.empty())
|
||||
WriteLog(format("SELFTEST curve outline %zu points, %.1f %.1f -> %.1f %.1f, %.1f m", m_sweep.outline.size(), m_sweep.outline.front().x, m_sweep.outline.front().z, m_sweep.outline.back().x, m_sweep.outline.back().z, m_sweep.curve_length));
|
||||
}
|
||||
else if (command == "projtest")
|
||||
{
|
||||
screen_projection const projection;
|
||||
for (std::size_t i = 0; i < m_sweep.outline.size() && i < 45; i += 4)
|
||||
{
|
||||
ImVec2 screen;
|
||||
auto const shown{projection.project(m_sweep.outline[i], screen)};
|
||||
auto const clip{projection.clip(m_sweep.outline[i])};
|
||||
WriteLog(format("SELFTEST proj %zu: %.1f %.1f %.1f -> %d %.0f %.0f w %.2f", i, m_sweep.outline[i].x, m_sweep.outline[i].y, m_sweep.outline[i].z, shown ? 1 : 0, screen.x, screen.y, clip.w));
|
||||
}
|
||||
}
|
||||
else if (command == "spread")
|
||||
{
|
||||
std::string name;
|
||||
words >> name >> m_spread.stop >> m_spread.reach;
|
||||
if (auto *track{simulation::Paths.find(name)})
|
||||
{
|
||||
std::string names;
|
||||
for (auto const *other : track_spread_from(*track))
|
||||
names += ' ' + other->name();
|
||||
WriteLog("SELFTEST spread " + name + ":" + names + " | prev " + (track->trPrev ? track->trPrev->name() : std::string{"-"}) + " next " + (track->trNext ? track->trNext->name() : std::string{"-"}));
|
||||
}
|
||||
}
|
||||
else if (command == "newscenery")
|
||||
{
|
||||
WriteLog(restart_for_new_scenery() ? "SELFTEST newscenery: started" : "SELFTEST newscenery: failed");
|
||||
return;
|
||||
}
|
||||
else if (command == "vehicles")
|
||||
{
|
||||
for (auto *vehicle : simulation::Vehicles.sequence())
|
||||
{
|
||||
if (vehicle == nullptr)
|
||||
continue;
|
||||
auto const head{vehicle->HeadPosition()};
|
||||
auto const front{vehicle->VectorFront()};
|
||||
auto const coupler = [&](int const End) {
|
||||
auto const &coupler{vehicle->MoverParameters->Couplers[End]};
|
||||
return coupler.Connected != nullptr ? coupler.Connected->Name + "/" + std::to_string(coupler.CouplingFlag) : std::string{"-"};
|
||||
};
|
||||
WriteLog(format("SELFTEST vehicle %s head %.2f %.2f front %.2f %.2f dir %d coupler0 %s coupler1 %s", vehicle->name().c_str(), head.x, head.z, front.x, front.z, vehicle->iDirection, coupler(0).c_str(), coupler(1).c_str()));
|
||||
}
|
||||
}
|
||||
else if (command == "screenshot")
|
||||
{
|
||||
Application.queue_screenshot();
|
||||
test.wait = 3;
|
||||
return;
|
||||
}
|
||||
else if (command == "save")
|
||||
{
|
||||
save();
|
||||
}
|
||||
else if (command == "log")
|
||||
{
|
||||
std::string text;
|
||||
std::getline(words, text);
|
||||
WriteLog("SELFTEST" + text);
|
||||
}
|
||||
else if (command == "quit")
|
||||
{
|
||||
if (false == test.quitting)
|
||||
{
|
||||
WriteLog("SELFTEST done");
|
||||
test.quitting = true;
|
||||
test.wait = 30;
|
||||
--test.next;
|
||||
return;
|
||||
}
|
||||
Application.queue_quit(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ namespace
|
||||
{
|
||||
|
||||
double const kSpeedLineReach{5000.0}; // m each way, for the line through the selected path
|
||||
double const kSpeedViewDistance{25.0};
|
||||
double const kSpeedOverlayRange{3000.0};
|
||||
|
||||
bool rail(TTrack const &Track)
|
||||
@@ -148,21 +147,7 @@ void editor_mode::speed_focus(int const Index)
|
||||
return;
|
||||
m_node = verdict.track;
|
||||
ui()->set_node(m_node);
|
||||
auto const &path{verdict.track->m_paths[std::min<std::size_t>(verdict.path, verdict.track->m_paths.size() - 1)]};
|
||||
geometry::bezier const curve{path};
|
||||
glm::dvec3 look{curve.first(0.5)};
|
||||
look.y = 0.0;
|
||||
look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0);
|
||||
if (glm::dot(Camera.Pos - verdict.position, look) > 0.0)
|
||||
look = -look;
|
||||
look = glm::normalize(look - glm::dvec3(0.0, 0.25, 0.0));
|
||||
m_focus_start_pos = Camera.Pos;
|
||||
m_focus_start_angle = Camera.Angle;
|
||||
m_focus_target_pos = verdict.position - look * kSpeedViewDistance;
|
||||
m_focus_target_angle = glm::vec3(static_cast<float>(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast<float>(std::atan2(-look.x, -look.z)), 0.0f);
|
||||
m_focus_active = true;
|
||||
m_focus_time = 0.0;
|
||||
m_focus_duration = 0.6;
|
||||
focus_track(*verdict.track, verdict.path, verdict.position);
|
||||
}
|
||||
|
||||
void editor_mode::speed_apply(std::vector<int> const &Indices, bool const Branches)
|
||||
|
||||
955
application/editormodesweep.cpp
Normal file
955
application/editormodesweep.cpp
Normal file
@@ -0,0 +1,955 @@
|
||||
/*
|
||||
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/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#include "application/editorprojection.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
#include "editor/editorGeometry.hpp"
|
||||
#include "editor/editorIncludeInfo.hpp"
|
||||
|
||||
#include "rendering/renderer.h"
|
||||
#include "scene/scenelayers.h"
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "world/Sweep.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "utilities/translation.h"
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using geometry::bezier;
|
||||
using geometry::plan_distance;
|
||||
using geometry::plan_of;
|
||||
|
||||
double const kSweepRouteReach{2000.0}; // m each way through the switches
|
||||
double const kSweepOutlineStep{1.0};
|
||||
double const kSweepNear{4.0}; // m, a model along a curve this close to the selected path belongs to its line
|
||||
double const kSweepGrab{25.0}; // m from the track, a press this close marks the stretch of the model
|
||||
std::size_t const kSweepModelList{400};
|
||||
double const kRailTop{0.18}; // m from the points of a track to the top of its rails
|
||||
|
||||
segment_data reversed(segment_data const &Path)
|
||||
{
|
||||
segment_data result{Path};
|
||||
result.points[segment_data::point::start] = Path.points[segment_data::point::end];
|
||||
result.points[segment_data::point::end] = Path.points[segment_data::point::start];
|
||||
result.points[segment_data::point::control1] = Path.points[segment_data::point::control2];
|
||||
result.points[segment_data::point::control2] = Path.points[segment_data::point::control1];
|
||||
result.rolls = {-Path.rolls[1], -Path.rolls[0]};
|
||||
return result;
|
||||
}
|
||||
|
||||
bool same_piece(segment_data const &A, segment_data const &B)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (glm::distance(A.points[i], B.points[i]) > 1e-3)
|
||||
return false;
|
||||
return std::abs(A.rolls[0] - B.rolls[0]) < 1e-3f && std::abs(A.rolls[1] - B.rolls[1]) < 1e-3f;
|
||||
}
|
||||
|
||||
std::string lower(std::string Text)
|
||||
{
|
||||
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
|
||||
return Text;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void editor_mode::sweep_scan_models()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
if (tool.scanned)
|
||||
return;
|
||||
tool.scanned = true;
|
||||
std::error_code error;
|
||||
std::set<std::string> found;
|
||||
for (auto const &entry : std::filesystem::recursive_directory_iterator("models", std::filesystem::directory_options::skip_permission_denied, error))
|
||||
{
|
||||
if (false == entry.is_regular_file(error))
|
||||
continue;
|
||||
auto const extension{lower(entry.path().extension().string())};
|
||||
if (extension != ".e3d" && extension != ".t3d")
|
||||
continue;
|
||||
auto name{std::filesystem::relative(entry.path(), "models", error).generic_string()};
|
||||
name.erase(name.size() - extension.size());
|
||||
found.insert(name);
|
||||
}
|
||||
for (auto const &entry : std::filesystem::recursive_directory_iterator("scenery", std::filesystem::directory_options::skip_permission_denied, error))
|
||||
{
|
||||
if (entry.is_regular_file(error) && lower(entry.path().extension().string()) == ".inc")
|
||||
found.insert(std::filesystem::relative(entry.path(), "scenery", error).generic_string());
|
||||
}
|
||||
tool.models.assign(found.begin(), found.end());
|
||||
}
|
||||
|
||||
|
||||
void editor_mode::sweep_curve()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
auto *track{selected_track()};
|
||||
if (track == tool.curve_for && tool.curve_history == m_history.size())
|
||||
return;
|
||||
tool.curve_for = track;
|
||||
tool.curve_history = m_history.size();
|
||||
tool.curve.clear();
|
||||
tool.curve_pieces.clear();
|
||||
if (track != nullptr && false == track->m_paths.empty() && editor_track::is_supported(*track))
|
||||
{
|
||||
tool.curve.push_back(track->m_paths.front());
|
||||
tool.curve_pieces.push_back({track, 0, true});
|
||||
}
|
||||
sweep_outline();
|
||||
}
|
||||
|
||||
void editor_mode::sweep_outline()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
tool.outline.clear();
|
||||
tool.stations.clear();
|
||||
tool.curve_length = 0.0;
|
||||
for (auto const &piece : tool.curve)
|
||||
{
|
||||
bezier const curve{piece};
|
||||
auto const count{std::clamp(static_cast<int>(curve.plan_length() / kSweepOutlineStep), 2, 2000)};
|
||||
for (int k = tool.outline.empty() ? 0 : 1; k <= count; ++k)
|
||||
{
|
||||
auto const point{curve.point(static_cast<double>(k) / count)};
|
||||
if (false == tool.outline.empty())
|
||||
tool.curve_length += plan_distance(tool.outline.back(), point);
|
||||
tool.outline.push_back(point);
|
||||
tool.stations.push_back(tool.curve_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool editor_mode::sweep_extend(bool const Atend, glm::dvec3 const &Cursor)
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
if (tool.curve_pieces.empty() || tool.curve_length > kSweepRouteReach)
|
||||
return false;
|
||||
auto const &edge{Atend ? tool.curve_pieces.back() : tool.curve_pieces.front()};
|
||||
auto const &edgepiece{Atend ? tool.curve.back() : tool.curve.front()};
|
||||
auto const point{Atend ? edgepiece.points[segment_data::point::end] : edgepiece.points[segment_data::point::start]};
|
||||
std::optional<sweep_tool::piece> best;
|
||||
segment_data bestpiece;
|
||||
auto bestdistance{std::numeric_limits<double>::max()};
|
||||
for (auto const &[other, end] : editor_track::connected_points(*edge.track, point))
|
||||
{
|
||||
if (other == nullptr || other->m_editorremoved || other->eType == tt_Table || false == editor_track::is_supported(*other) || end.path >= static_cast<int>(other->m_paths.size()))
|
||||
continue;
|
||||
if (std::any_of(tool.curve_pieces.begin(), tool.curve_pieces.end(), [&](sweep_tool::piece const &Piece) { return Piece.track == other && Piece.path == end.path; }))
|
||||
continue;
|
||||
auto const startshere{end.kind == editor_track::point_kind::start};
|
||||
auto const forward{Atend ? startshere : false == startshere};
|
||||
auto const piece{forward ? other->m_paths[end.path] : reversed(other->m_paths[end.path])};
|
||||
bezier const curve{piece};
|
||||
auto distance{std::numeric_limits<double>::max()};
|
||||
for (int k = 0; k <= 32; ++k)
|
||||
distance = std::min(distance, plan_distance(curve.point(k / 32.0), Cursor));
|
||||
if (distance < bestdistance)
|
||||
{
|
||||
bestdistance = distance;
|
||||
best = sweep_tool::piece{other, end.path, forward};
|
||||
bestpiece = piece;
|
||||
}
|
||||
}
|
||||
if (false == best.has_value())
|
||||
return false;
|
||||
if (Atend)
|
||||
{
|
||||
tool.curve_pieces.push_back(*best);
|
||||
tool.curve.push_back(bestpiece);
|
||||
sweep_outline();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const before{tool.curve_length};
|
||||
tool.curve_pieces.insert(tool.curve_pieces.begin(), *best);
|
||||
tool.curve.insert(tool.curve.begin(), bestpiece);
|
||||
sweep_outline();
|
||||
tool.anchor += tool.curve_length - before;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double editor_mode::sweep_station(glm::dvec3 const &Point, double &Lateral) const
|
||||
{
|
||||
auto const &tool{m_sweep};
|
||||
double best{std::numeric_limits<double>::max()};
|
||||
double station{0.0};
|
||||
Lateral = 0.0;
|
||||
auto const point{plan_of(Point)};
|
||||
for (std::size_t i = 1; i < tool.outline.size(); ++i)
|
||||
{
|
||||
auto const a{plan_of(tool.outline[i - 1])};
|
||||
auto const ab{plan_of(tool.outline[i]) - a};
|
||||
auto const length2{glm::dot(ab, ab)};
|
||||
if (length2 < 1e-12)
|
||||
continue;
|
||||
auto const t{std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0)};
|
||||
auto const distance{glm::length(point - (a + ab * t))};
|
||||
if (distance < best)
|
||||
{
|
||||
best = distance;
|
||||
station = tool.stations[i - 1] + (tool.stations[i] - tool.stations[i - 1]) * t;
|
||||
Lateral = geometry::cross(glm::normalize(ab), point - a) >= 0.0 ? distance : -distance;
|
||||
}
|
||||
}
|
||||
return station;
|
||||
}
|
||||
|
||||
void editor_mode::sweep_measure()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
std::string const model{tool.model};
|
||||
if (model == tool.measured)
|
||||
return;
|
||||
tool.measured = model;
|
||||
tool.model_length = 0.0;
|
||||
if (model.empty())
|
||||
return;
|
||||
scene::node_data data;
|
||||
sweep_node probe{data};
|
||||
auto state{sweep_definition()};
|
||||
state.pieces.clear();
|
||||
state.along_x = false;
|
||||
probe.define(state);
|
||||
auto const alongz{probe.model_length()};
|
||||
state.along_x = true;
|
||||
probe.define(state);
|
||||
auto const alongx{probe.model_length()};
|
||||
tool.settings.along_x = tool.axis == 2 || (tool.axis == 0 && alongx > alongz);
|
||||
tool.model_length = tool.settings.along_x ? alongx : alongz;
|
||||
}
|
||||
|
||||
sweep_node::state editor_mode::sweep_definition() const
|
||||
{
|
||||
auto const &tool{m_sweep};
|
||||
auto state{tool.settings};
|
||||
state.model = tool.model;
|
||||
state.skin = tool.skin[0] != '\0' ? tool.skin : "none";
|
||||
state.pieces = tool.curve;
|
||||
state.height = tool.settings.height + kRailTop;
|
||||
if (tool.length_mode == 0 && tool.model_length > 0.0)
|
||||
state.to = state.from + tool.model_length;
|
||||
else if (tool.length_mode == 1)
|
||||
state.to = state.from + std::max(0.5, tool.length);
|
||||
state.parameters.clear();
|
||||
for (auto const &value : tool.parameters)
|
||||
state.parameters.emplace_back(value[0] != '\0' ? value.data() : "none");
|
||||
return state;
|
||||
}
|
||||
|
||||
void editor_mode::sweep_create()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
sweep_curve();
|
||||
if (tool.curve.empty() || tool.model[0] == '\0')
|
||||
return;
|
||||
static int counter{0};
|
||||
scene::node_data data;
|
||||
data.type = "sweep";
|
||||
data.range_max = -1.0;
|
||||
data.layer = scene::Layers.active();
|
||||
do
|
||||
{
|
||||
data.name = "sweep_" + std::to_string(++counter);
|
||||
} while (simulation::Sweeps.find(data.name) != nullptr);
|
||||
auto *sweep{new sweep_node(data)};
|
||||
sweep->define(sweep_definition());
|
||||
sweep->mark_dirty();
|
||||
simulation::Sweeps.insert(sweep);
|
||||
scene::Layers.count(sweep->layer(), scene::layer_item::model);
|
||||
sweep->show();
|
||||
// the pieces are the paths of the line, so the model follows them when they change
|
||||
for (auto const &piece : tool.curve_pieces)
|
||||
if (piece.track != nullptr && false == piece.track->m_editorremoved)
|
||||
sweeps_captured(*piece.track);
|
||||
EditorSnapshot snap;
|
||||
snap.action = EditorSnapshot::Action::Other;
|
||||
snap.node_name = sweep->name();
|
||||
snap.sweeps_toggled.push_back(sweep);
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
tool.edited = sweep;
|
||||
tool.status = format(STR_C("%s laid along %.1f m, Ctrl+Z takes it back"), tool.model, sweep->end() - sweep->start());
|
||||
if (scene::Layers.active() == null_handle)
|
||||
tool.status += "\n" + std::string{STR_C("The scenery isn't opened for editing, it won't be saved")};
|
||||
WriteLog("Editor: " + tool.status, logtype::generic);
|
||||
}
|
||||
|
||||
void editor_mode::sweep_apply()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
auto *sweep{tool.edited};
|
||||
if (sweep == nullptr || sweep->m_editorremoved)
|
||||
return;
|
||||
auto state{sweep_definition()};
|
||||
state.pieces = sweep->definition().pieces;
|
||||
if (state == sweep->definition())
|
||||
return;
|
||||
EditorSnapshot snap;
|
||||
snap.action = EditorSnapshot::Action::Other;
|
||||
snap.node_name = sweep->name();
|
||||
snap.sweeps.emplace_back(sweep, sweep->definition());
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
sweep->define(state);
|
||||
sweep->mark_dirty();
|
||||
}
|
||||
|
||||
void editor_mode::sweep_delete(sweep_node &Sweep)
|
||||
{
|
||||
Sweep.m_editorremoved = true;
|
||||
Sweep.hide();
|
||||
Sweep.mark_dirty();
|
||||
EditorSnapshot snap;
|
||||
snap.action = EditorSnapshot::Action::Other;
|
||||
snap.node_name = Sweep.name();
|
||||
snap.sweeps_toggled.push_back(&Sweep);
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
if (m_sweep.edited == &Sweep)
|
||||
m_sweep.edited = nullptr;
|
||||
}
|
||||
|
||||
void editor_mode::sweep_edit(sweep_node &Sweep)
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
tool.edited = &Sweep;
|
||||
auto const &state{Sweep.definition()};
|
||||
tool.settings = state;
|
||||
tool.settings.height = state.height - kRailTop;
|
||||
tool.settings.pieces.clear();
|
||||
std::snprintf(tool.model, sizeof(tool.model), "%s", state.model.c_str());
|
||||
std::snprintf(tool.skin, sizeof(tool.skin), "%s", state.skin.c_str());
|
||||
tool.axis = state.along_x ? 2 : 1;
|
||||
tool.measured.clear();
|
||||
tool.parameters_for.clear();
|
||||
tool.parameters.clear();
|
||||
for (auto const &value : state.parameters)
|
||||
{
|
||||
tool.parameters.emplace_back();
|
||||
std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", value.c_str());
|
||||
}
|
||||
tool.parameters_for = state.model;
|
||||
}
|
||||
|
||||
void editor_mode::restore_sweeps(EditorSnapshot &Snapshot)
|
||||
{
|
||||
for (auto &entry : Snapshot.sweeps)
|
||||
{
|
||||
auto current{entry.first->definition()};
|
||||
entry.first->define(entry.second);
|
||||
entry.first->mark_dirty();
|
||||
entry.second = std::move(current);
|
||||
}
|
||||
for (auto *sweep : Snapshot.sweeps_toggled)
|
||||
{
|
||||
sweep->m_editorremoved = false == sweep->m_editorremoved;
|
||||
if (sweep->m_editorremoved)
|
||||
sweep->hide();
|
||||
else
|
||||
sweep->show();
|
||||
sweep->mark_dirty();
|
||||
if (m_sweep.edited == sweep && sweep->m_editorremoved)
|
||||
m_sweep.edited = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<sweep_node *> editor_mode::sweeps_near(TTrack const &Track) const
|
||||
{
|
||||
std::vector<sweep_node *> result;
|
||||
std::vector<glm::dvec3> points;
|
||||
for (auto const &path : Track.m_paths)
|
||||
{
|
||||
bezier const curve{path};
|
||||
for (int k = 0; k <= 8; ++k)
|
||||
points.push_back(curve.point(k / 8.0));
|
||||
}
|
||||
for (auto *sweep : simulation::Sweeps.sequence())
|
||||
{
|
||||
if (sweep == nullptr || sweep->m_editorremoved)
|
||||
continue;
|
||||
auto const &definition{sweep->definition()};
|
||||
bool near{false};
|
||||
for (auto const &piece : definition.pieces)
|
||||
{
|
||||
bezier const curve{piece};
|
||||
for (int k = 0; k <= 8 && false == near; ++k)
|
||||
{
|
||||
auto const point{curve.point(k / 8.0)};
|
||||
near = std::any_of(points.begin(), points.end(), [&](glm::dvec3 const &Other) { return plan_distance(point, Other) < kSweepNear; });
|
||||
}
|
||||
if (near)
|
||||
break;
|
||||
}
|
||||
if (near)
|
||||
result.push_back(sweep);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool editor_mode::sweep_press()
|
||||
{
|
||||
return sweep_press_at(Global.pCamera.Pos + GfxRenderer->Mouse_Position(), false);
|
||||
}
|
||||
|
||||
bool editor_mode::sweep_grabs(glm::dvec3 const &Point)
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
if (false == tool.open || tool.model[0] == '\0')
|
||||
return false;
|
||||
sweep_curve();
|
||||
if (tool.outline.size() < 2)
|
||||
return false;
|
||||
sweep_measure();
|
||||
if (tool.model_length <= 0.0)
|
||||
return false;
|
||||
double lateral;
|
||||
sweep_station(Point, lateral);
|
||||
return std::abs(lateral) <= kSweepGrab;
|
||||
}
|
||||
|
||||
bool editor_mode::sweep_press_at(glm::dvec3 const &Point, bool const Anywhere)
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
sweep_curve();
|
||||
if (false == tool.open || tool.outline.size() < 2 || tool.model[0] == '\0')
|
||||
return false;
|
||||
sweep_measure();
|
||||
if (tool.model_length <= 0.0)
|
||||
return false;
|
||||
auto *track{selected_track()};
|
||||
if (track != nullptr && track->eType == tt_Switch && track->m_paths.size() > 1 && tool.curve_pieces.size() == 1)
|
||||
{
|
||||
auto nearest{0};
|
||||
auto best{std::numeric_limits<double>::max()};
|
||||
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
||||
{
|
||||
bezier const curve{track->m_paths[i]};
|
||||
for (int k = 0; k <= 32; ++k)
|
||||
if (auto const distance{plan_distance(curve.point(k / 32.0), Point)}; distance < best)
|
||||
{
|
||||
best = distance;
|
||||
nearest = i;
|
||||
}
|
||||
}
|
||||
tool.curve = {track->m_paths[nearest]};
|
||||
tool.curve_pieces = {{track, nearest, true}};
|
||||
sweep_outline();
|
||||
}
|
||||
double lateral;
|
||||
auto const station{sweep_station(Point, lateral)};
|
||||
// a press away from the track is left to the selection of the tracks
|
||||
if (false == Anywhere && std::abs(lateral) > kSweepGrab)
|
||||
return false;
|
||||
// the press tells the side, the distance from the track is the one set
|
||||
auto const distance{std::abs(tool.settings.lateral) > 1e-6 ? std::abs(tool.settings.lateral) : std::round(std::abs(lateral) / 0.05) * 0.05};
|
||||
tool.settings.lateral = lateral >= 0.0 ? distance : -distance;
|
||||
if (tool.settings.side_anchor == 0)
|
||||
tool.settings.side_anchor = 1;
|
||||
tool.anchor = std::round(station * 10.0) / 10.0;
|
||||
tool.settings.from = tool.settings.to = tool.anchor;
|
||||
tool.dragging = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void editor_mode::sweep_drag()
|
||||
{
|
||||
if (m_sweep.dragging)
|
||||
sweep_drag_at(Global.pCamera.Pos + GfxRenderer->Mouse_Position());
|
||||
}
|
||||
|
||||
void editor_mode::sweep_drag_at(glm::dvec3 const &Point)
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
if (false == tool.dragging)
|
||||
return;
|
||||
double lateral;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
auto const raw{sweep_station(Point, lateral)};
|
||||
if (raw >= tool.curve_length - 0.5 && sweep_extend(true, Point))
|
||||
continue;
|
||||
if (raw <= 0.5 && sweep_extend(false, Point))
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
auto const station{std::round(sweep_station(Point, lateral) * 10.0) / 10.0};
|
||||
tool.settings.from = std::min(tool.anchor, station);
|
||||
tool.settings.to = std::max(tool.anchor, station);
|
||||
}
|
||||
|
||||
void editor_mode::sweep_release()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
if (false == tool.dragging)
|
||||
return;
|
||||
tool.dragging = false;
|
||||
// a click lays one copy as long as the original from the place clicked, a drag the stretch dragged over
|
||||
if (tool.settings.to - tool.settings.from < 1.0)
|
||||
{
|
||||
tool.length_mode = 0;
|
||||
tool.settings.from = tool.anchor;
|
||||
}
|
||||
else
|
||||
{
|
||||
tool.length_mode = 1;
|
||||
tool.length = tool.settings.to - tool.settings.from;
|
||||
}
|
||||
if (tool.edited == nullptr)
|
||||
{
|
||||
sweep_create();
|
||||
return;
|
||||
}
|
||||
// the model being edited is laid again along the line, over the stretch marked
|
||||
auto state{sweep_definition()};
|
||||
EditorSnapshot snap;
|
||||
snap.action = EditorSnapshot::Action::Other;
|
||||
snap.node_name = tool.edited->name();
|
||||
snap.sweeps.emplace_back(tool.edited, tool.edited->definition());
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
tool.edited->define(state);
|
||||
tool.edited->mark_dirty();
|
||||
if (auto *track{selected_track()})
|
||||
sweeps_captured(*track);
|
||||
tool.status = format(STR_C("%s laid along %.1f m, Ctrl+Z takes it back"), tool.model, tool.edited->end() - tool.edited->start());
|
||||
}
|
||||
|
||||
void editor_mode::sweeps_captured(TTrack const &Track)
|
||||
{
|
||||
for (auto *sweep : simulation::Sweeps.sequence())
|
||||
{
|
||||
if (sweep == nullptr || sweep->m_editorremoved)
|
||||
continue;
|
||||
auto const &pieces{sweep->definition().pieces};
|
||||
for (std::size_t i = 0; i < pieces.size(); ++i)
|
||||
for (int path = 0; path < static_cast<int>(Track.m_paths.size()); ++path)
|
||||
{
|
||||
bool const along{same_piece(pieces[i], Track.m_paths[path])};
|
||||
bool const against{false == along && same_piece(pieces[i], reversed(Track.m_paths[path]))};
|
||||
if (false == along && false == against)
|
||||
continue;
|
||||
if (std::none_of(m_sweep_links.begin(), m_sweep_links.end(), [&](sweep_link const &Link) { return Link.sweep == sweep && Link.piece == i; }))
|
||||
m_sweep_links.push_back({sweep, i, const_cast<TTrack *>(&Track), path, against});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::sweeps_committed(std::vector<TTrack *> const &Tracks)
|
||||
{
|
||||
std::vector<sweep_node *> changed;
|
||||
std::vector<std::pair<sweep_node *, sweep_node::state>> states;
|
||||
for (auto const &link : m_sweep_links)
|
||||
{
|
||||
if (link.track == nullptr || link.track->m_editorremoved || link.sweep->m_editorremoved || std::find(Tracks.begin(), Tracks.end(), link.track) == Tracks.end() ||
|
||||
link.path >= static_cast<int>(link.track->m_paths.size()))
|
||||
continue;
|
||||
auto entry{std::find_if(states.begin(), states.end(), [&](auto const &State) { return State.first == link.sweep; })};
|
||||
if (entry == states.end())
|
||||
{
|
||||
states.emplace_back(link.sweep, link.sweep->definition());
|
||||
entry = std::prev(states.end());
|
||||
}
|
||||
if (link.piece >= entry->second.pieces.size())
|
||||
continue;
|
||||
auto const &path{link.track->m_paths[link.path]};
|
||||
entry->second.pieces[link.piece] = link.reversed ? reversed(path) : path;
|
||||
}
|
||||
for (auto &entry : states)
|
||||
{
|
||||
if (entry.second == entry.first->definition())
|
||||
continue;
|
||||
entry.first->define(entry.second);
|
||||
entry.first->mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void editor_mode::sweeps_split(TTrack &Original, TTrack &Created)
|
||||
{
|
||||
if (Created.m_paths.empty())
|
||||
return;
|
||||
std::vector<sweep_link> added;
|
||||
for (auto &link : m_sweep_links)
|
||||
{
|
||||
if (link.track != &Original || link.path != 0)
|
||||
continue;
|
||||
auto state{link.sweep->definition()};
|
||||
if (link.piece >= state.pieces.size())
|
||||
continue;
|
||||
// the second part goes after the first one along the curve, or ahead of it on a curve laid against the path
|
||||
auto const at{link.piece + (link.reversed ? 0 : 1)};
|
||||
state.pieces.insert(state.pieces.begin() + at, link.reversed ? reversed(Created.m_paths.front()) : Created.m_paths.front());
|
||||
for (auto &other : m_sweep_links)
|
||||
if (other.sweep == link.sweep && other.piece >= at && &other != &link)
|
||||
++other.piece;
|
||||
if (link.reversed)
|
||||
++link.piece;
|
||||
added.push_back({link.sweep, at, &Created, 0, link.reversed});
|
||||
link.sweep->define(state);
|
||||
}
|
||||
m_sweep_links.insert(m_sweep_links.end(), added.begin(), added.end());
|
||||
}
|
||||
|
||||
void editor_mode::render_sweep_ui()
|
||||
{
|
||||
auto &tool{m_sweep};
|
||||
auto *track{selected_track()};
|
||||
sweep_curve();
|
||||
sweep_scan_models();
|
||||
ImGui::TextDisabled("%s", STR_C("A model bent to follow the track, or repeated along it: platforms, walls, fences, barriers, lamps"));
|
||||
|
||||
// models along curves by this line
|
||||
if (track != nullptr)
|
||||
{
|
||||
auto const near{sweeps_near(*track)};
|
||||
if (false == near.empty())
|
||||
{
|
||||
ImGui::TextDisabled("%s", STR_C("By this track:"));
|
||||
for (auto *sweep : near)
|
||||
{
|
||||
ImGui::PushID(sweep);
|
||||
auto const &definition{sweep->definition()};
|
||||
if (ImGui::Selectable(format("%s %s, %.0f m", sweep->name().c_str(), definition.model.c_str(), sweep->end() - sweep->start()).c_str(), tool.edited == sweep, 0, ImVec2(ImGui::GetContentRegionAvail().x - 60.0f, 0.0f)))
|
||||
sweep_edit(*sweep);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(STR_C("Delete")))
|
||||
sweep_delete(*sweep);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (tool.edited != nullptr && ImGui::SmallButton(STR_C("New one")))
|
||||
tool.edited = nullptr;
|
||||
ImGui::Separator();
|
||||
}
|
||||
}
|
||||
bool changed{false};
|
||||
auto const edited = [&](bool const Changed) { changed |= Changed; };
|
||||
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::InputTextWithHint("##sweepmodel", STR_C("model, e.g. peronowe/1k96w200m"), tool.model, sizeof(tool.model)))
|
||||
changed = true;
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
ImGui::InputTextWithHint("##sweepsearch", STR_C("search the models"), tool.search, sizeof(tool.search));
|
||||
std::string const text{lower(tool.search)};
|
||||
if (false == text.empty())
|
||||
{
|
||||
ImGui::BeginChild("##sweepmodels", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 7.0f + 6.0f), true);
|
||||
std::size_t listed{0};
|
||||
for (auto const &model : tool.models)
|
||||
{
|
||||
if (lower(model).find(text) == std::string::npos)
|
||||
continue;
|
||||
if (++listed > kSweepModelList)
|
||||
break;
|
||||
if (ImGui::Selectable(model.c_str(), model == tool.model))
|
||||
{
|
||||
std::snprintf(tool.model, sizeof(tool.model), "%s", model.c_str());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
// a template takes its parameters, the ones which place it are zeros: it's laid out in its own space
|
||||
std::string const model{tool.model};
|
||||
bool const istemplate{model.size() > 4 && lower(model.substr(model.size() - 4)) == ".inc"};
|
||||
if (istemplate && model != tool.parameters_for)
|
||||
{
|
||||
tool.parameters_for = model;
|
||||
include_info info;
|
||||
std::string error;
|
||||
editor_includes::load(model, info, error);
|
||||
editor_includes::suggest(model, info);
|
||||
auto const count{editor_includes::parameter_count(model)};
|
||||
tool.parameters.assign(static_cast<std::size_t>(count), {});
|
||||
tool.parameter_labels.assign(static_cast<std::size_t>(count), {});
|
||||
tool.parameter_placement.assign(static_cast<std::size_t>(count), false);
|
||||
tool.parameter_choices.assign(static_cast<std::size_t>(count), {});
|
||||
// a parameter which makes a part of the name of a texture gets the variants of that texture to choose from
|
||||
{
|
||||
std::ifstream file(Global.asCurrentSceneryPath + model);
|
||||
std::stringstream content;
|
||||
content << file.rdbuf();
|
||||
std::string word;
|
||||
while (content >> word)
|
||||
{
|
||||
auto const open{word.find("(p")};
|
||||
auto const close{open != std::string::npos ? word.find(')', open) : std::string::npos};
|
||||
if (close == std::string::npos || word.find('/') == std::string::npos || word.find('/') > open)
|
||||
continue;
|
||||
auto const id{std::atoi(word.substr(open + 2, close - open - 2).c_str())};
|
||||
if (id < 1 || id > count || false == tool.parameter_choices[id - 1].empty())
|
||||
continue;
|
||||
auto const prefix{lower(word.substr(0, open))};
|
||||
auto suffix{word.substr(close + 1)};
|
||||
suffix = suffix.substr(0, suffix.find(':'));
|
||||
auto const slash{prefix.rfind('/')};
|
||||
auto const folder{std::string{"textures/"} + prefix.substr(0, slash)};
|
||||
auto const start{prefix.substr(slash + 1)};
|
||||
std::set<std::string> variants;
|
||||
std::error_code error;
|
||||
for (auto const &entry : std::filesystem::directory_iterator(folder, error))
|
||||
{
|
||||
auto const stem{lower(entry.path().stem().string())};
|
||||
if (stem.size() > start.size() + suffix.size() && stem.compare(0, start.size(), start) == 0 && (suffix.empty() || stem.compare(stem.size() - suffix.size(), suffix.size(), lower(suffix)) == 0))
|
||||
variants.insert(stem.substr(start.size(), stem.size() - start.size() - suffix.size()));
|
||||
}
|
||||
tool.parameter_choices[id - 1].assign(variants.begin(), variants.end());
|
||||
}
|
||||
}
|
||||
for (int id = 1; id <= count; ++id)
|
||||
{
|
||||
auto const ¶meter{info.parameter(id)};
|
||||
auto const placement{parameter.role.rfind("pos.", 0) == 0 || parameter.role.rfind("rot.", 0) == 0};
|
||||
auto const value{placement ? std::string{"0"} : parameter.value.empty() ? std::string{"none"} : parameter.value};
|
||||
std::snprintf(tool.parameters[id - 1].data(), tool.parameters[id - 1].size(), "%s", value.c_str());
|
||||
tool.parameter_labels[id - 1] = parameter.label.empty() ? "p" + std::to_string(id) + (parameter.role != "free" ? " (" + parameter.role + ")" : std::string{}) : parameter.label;
|
||||
tool.parameter_placement[id - 1] = placement;
|
||||
}
|
||||
tool.measured.clear();
|
||||
changed = true;
|
||||
}
|
||||
else if (false == istemplate && false == tool.parameters.empty())
|
||||
{
|
||||
tool.parameters.clear();
|
||||
tool.parameters_for.clear();
|
||||
}
|
||||
if (false == tool.parameters.empty() && ImGui::TreeNodeEx(STR_C("Parameters of the template"), ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
for (std::size_t i = 0; i < tool.parameters.size(); ++i)
|
||||
{
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
auto const label{i < tool.parameter_labels.size() ? tool.parameter_labels[i] : "p" + std::to_string(i + 1)};
|
||||
auto const *choices{i < tool.parameter_choices.size() && false == tool.parameter_choices[i].empty() ? &tool.parameter_choices[i] : nullptr};
|
||||
if (choices != nullptr && ImGui::BeginCombo(label.c_str(), tool.parameters[i].data()))
|
||||
{
|
||||
for (auto const &choice : *choices)
|
||||
if (ImGui::Selectable(choice.c_str(), choice == tool.parameters[i].data()))
|
||||
{
|
||||
std::snprintf(tool.parameters[i].data(), tool.parameters[i].size(), "%s", choice.c_str());
|
||||
tool.measured.clear();
|
||||
changed = true;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
else if (choices == nullptr)
|
||||
{
|
||||
ImGui::InputText(label.c_str(), tool.parameters[i].data(), tool.parameters[i].size());
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
tool.measured.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::TextDisabled("%s", STR_C("The location and the rotation are zeros, the template is laid out along the track"));
|
||||
ImGui::TreePop();
|
||||
}
|
||||
sweep_measure();
|
||||
if (tool.model_length > 0.0)
|
||||
ImGui::TextDisabled(STR_C("The model is %.2f m long along the track"), tool.model_length);
|
||||
else if (tool.model[0] != '\0')
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("The model has no triangles to lay, or it can't be loaded"));
|
||||
|
||||
auto &settings{tool.settings};
|
||||
int mode{settings.bend ? 0 : 1};
|
||||
edited(ImGui::RadioButton(STR_C("Bent along the track"), &mode, 0));
|
||||
ImGui::SameLine();
|
||||
edited(ImGui::RadioButton(STR_C("Repeated"), &mode, 1));
|
||||
settings.bend = mode == 0;
|
||||
if (false == settings.bend)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80.0f);
|
||||
edited(ImGui::InputDouble(STR_C("every (m, 0: its length)"), &settings.step, 0.0, 0.0, "%.2f"));
|
||||
settings.step = std::max(0.0, settings.step);
|
||||
}
|
||||
ImGui::TextDisabled("%s", STR_C("Platform edge:"));
|
||||
for (auto const height : {0.55, 0.76, 0.96})
|
||||
{
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(format("%.2f m##platform", height).c_str()))
|
||||
{
|
||||
settings.lateral = std::copysign(1.725, settings.lateral == 0.0 ? 1.0 : settings.lateral);
|
||||
settings.side_anchor = 1;
|
||||
settings.height = height;
|
||||
settings.height_anchor = 3;
|
||||
settings.face = true;
|
||||
settings.widen = true;
|
||||
changed = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("The edge of the platform 1.725 m from the axis of the track, its top this high over the rail top"));
|
||||
}
|
||||
ImGui::TextDisabled(STR_C("%.2f m %s of the axis, %.2f m over the rail top"), std::abs(settings.lateral), settings.lateral >= 0.0 ? STR_C("right") : STR_C("left"), settings.height);
|
||||
ImGui::TextWrapped("%s", STR_C("Press beside the track where the model begins and drag along to where it ends; the side of the press is the side of the model. "
|
||||
"Past the end of the path it goes on along the joined ones. A click lays one copy."));
|
||||
|
||||
if (tool.edited != nullptr && changed)
|
||||
sweep_apply();
|
||||
if (tool.edited == nullptr && false == tool.curve.empty() && tool.model_length > 0.0 && ImGui::Button(STR_C("Lay it along the selected path")))
|
||||
{
|
||||
tool.length_mode = 2;
|
||||
settings.from = 0.0;
|
||||
settings.to = -1.0;
|
||||
sweep_create();
|
||||
}
|
||||
if (tool.edited != nullptr && false == tool.curve.empty() && ImGui::Button(STR_C("Lay it again along the selected line")))
|
||||
{
|
||||
auto state{sweep_definition()};
|
||||
EditorSnapshot snap;
|
||||
snap.action = EditorSnapshot::Action::Other;
|
||||
snap.node_name = tool.edited->name();
|
||||
snap.sweeps.emplace_back(tool.edited, tool.edited->definition());
|
||||
trim_history();
|
||||
m_history.push_back(std::move(snap));
|
||||
g_redo.clear();
|
||||
tool.edited->define(state);
|
||||
tool.edited->mark_dirty();
|
||||
if (track != nullptr)
|
||||
sweeps_captured(*track);
|
||||
}
|
||||
|
||||
changed = false;
|
||||
if (ImGui::CollapsingHeader(STR_C("More settings")))
|
||||
{
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
edited(ImGui::InputText(STR_C("Skin"), tool.skin, sizeof(tool.skin), ImGuiInputTextFlags_EnterReturnsTrue));
|
||||
edited(ImGui::IsItemDeactivatedAfterEdit());
|
||||
ImGui::PushItemWidth(90.0f);
|
||||
edited(ImGui::InputDouble(STR_C("Sideways (m, + right)"), &settings.lateral, 0.0, 0.0, "%.3f"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle")};
|
||||
edited(ImGui::Combo("##sweepsideanchor", &settings.side_anchor, sides, IM_ARRAYSIZE(sides)));
|
||||
edited(ImGui::InputDouble(STR_C("Up (m, from the rail top)"), &settings.height, 0.0, 0.0, "%.3f"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track")};
|
||||
edited(ImGui::Combo("##sweepheightanchor", &settings.height_anchor, heights, IM_ARRAYSIZE(heights)));
|
||||
if (settings.bend)
|
||||
{
|
||||
edited(ImGui::InputDouble(STR_C("Copy length (m, 0: the model's)"), &settings.step, 0.0, 0.0, "%.2f"));
|
||||
settings.step = std::max(0.0, settings.step);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
char const *axes[] = {"auto", "z", "x"};
|
||||
ImGui::SetNextItemWidth(70.0f);
|
||||
if (ImGui::Combo(STR_C("Axis of the model along the track"), &tool.axis, axes, IM_ARRAYSIZE(axes)))
|
||||
{
|
||||
tool.measured.clear();
|
||||
changed = true;
|
||||
}
|
||||
edited(ImGui::Checkbox(STR_C("Edge to the track"), &settings.face));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("The model is turned so its -x side faces the track, the way the platforms and the walls of the scenery are made"));
|
||||
ImGui::SameLine();
|
||||
edited(ImGui::Checkbox(STR_C("Turned around"), &settings.flip));
|
||||
ImGui::SameLine();
|
||||
edited(ImGui::Checkbox(STR_C("Mirrored"), &settings.mirror));
|
||||
ImGui::SameLine();
|
||||
edited(ImGui::Checkbox(STR_C("Leans with the cant"), &settings.tilt));
|
||||
edited(ImGui::Checkbox(STR_C("Set back by the widening of the structure gauge in the curves"), &settings.widen));
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\n"
|
||||
"more in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\n"
|
||||
"it begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check"));
|
||||
ImGui::TextDisabled("%s", STR_C("Length"));
|
||||
ImGui::SameLine();
|
||||
edited(ImGui::RadioButton(STR_C("as the original"), &tool.length_mode, 0));
|
||||
ImGui::SameLine();
|
||||
edited(ImGui::RadioButton(STR_C("given"), &tool.length_mode, 1));
|
||||
ImGui::SameLine();
|
||||
edited(ImGui::RadioButton(STR_C("the whole stretch"), &tool.length_mode, 2));
|
||||
ImGui::PushItemWidth(90.0f);
|
||||
edited(ImGui::InputDouble(STR_C("From (m)"), &settings.from, 0.0, 0.0, "%.1f"));
|
||||
ImGui::SameLine();
|
||||
if (tool.length_mode == 1)
|
||||
edited(ImGui::InputDouble(STR_C("Length (m)"), &tool.length, 0.0, 0.0, "%.1f"));
|
||||
else if (tool.length_mode == 2)
|
||||
edited(ImGui::InputDouble(STR_C("To (m, -1: the end)"), &settings.to, 0.0, 0.0, "%.1f"));
|
||||
ImGui::PopItemWidth();
|
||||
settings.from = std::max(0.0, settings.from);
|
||||
if (tool.model_length > 0.0)
|
||||
{
|
||||
auto const state{sweep_definition()};
|
||||
auto const end{state.to < 0.0 ? tool.curve_length : std::min(state.to, tool.curve_length)};
|
||||
auto const stretch{std::max(0.0, end - state.from)};
|
||||
auto const own{settings.step > 0.01 ? settings.step : tool.model_length};
|
||||
auto const copies{settings.bend ? std::max(1, static_cast<int>(std::round(stretch / own))) : static_cast<int>(std::floor(stretch / own + 1e-6)) + 1};
|
||||
if (settings.bend)
|
||||
ImGui::TextDisabled(STR_C("%.1f m: %d copies of %.2f m (the original %.2f m)"), stretch, copies, stretch / copies, tool.model_length);
|
||||
else
|
||||
ImGui::TextDisabled(STR_C("%.1f m: %d copies every %.2f m"), stretch, copies, own);
|
||||
}
|
||||
if (tool.edited != nullptr && changed)
|
||||
sweep_apply();
|
||||
}
|
||||
if (false == tool.status.empty())
|
||||
ImGui::TextWrapped("%s", tool.status.c_str());
|
||||
}
|
||||
|
||||
void editor_mode::draw_sweep_overlay() const
|
||||
{
|
||||
auto const &tool{m_sweep};
|
||||
if (false == tool.open || tool.outline.size() < 2)
|
||||
return;
|
||||
screen_projection const projection;
|
||||
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
||||
auto const &settings{tool.settings};
|
||||
auto const from{std::max(0.0, settings.from)};
|
||||
auto const to{settings.to < 0.0 ? tool.curve_length : std::min(settings.to, tool.curve_length)};
|
||||
glm::dvec3 previous{0.0};
|
||||
bool started{false};
|
||||
for (std::size_t i = 1; i < tool.outline.size(); ++i)
|
||||
{
|
||||
if (tool.stations[i] < from || tool.stations[i - 1] > to)
|
||||
continue;
|
||||
auto const direction{plan_of(tool.outline[i] - tool.outline[i - 1])};
|
||||
if (glm::length(direction) < 1e-9)
|
||||
continue;
|
||||
auto const unit{glm::normalize(direction)};
|
||||
glm::dvec3 const left{-unit.y, 0.0, unit.x};
|
||||
auto const point{tool.outline[i] + left * settings.lateral + glm::dvec3{0.0, settings.height + kRailTop, 0.0}};
|
||||
if (started)
|
||||
projection.line(drawlist, previous, point, IM_COL32(255, 160, 40, 230), 3.0f);
|
||||
else
|
||||
{
|
||||
ImVec2 screen;
|
||||
if (projection.project(point, screen))
|
||||
drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(255, 160, 40, 230));
|
||||
}
|
||||
previous = point;
|
||||
started = true;
|
||||
}
|
||||
ImVec2 screen;
|
||||
if (started && projection.project(previous, screen))
|
||||
drawlist->AddCircle(screen, 6.0f, IM_COL32(255, 160, 40, 230), 12, 2.0f);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1359
application/editormodetrackux.cpp
Normal file
1359
application/editormodetrackux.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1025
application/editormodeturntable.cpp
Normal file
1025
application/editormodeturntable.cpp
Normal file
File diff suppressed because it is too large
Load Diff
315
application/editormodevehicle.cpp
Normal file
315
application/editormodevehicle.cpp
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
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/editormode.h"
|
||||
#include "application/editoruilayer.h"
|
||||
#include "application/editorprojection.h"
|
||||
#include "application/application.h"
|
||||
#include "editor/editorFormat.hpp"
|
||||
#include "editor/editorGeometry.hpp"
|
||||
#include "launcher/textures_scanner.h"
|
||||
|
||||
#include "simulation/simulation.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "vehicle/DynObj.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "utilities/translation.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <optional>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using geometry::bezier;
|
||||
|
||||
std::size_t const kVehicleList{300};
|
||||
double const kHalfVehicle{8.0}; // m, the vehicle is placed about centred on the point
|
||||
|
||||
std::string lower(std::string Text)
|
||||
{
|
||||
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
|
||||
return Text;
|
||||
}
|
||||
|
||||
std::string skin_label(ui::vehicle_desc const &Vehicle, ui::skin_set const &Skin)
|
||||
{
|
||||
auto label{Skin.meta != nullptr && false == Skin.meta->name.empty() ? Skin.meta->name : Skin.skin};
|
||||
return label + " (" + Vehicle.path.generic_string() + ")";
|
||||
}
|
||||
|
||||
int path_under(TTrack const &Track, glm::dvec3 const &Point)
|
||||
{
|
||||
int result{0};
|
||||
auto best{std::numeric_limits<double>::max()};
|
||||
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
|
||||
{
|
||||
bezier const curve{Track.m_paths[i]};
|
||||
for (int k = 0; k <= 64; ++k)
|
||||
{
|
||||
auto const distance{geometry::plan_distance(curve.point(k / 64.0), Point)};
|
||||
if (distance < best)
|
||||
{
|
||||
best = distance;
|
||||
result = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
double nearest_on(TTrack const &Track, int const Path, glm::dvec3 const &Point)
|
||||
{
|
||||
bezier const curve{Track.m_paths[Path]};
|
||||
auto parameter{0.0};
|
||||
auto best{std::numeric_limits<double>::max()};
|
||||
for (int k = 0; k <= 256; ++k)
|
||||
{
|
||||
auto const distance{geometry::plan_distance(curve.point(k / 256.0), Point)};
|
||||
if (distance < best)
|
||||
{
|
||||
best = distance;
|
||||
parameter = k / 256.0;
|
||||
}
|
||||
}
|
||||
return parameter;
|
||||
}
|
||||
|
||||
glm::dvec2 heading_at(TTrack const &Track, int const Path, glm::dvec3 const &Point)
|
||||
{
|
||||
auto const tangent{bezier{Track.m_paths[Path]}.first(nearest_on(Track, Path, Point))};
|
||||
glm::dvec2 const planar{tangent.x, tangent.z};
|
||||
return glm::length(planar) > 1e-9 ? glm::normalize(planar) : glm::dvec2{0.0, 1.0};
|
||||
}
|
||||
|
||||
// distance along the path of the track from its start to the point nearest to the one given
|
||||
double distance_along(TTrack const &Track, int const Path, glm::dvec3 const &Point)
|
||||
{
|
||||
bezier const curve{Track.m_paths[Path]};
|
||||
auto const parameter{nearest_on(Track, Path, Point)};
|
||||
int const count{64};
|
||||
double length{0.0}, total{0.0};
|
||||
auto previous{curve.point(0.0)};
|
||||
for (int k = 1; k <= count; ++k)
|
||||
{
|
||||
auto const t{static_cast<double>(k) / count};
|
||||
auto const next{curve.point(t)};
|
||||
auto const step{glm::distance(previous, next)};
|
||||
total += step;
|
||||
if (t <= parameter)
|
||||
length += step;
|
||||
else if (t - 1.0 / count < parameter)
|
||||
length += step * (parameter - (t - 1.0 / count)) * count;
|
||||
previous = next;
|
||||
}
|
||||
return std::clamp(length * Track.Length() / std::max(total, 1e-6), 0.0, Track.Length());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void editor_mode::vehicle_start(TTrack &Track, glm::dvec3 const &Point)
|
||||
{
|
||||
m_vehicle.track = &Track;
|
||||
m_vehicle.point = Point;
|
||||
m_vehicle.expand = true;
|
||||
m_vehicle.status.clear();
|
||||
auto const heading{heading_at(Track, Track.eType == tt_Switch ? path_under(Track, Point) : 0, Point)};
|
||||
glm::dvec2 const away{Point.x - Global.pCamera.Pos.x, Point.z - Global.pCamera.Pos.z};
|
||||
m_vehicle.facing = glm::dot(heading, away) >= 0.0 ? 1 : -1;
|
||||
}
|
||||
|
||||
bool editor_mode::vehicle_place()
|
||||
{
|
||||
auto &tool{m_vehicle};
|
||||
if (tool.track == nullptr || tool.track->m_editorremoved)
|
||||
return false;
|
||||
auto train{tool.consist};
|
||||
if (train.empty())
|
||||
{
|
||||
if (tool.vehicle == nullptr || tool.skin == nullptr)
|
||||
return false;
|
||||
train.push_back({tool.vehicle, tool.skin, false});
|
||||
}
|
||||
auto const path{tool.track->eType == tt_Switch ? path_under(*tool.track, tool.point) : 0};
|
||||
if (tool.track->eType == tt_Switch && tool.track->GetSwitchState() != path)
|
||||
tool.track->Switch(path);
|
||||
auto const along{distance_along(*tool.track, path, tool.point)};
|
||||
auto const against{tool.facing < 0};
|
||||
std::vector<std::string> names;
|
||||
std::string nodes;
|
||||
std::string head;
|
||||
for (std::size_t i = 0; i < train.size(); ++i)
|
||||
{
|
||||
auto const &item{train[i]};
|
||||
auto const stem{item.vehicle->path.stem().generic_string()};
|
||||
std::string name;
|
||||
for (int index = 1; index < 10000; ++index)
|
||||
{
|
||||
name = lower("editor_" + stem + "_" + std::to_string(index));
|
||||
if (simulation::Vehicles.find(name) == nullptr && std::find(names.begin(), names.end(), name) == names.end())
|
||||
break;
|
||||
}
|
||||
names.push_back(name);
|
||||
auto const leads{i == 0};
|
||||
if (leads)
|
||||
head = name;
|
||||
nodes += "node -1 0 " + name + " dynamic " + item.vehicle->path.parent_path().generic_string() + " " + item.skin->skin + " " + stem + " " + (item.turned ? "-1" : "0") + " " + (leads && tool.driver == 0 ? "headdriver" : "nobody") + " 3 0 enddynamic\n";
|
||||
}
|
||||
auto const offset{against ? along - kHalfVehicle : along + kHalfVehicle};
|
||||
auto const vehicles{simulation::State.insert_trainset(head, tool.track, offset, nodes, against)};
|
||||
if (vehicles.empty())
|
||||
{
|
||||
tool.status = STR_C("The vehicle couldn't be placed, the log says why");
|
||||
return false;
|
||||
}
|
||||
tool.placed = head;
|
||||
tool.status = format(STR_C("%s and %d vehicle(s) behind it stand on %s. It isn't saved with the scenery"), head.c_str(), static_cast<int>(vehicles.size()) - 1, tool.track->name().empty() ? "(noname)" : tool.track->name().c_str());
|
||||
WriteLog("Editor: " + tool.status, logtype::generic);
|
||||
return true;
|
||||
}
|
||||
|
||||
void editor_mode::render_vehicle_ui()
|
||||
{
|
||||
auto &tool{m_vehicle};
|
||||
auto *selected{selected_track()};
|
||||
if (selected != nullptr && selected != tool.track)
|
||||
vehicle_start(*selected, bezier{selected->m_paths.front()}.point(0.5));
|
||||
if (tool.track == nullptr)
|
||||
return;
|
||||
if (tool.bank == nullptr)
|
||||
{
|
||||
tool.bank = std::make_shared<ui::vehicles_bank>();
|
||||
tool.bank->scan_textures();
|
||||
}
|
||||
ImGui::TextDisabled("%s", STR_C("A vehicle to drive, placed where RMB on the track was clicked, or in the middle of the path.\nIt isn't written to the scenery files"));
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
ImGui::InputTextWithHint("##vehiclesearch", STR_C("search: name, series, skin"), tool.search, sizeof(tool.search));
|
||||
ImGui::Checkbox(STR_C("Only the ones with a cab"), &tool.controllable_only);
|
||||
std::string const text{lower(tool.search)};
|
||||
std::vector<std::pair<std::shared_ptr<ui::vehicle_desc>, std::shared_ptr<ui::skin_set>>> listed;
|
||||
std::size_t total{0};
|
||||
for (auto const &[path, vehicle] : tool.bank->vehicles)
|
||||
{
|
||||
if (vehicle == nullptr || (tool.controllable_only && false == vehicle->controllable))
|
||||
continue;
|
||||
for (auto const &skin : vehicle->matching_skinsets)
|
||||
{
|
||||
if (skin == nullptr)
|
||||
continue;
|
||||
if (false == text.empty() && lower(skin_label(*vehicle, *skin)).find(text) == std::string::npos && (skin->meta == nullptr || skin->meta->search_lowered.find(text) == std::string::npos))
|
||||
continue;
|
||||
++total;
|
||||
if (listed.size() < kVehicleList)
|
||||
listed.emplace_back(vehicle, skin);
|
||||
}
|
||||
}
|
||||
ImGui::BeginChild("##vehicles", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 8.0f + 6.0f), true);
|
||||
for (std::size_t i = 0; i < listed.size(); ++i)
|
||||
{
|
||||
auto const &[vehicle, skin] = listed[i];
|
||||
if (ImGui::Selectable(format("%s##vehicle%zu", skin_label(*vehicle, *skin).c_str(), i).c_str(), tool.skin == skin))
|
||||
{
|
||||
tool.vehicle = vehicle;
|
||||
tool.skin = skin;
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
if (total > listed.size())
|
||||
ImGui::TextDisabled(STR_C("%zu found, the first %zu listed"), total, listed.size());
|
||||
if (tool.vehicle != nullptr && tool.skin != nullptr && ImGui::Button(STR_C("Add to the train")))
|
||||
tool.consist.push_back({tool.vehicle, tool.skin, false});
|
||||
if (false == tool.consist.empty())
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(STR_C("Train: %d vehicle(s), the first one leads"), static_cast<int>(tool.consist.size()));
|
||||
std::optional<std::size_t> remove;
|
||||
std::optional<std::size_t> raise;
|
||||
for (std::size_t i = 0; i < tool.consist.size(); ++i)
|
||||
{
|
||||
auto &item{tool.consist[i]};
|
||||
auto const vehicle{item.skin->vehicle.lock()};
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::Checkbox("##turned", &item.turned);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("Turned around in the train"));
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%2d. %s", static_cast<int>(i + 1), vehicle != nullptr ? skin_label(*vehicle, *item.skin).c_str() : item.skin->skin.c_str());
|
||||
ImGui::SameLine();
|
||||
if (i > 0 && ImGui::SmallButton(STR_C("up")))
|
||||
raise = i;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x"))
|
||||
remove = i;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (raise)
|
||||
std::swap(tool.consist[*raise], tool.consist[*raise - 1]);
|
||||
if (remove)
|
||||
tool.consist.erase(tool.consist.begin() + static_cast<std::ptrdiff_t>(*remove));
|
||||
if (ImGui::SmallButton(STR_C("Empty the train")))
|
||||
tool.consist.clear();
|
||||
}
|
||||
if (ImGui::Button(STR_C("Turn the train around")))
|
||||
tool.facing = -tool.facing;
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", STR_C("the head goes where the arrow at the marker points"));
|
||||
ImGui::RadioButton(STR_C("With a driver, to drive"), &tool.driver, 0);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton(STR_C("Nobody, parked"), &tool.driver, 1);
|
||||
if (tool.consist.empty() && (tool.vehicle == nullptr || tool.skin == nullptr))
|
||||
ImGui::TextDisabled("%s", STR_C("Pick a vehicle from the list"));
|
||||
else
|
||||
{
|
||||
if (ImGui::Button(STR_C("Place")))
|
||||
vehicle_place();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(STR_C("Place and drive")) && vehicle_place())
|
||||
{
|
||||
Global.editor_enter_vehicle = tool.placed;
|
||||
tool.leave = true;
|
||||
}
|
||||
}
|
||||
if (false == tool.status.empty())
|
||||
ImGui::TextWrapped("%s", tool.status.c_str());
|
||||
}
|
||||
|
||||
void editor_mode::draw_vehicle_marker() const
|
||||
{
|
||||
auto const &tool{m_vehicle};
|
||||
if (false == tool.open || tool.track == nullptr)
|
||||
return;
|
||||
screen_projection const projection;
|
||||
ImVec2 screen;
|
||||
if (false == projection.project(tool.point, screen))
|
||||
return;
|
||||
auto *drawlist{ImGui::GetBackgroundDrawList()};
|
||||
auto const colour{IM_COL32(255, 220, 60, 235)};
|
||||
drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f);
|
||||
if (tool.track->m_editorremoved || tool.track->m_paths.empty())
|
||||
return;
|
||||
auto const heading{heading_at(*tool.track, tool.track->eType == tt_Switch ? path_under(*tool.track, tool.point) : 0, tool.point) * static_cast<double>(tool.facing)};
|
||||
glm::dvec3 const tip{tool.point.x + heading.x * 8.0, tool.point.y, tool.point.z + heading.y * 8.0};
|
||||
ImVec2 end;
|
||||
if (false == projection.project(tip, end))
|
||||
return;
|
||||
drawlist->AddLine(screen, end, colour, 3.0f);
|
||||
auto const dx{end.x - screen.x};
|
||||
auto const dy{end.y - screen.y};
|
||||
auto const size{std::sqrt(dx * dx + dy * dy)};
|
||||
if (size < 1.0f)
|
||||
return;
|
||||
ImVec2 const unit{dx / size, dy / size};
|
||||
ImVec2 const side{-unit.y, unit.x};
|
||||
drawlist->AddTriangleFilled(ImVec2(end.x + unit.x * 10.0f, end.y + unit.y * 10.0f), ImVec2(end.x + side.x * 7.0f, end.y + side.y * 7.0f), ImVec2(end.x - side.x * 7.0f, end.y - side.y * 7.0f), colour);
|
||||
}
|
||||
@@ -76,6 +76,31 @@ class screen_projection
|
||||
auto const &mouse{ImGui::GetIO().MousePos};
|
||||
return (position.x - mouse.x) * (position.x - mouse.x) + (position.y - mouse.y) * (position.y - mouse.y);
|
||||
}
|
||||
// world space ray through the point of the screen
|
||||
void ray(ImVec2 const &Screen, glm::dvec3 &Origin, glm::dvec3 &Direction) const
|
||||
{
|
||||
auto const inverse{glm::inverse(m_viewprojection)};
|
||||
glm::vec2 const ndc{m_size.x > 0.0f ? Screen.x / m_size.x * 2.0f - 1.0f : 0.0f, m_size.y > 0.0f ? 1.0f - Screen.y / m_size.y * 2.0f : 0.0f};
|
||||
auto nearpoint{inverse * glm::vec4(ndc, -1.0f, 1.0f)};
|
||||
auto farpoint{inverse * glm::vec4(ndc, 1.0f, 1.0f)};
|
||||
nearpoint /= nearpoint.w;
|
||||
farpoint /= farpoint.w;
|
||||
Origin = m_camera + glm::dvec3(glm::vec3(nearpoint));
|
||||
Direction = glm::normalize(glm::dvec3(glm::vec3(farpoint - nearpoint)));
|
||||
}
|
||||
// where the ray through the point of the screen crosses the level of the height; false if it runs along it or away
|
||||
bool on_level(ImVec2 const &Screen, double const Height, glm::dvec3 &Point) const
|
||||
{
|
||||
glm::dvec3 origin, direction;
|
||||
ray(Screen, origin, direction);
|
||||
if (std::abs(direction.y) < 1e-6)
|
||||
return false;
|
||||
auto const distance{(Height - origin.y) / direction.y};
|
||||
if (distance < 0.0)
|
||||
return false;
|
||||
Point = origin + direction * distance;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr float kNear{0.1f};
|
||||
@@ -87,6 +112,9 @@ class screen_projection
|
||||
// starts a gizmo frame over the whole display, the view and the projection are those of the current camera
|
||||
struct gizmo_frame
|
||||
{
|
||||
static inline int drawn{-1};
|
||||
// the cursor is over a gizmo shown in this frame, ImGuizmo alone tells it of the last one shown even when it's gone
|
||||
static bool over() { return ImGui::GetFrameCount() - drawn <= 1 && ImGuizmo::IsOver(); }
|
||||
glm::mat4 view;
|
||||
glm::mat4 projection;
|
||||
glm::dvec3 camera;
|
||||
@@ -106,6 +134,7 @@ struct gizmo_frame
|
||||
{
|
||||
glm::vec3 snapvalue(Snap);
|
||||
float const *snap = Global.ctrlState && Snap > 0.0f ? &snapvalue.x : nullptr;
|
||||
drawn = ImGui::GetFrameCount();
|
||||
return ImGuizmo::Manipulate(&view[0][0], &projection[0][0], Operation, ImGuizmo::WORLD, &Gizmo[0][0], Delta != nullptr ? &(*Delta)[0][0] : nullptr, snap);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,6 +109,10 @@ void editor_ui::render_menu_contents()
|
||||
{
|
||||
if (ImGui::BeginMenu(STR_C("File")))
|
||||
{
|
||||
if (ImGui::MenuItem(STR_C("New scenery...")) && m_newscenery)
|
||||
m_newscenery();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("%s", STR_C("The editor starts again with the wizard of a new scenery: its name and its centre on the map"));
|
||||
// changes go to the scenery files only if the scenery was loaded with its sources tracked
|
||||
auto const editsession{false == scene::Layers.empty()};
|
||||
if (ImGui::MenuItem(STR_C("Save"), "Ctrl+S", false, editsession) && m_save)
|
||||
|
||||
@@ -68,6 +68,7 @@ class editor_ui : public ui_layer
|
||||
}
|
||||
// actions of the file menu, performed by the editor mode
|
||||
void set_file_actions(std::function<void()> Save, std::function<void()> Export);
|
||||
void set_new_scenery(std::function<void()> New) { m_newscenery = std::move(New); }
|
||||
// shows outcome of an operation in the layers window
|
||||
void set_status(std::string const &Status, bool const Error = false);
|
||||
|
||||
@@ -84,6 +85,7 @@ class editor_ui : public ui_layer
|
||||
includes_panel m_includespanel{"Include database", false};
|
||||
std::function<void()> m_save;
|
||||
std::function<void()> m_export;
|
||||
std::function<void()> m_newscenery;
|
||||
brush_object_list m_brushobjects;
|
||||
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
|
||||
model_set_ref m_insertset;
|
||||
|
||||
Reference in New Issue
Block a user