17
0
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:
maj00r
2026-10-06 22:19:54 +02:00
parent 0a3cd55e59
commit 24923dd472
26 changed files with 9991 additions and 468 deletions

View File

@@ -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);
}

View File

@@ -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 &current_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();

View File

@@ -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);
}
}

View 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());
}
}

View 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);
}
}

View 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));
}

View File

@@ -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())
{

View 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;
}
}
}

View File

@@ -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)

View 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 &parameter{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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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);
}

View File

@@ -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);
}
};

View File

@@ -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)

View File

@@ -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;