diff --git a/application/editormode.cpp b/application/editormode.cpp index 24916f38..7958465c 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -8,6 +8,10 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" +#include +#include +#include +#include #include "application/editormode.h" #include "application/editoruilayer.h" #ifndef _WIN32 @@ -23,6 +27,7 @@ extern char **environ; #include "editor/editorIncludeInfo.hpp" #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" #include "utilities/translation.h" #include "utilities/Globals.h" #include "simulation/simulation.h" @@ -483,8 +488,10 @@ editor_mode::editor_mode() { ui()->set_fill_options([this]() { render_area_fill(); }); ui()->set_gizmo_options([this]() { render_gizmo_options(); }); ui()->set_array_options([this]() { render_array(); }); + ui()->set_bend_options([this]() { render_bend(); }); ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); ui()->set_new_scenery([this]() { m_newscenery_asked = true; }); + ui()->set_open_scenery([this]() { m_openscenery_asked = true; }); ui()->set_menu_options([this]() { render_object_menu(); render_track_menu(); @@ -911,6 +918,13 @@ void editor_mode::undo_last() EditorSnapshot snap = m_history.back(); m_history.pop_back(); + if (snap.action == EditorSnapshot::Action::Bend) + { + restore_bend(snap, true); + g_redo.push_back(std::move(snap)); + return; + } + if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty()) { restore_sweeps(snap); @@ -921,8 +935,11 @@ void editor_mode::undo_last() if (false == snap.instances.empty() || false == snap.directives.empty()) { restore_includes(snap); - g_redo.push_back(std::move(snap)); - return; + if (snap.action != EditorSnapshot::Action::TrackEdit) + { + g_redo.push_back(std::move(snap)); + return; + } } if (snap.instance != 0) @@ -1035,6 +1052,13 @@ void editor_mode::redo_last() EditorSnapshot snap = g_redo.back(); g_redo.pop_back(); + if (snap.action == EditorSnapshot::Action::Bend) + { + restore_bend(snap, false); + m_history.push_back(std::move(snap)); + return; + } + if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty()) { restore_sweeps(snap); @@ -1045,8 +1069,11 @@ void editor_mode::redo_last() if (false == snap.instances.empty() || false == snap.directives.empty()) { restore_includes(snap); - m_history.push_back(std::move(snap)); - return; + if (snap.action != EditorSnapshot::Action::TrackEdit) + { + m_history.push_back(std::move(snap)); + return; + } } if (snap.instance != 0) @@ -1334,8 +1361,6 @@ bool editor_mode::update() draw_hekto_overlay(); draw_fouling_overlay(); draw_vehicle_marker(); - sweep_drag(); - draw_sweep_overlay(); } render_track_context(); update_build_tools(); @@ -1353,6 +1378,13 @@ bool editor_mode::update() draw_speed_overlay(); draw_joints_overlay(); } + else + { + draw_sweep_overlay(); + m_sweep.open = false; + bend_drag_update(); + draw_bend_overlay(); + } update_gauge(); render_gauge_window(); @@ -1370,6 +1402,7 @@ bool editor_mode::update() render_orthophoto_window(); render_new_scenery_popup(); + render_open_scenery_popup(); // --- ImGui: Editor Settings & History windows --- if(m_settings_open) @@ -1384,15 +1417,15 @@ bool editor_mode::update() void editor_mode::render_settings() { - ImGui::Begin("Editor Settings", &m_settings_open, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Begin(STR_C("Editor Settings"), &m_settings_open, ImGuiWindowFlags_AlwaysAutoResize); if (ImGui::BeginTabBar("##editorsettings")) { - if (ImGui::BeginTabItem("General")) + if (ImGui::BeginTabItem(STR_C("General"))) { - ImGui::TextUnformatted("Camera movement"); + ImGui::TextUnformatted(STR_C("Camera movement")); - const char *schemes[] = {"WSAD (new)", "Arrows (legacy)"}; + const char *schemes[] = {STR_C("WSAD (new)"), STR_C("Arrows (legacy)")}; int current = EditorSettings.movement() == editorSettings::movement_scheme::legacy ? 1 : 0; if (ImGui::Combo("##movement_scheme", ¤t, schemes, IM_ARRAYSIZE(schemes))) { @@ -1403,12 +1436,12 @@ void editor_mode::render_settings() } ImGui::Separator(); - ImGui::Checkbox("Transform gizmo (ImGuizmo)", &m_gizmo_enabled); + ImGui::Checkbox(STR_C("Transform gizmo (ImGuizmo)"), &m_gizmo_enabled); ImGui::EndTabItem(); } bool const terrainwanted = m_terrain_tab_wanted; m_terrain_tab_wanted = false; - if (ImGui::BeginTabItem("Terrain", nullptr, terrainwanted ? ImGuiTabItemFlags_SetSelected : 0)) + if (ImGui::BeginTabItem(STR_C("Terrain"), nullptr, terrainwanted ? ImGuiTabItemFlags_SetSelected : 0)) { render_terrain_ui(); ImGui::EndTabItem(); @@ -1422,18 +1455,18 @@ void editor_mode::render_settings() void editor_mode::render_terrain_ui() { ImGui::SetNextItemWidth(120.0f); - ImGui::InputInt("Grid cells", &m_terrain_cells); + ImGui::InputInt(STR_C("Grid cells"), &m_terrain_cells); m_terrain_cells = std::clamp(m_terrain_cells, 1, 512); ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Cell size (m)", &m_terrain_cellsize); + ImGui::InputFloat(STR_C("Cell size (m)"), &m_terrain_cellsize); if (m_terrain_cellsize < 0.1f) m_terrain_cellsize = 0.1f; ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Base height (m)", &m_terrain_baseheight); + ImGui::InputFloat(STR_C("Base height (m)"), &m_terrain_baseheight); ImGui::SetNextItemWidth(200.0f); - ImGui::InputText("Texture (optional)", m_terrain_texture, IM_ARRAYSIZE(m_terrain_texture)); + ImGui::InputText(STR_C("Texture (optional)"), m_terrain_texture, IM_ARRAYSIZE(m_terrain_texture)); - if (ImGui::Button("Create flat terrain")) + if (ImGui::Button(STR_C("Create flat terrain"))) { // centre the new patch horizontally on the camera, flat at the requested base height glm::dvec3 const center(Camera.Pos.x, static_cast(m_terrain_baseheight), Camera.Pos.z); @@ -1449,29 +1482,29 @@ void editor_mode::render_terrain_ui() } ImGui::SetNextItemWidth(120.0f); - ImGui::InputInt("Chunks / side", &m_terrain_chunks); + ImGui::InputInt(STR_C("Chunks / side"), &m_terrain_chunks); m_terrain_chunks = std::clamp(m_terrain_chunks, 1, 32); ImGui::SameLine(); - if (ImGui::Button("Create chunked terrain")) + if (ImGui::Button(STR_C("Create chunked terrain"))) create_chunked_terrain(); - ImGui::TextDisabled("total %d x %d m, %d chunks", + ImGui::TextDisabled(STR_C("total %d x %d m, %d chunks"), static_cast(m_terrain_chunks * m_terrain_cells * m_terrain_cellsize), static_cast(m_terrain_chunks * m_terrain_cells * m_terrain_cellsize), m_terrain_chunks * m_terrain_chunks); - if (ImGui::Checkbox("Chunk edit mode (LMB add neighbour / Shift = delete)", &m_chunk_edit)) + if (ImGui::Checkbox(STR_C("Chunk edit mode (LMB add neighbour / Shift = delete)"), &m_chunk_edit)) if (m_chunk_edit) m_terrain_sculpt = false; // mutually exclusive with sculpting - ImGui::Text("Grid chunks: %zu", m_grid_chunks.size()); + ImGui::Text(STR_C("Grid chunks: %zu"), m_grid_chunks.size()); ImGui::Separator(); - ImGui::TextUnformatted("Streaming (open world, follows camera)"); + ImGui::TextUnformatted(STR_C("Streaming (open world, follows camera)")); ImGui::SetNextItemWidth(120.0f); - ImGui::InputInt("Stream radius", &m_stream_radius); + ImGui::InputInt(STR_C("Stream radius"), &m_stream_radius); m_stream_radius = std::clamp(m_stream_radius, 0, 16); - ImGui::Checkbox("Persist edits to disk (16-bit)", &m_stream_persist); + ImGui::Checkbox(STR_C("Persist edits to disk (16-bit)"), &m_stream_persist); bool streaming = m_streamer.active(); - if (ImGui::Checkbox("Stream terrain", &streaming)) + if (ImGui::Checkbox(STR_C("Stream terrain"), &streaming)) { if (streaming) { @@ -1514,20 +1547,20 @@ void editor_mode::render_terrain_ui() m_streamer.radius(m_stream_radius); m_streamer.simplify(m_terrain_auto_optimize, m_terrain_simplify_error); m_streamer.persist(m_stream_persist); - ImGui::Text("Resident chunks: %zu (dir: %s)", m_streamer.resident(), m_streamer.directory().c_str()); + ImGui::Text(STR_C("Resident chunks: %zu (dir: %s)"), m_streamer.resident(), m_streamer.directory().c_str()); } - ImGui::Text("Patches: %zu", m_terrains.size()); + ImGui::Text(STR_C("Patches: %zu"), m_terrains.size()); // capture: sample the selected model's geometry into an editable patch and remove the original if (dynamic_cast(m_node) != nullptr) { - if (ImGui::Button("Capture selected model as terrain")) + if (ImGui::Button(STR_C("Capture selected model as terrain"))) capture_terrain(); } else { - ImGui::TextDisabled("Capture: select a model instance first"); + ImGui::TextDisabled(STR_C("Capture: select a model instance first")); } std::vector const terrains = active_terrains(); @@ -1545,36 +1578,36 @@ void editor_mode::render_terrain_ui() if (m_terrain_brush_smooth && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Evens out the ground under the brush: bumps and sharp edges go, the shape stays.\nThe strength sets how fast")); ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Brush radius", &m_terrain_brush_radius); + ImGui::InputFloat(STR_C("Brush radius"), &m_terrain_brush_radius); if (m_terrain_brush_radius < 0.5f) m_terrain_brush_radius = 0.5f; ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Brush strength", &m_terrain_brush_strength); + ImGui::InputFloat(STR_C("Brush strength"), &m_terrain_brush_strength); // one-shot nudge of the most recent manual patch at its centre, handy for a quick test if (!m_terrains.empty()) { auto &terrain = m_terrains.back(); glm::dvec3 const c = terrain->centre(); - if (ImGui::Button("Raise centre")) + if (ImGui::Button(STR_C("Raise centre"))) terrain->sculpt(c.x, c.z, m_terrain_brush_radius, m_terrain_brush_strength); ImGui::SameLine(); - if (ImGui::Button("Lower centre")) + if (ImGui::Button(STR_C("Lower centre"))) terrain->sculpt(c.x, c.z, m_terrain_brush_radius, -m_terrain_brush_strength); } ImGui::Separator(); - ImGui::TextUnformatted("Optimize (mesh simplification, all patches)"); + ImGui::TextUnformatted(STR_C("Optimize (mesh simplification, all patches)")); ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Flatness tol (m)", &m_terrain_simplify_error); + ImGui::InputFloat(STR_C("Flatness tol (m)"), &m_terrain_simplify_error); if (m_terrain_simplify_error < 0.01f) m_terrain_simplify_error = 0.01f; - ImGui::Checkbox("Auto-optimize after sculpt", &m_terrain_auto_optimize); - if (ImGui::Button("Optimize all")) + ImGui::Checkbox(STR_C("Auto-optimize after sculpt"), &m_terrain_auto_optimize); + if (ImGui::Button(STR_C("Optimize all"))) for (editor_terrain *t : terrains) t->optimize(m_terrain_simplify_error); ImGui::SameLine(); - if (ImGui::Button("Full-res all")) + if (ImGui::Button(STR_C("Full-res all"))) for (editor_terrain *t : terrains) t->unoptimize(); @@ -1584,7 +1617,7 @@ void editor_mode::render_terrain_ui() tris += t->triangles(); full += t->full_triangles(); } - ImGui::Text("Triangles: %zu / %zu", tris, full); + ImGui::Text(STR_C("Triangles: %zu / %zu"), tris, full); } } @@ -1644,18 +1677,18 @@ void editor_mode::render_orthophoto_ui() bool persist = false; // and store in the editor settings bool enabled = m_orthophoto.enabled(); - if (ImGui::Checkbox("Show orthophoto (geoportal.gov.pl)", &enabled)) + if (ImGui::Checkbox(STR_C("Show orthophoto (geoportal.gov.pl)"), &enabled)) m_orthophoto.enabled(enabled); ImGui::Separator(); - ImGui::TextUnformatted("Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180"); + ImGui::TextUnformatted(STR_C("Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180")); // geodetic convention, as displayed by geoportal.gov.pl: X grows north, Y grows east. // applied once editing ends, so half-typed numbers don't trigger downloads ImGui::SetNextItemWidth(160.0f); - ImGui::InputDouble("X (northing, m)", &m_orthophoto_origin_edit.x, 0.0, 0.0, "%.2f"); + ImGui::InputDouble(STR_C("X (northing, m)"), &m_orthophoto_origin_edit.x, 0.0, 0.0, "%.2f"); bool const northedited = ImGui::IsItemDeactivatedAfterEdit(); ImGui::SetNextItemWidth(160.0f); - ImGui::InputDouble("Y (easting, m)", &m_orthophoto_origin_edit.y, 0.0, 0.0, "%.2f"); + ImGui::InputDouble(STR_C("Y (easting, m)"), &m_orthophoto_origin_edit.y, 0.0, 0.0, "%.2f"); bool const eastedited = ImGui::IsItemDeactivatedAfterEdit(); if (northedited || eastedited) { @@ -1679,13 +1712,13 @@ void editor_mode::render_orthophoto_ui() } } 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"); + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), STR_C("Enter the origin of this scenery first")); else - ImGui::TextDisabled("Camera at X %.1f Y %.1f", config.north + Camera.Pos.z, config.east - Camera.Pos.x); + ImGui::TextDisabled(STR_C("Camera at X %.1f Y %.1f"), config.north + Camera.Pos.z, config.east - Camera.Pos.x); ImGui::Separator(); ImGui::SetNextItemWidth(160.0f); - changed |= ImGui::SliderInt("Distance (tiles)", &config.radius, 0, editor_orthophoto::max_radius); + changed |= ImGui::SliderInt(STR_C("Distance (tiles)"), &config.radius, 0, editor_orthophoto::max_radius); persist |= ImGui::IsItemDeactivatedAfterEdit(); ImGui::SameLine(); int const span = static_cast((2 * config.radius + 1) * editor_orthophoto::tile_size); @@ -1705,15 +1738,15 @@ void editor_mode::render_orthophoto_ui() ImGui::PopItemFlag(); }; - if (ImGui::Checkbox("Fit to terrain", &config.drape)) + if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) changed = persist = true; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, editor terrain).\n" - "Places without any ground use the height below."); + ImGui::SetTooltip(STR_C("Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, editor terrain).\n" + "Places without any ground use the height below.")); if (config.drape) { ImGui::SameLine(); - if (ImGui::Button("Refit")) + if (ImGui::Button(STR_C("Refit"))) { // the ground was edited: what's kept of it has to go as well ground_tiles.clear(); @@ -1721,46 +1754,46 @@ void editor_mode::render_orthophoto_ui() m_orthophoto.refit(); } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Samples the ground again, after it was edited"); + ImGui::SetTooltip(STR_C("Samples the ground again, after it was edited")); } begin_disabled(!config.drape); ImGui::SetNextItemWidth(160.0f); - changed |= ImGui::DragFloat("Lift above ground (m)", &config.lift, 0.01f, 0.0f, 50.0f, "%.2f"); + changed |= ImGui::DragFloat(STR_C("Lift above ground (m)"), &config.lift, 0.01f, 0.0f, 50.0f, "%.2f"); persist |= ImGui::IsItemDeactivatedAfterEdit(); end_disabled(!config.drape); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Raises the fitted imagery above the ground. Keep it below the objects you want to see:\n" - "in the scene everything lower than this ends up under the imagery."); + ImGui::SetTooltip(STR_C("Raises the fitted imagery above the ground. Keep it below the objects you want to see:\n" + "in the scene everything lower than this ends up under the imagery.")); begin_disabled(config.drape); ImGui::SetNextItemWidth(160.0f); - changed |= ImGui::DragFloat("Height (m)", &config.height, 0.1f, -1000.0f, 3000.0f, "%.2f"); + changed |= ImGui::DragFloat(STR_C("Height (m)"), &config.height, 0.1f, -1000.0f, 3000.0f, "%.2f"); persist |= ImGui::IsItemDeactivatedAfterEdit(); end_disabled(config.drape); - if (ImGui::Checkbox("Covered by objects and terrain", &config.in_scene)) + if (ImGui::Checkbox(STR_C("Covered by objects and terrain"), &config.in_scene)) changed = persist = true; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\n" + ImGui::SetTooltip(STR_C("Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\n" "It is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\n" "translucent, it doesn't catch the mouse.\n" - "Not available with the Better Renderer, which doesn't pick up geometry added in the editor."); + "Not available with the Better Renderer, which doesn't pick up geometry added in the editor.")); ImGui::SetNextItemWidth(160.0f); - changed |= ImGui::SliderFloat("Opacity", &config.opacity, 0.0f, 1.0f, "%.2f"); + changed |= ImGui::SliderFloat(STR_C("Opacity"), &config.opacity, 0.0f, 1.0f, "%.2f"); persist |= ImGui::IsItemDeactivatedAfterEdit(); if (config.in_scene && ImGui::IsItemHovered()) - ImGui::SetTooltip("In the scene the opacity is part of the textures: the tiles are read again from the cache\n" - "shortly after the slider stops, which takes a moment."); + ImGui::SetTooltip(STR_C("In the scene the opacity is part of the textures: the tiles are read again from the cache\n" + "shortly after the slider stops, which takes a moment.")); // newest imagery, or the newest imagery taken up to the end of the selected year int const thisyear = static_cast(std::chrono::year_month_day{std::chrono::floor(std::chrono::system_clock::now())}.year()); - std::string const yearlabel = config.year ? std::to_string(config.year) : std::string("Newest"); + std::string const yearlabel = config.year ? std::to_string(config.year) : std::string(STR_C("Newest")); ImGui::SetNextItemWidth(160.0f); if (ImGui::BeginCombo("Photo year", yearlabel.c_str())) { - if (ImGui::Selectable("Newest", config.year == 0)) + if (ImGui::Selectable(STR_C("Newest"), config.year == 0)) { config.year = 0; changed = persist = true; @@ -1776,11 +1809,11 @@ void editor_mode::render_orthophoto_ui() ImGui::EndCombo(); } - if (ImGui::Checkbox("4K tiles where available", &config.hires)) + if (ImGui::Checkbox(STR_C("4K tiles where available"), &config.hires)) changed = persist = true; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\n" - "Areas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory."); + ImGui::SetTooltip(STR_C("4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\n" + "Areas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory.")); if (changed) m_orthophoto.settings(config); @@ -1789,17 +1822,17 @@ void editor_mode::render_orthophoto_ui() ImGui::Separator(); auto const stats = m_orthophoto.stats(); - ImGui::Text("Tiles: %d shown, %d loading, %d failed", stats.resident, stats.loading, stats.failed); + ImGui::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed); if (stats.failed > 0) { ImGui::SameLine(); - if (ImGui::Button("Retry")) + if (ImGui::Button(STR_C("Retry"))) m_orthophoto.retry_failed(); } if (!editor_orthophoto::can_download()) - ImGui::TextDisabled("This build has no HTTP client, only cached tiles are shown"); - ImGui::TextDisabled("Cache: %s", editor_orthophoto::cache_directory().c_str()); - ImGui::TextDisabled("Imagery: GUGiK, geoportal.gov.pl"); + ImGui::TextDisabled(STR_C("This build has no HTTP client, only cached tiles are shown")); + ImGui::TextDisabled(STR_C("Cache: %s"), editor_orthophoto::cache_directory().c_str()); + ImGui::TextDisabled(STR_C("Imagery: GUGiK, geoportal.gov.pl")); } void editor_mode::draw_orthophoto() @@ -2219,7 +2252,7 @@ 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 ", true); + ui()->set_status(STR("The scenery wasn't opened for editing. Start the simulator with: -edit "), true); return false; } @@ -2241,8 +2274,28 @@ bool editor_mode::save() if (m_profile.changed) profile_store(); infra_store(); - auto const result = scene::Layers.save(rootstatements); - if (result.success && false == rootstatements.empty()) + if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) + { + m_saved_camera = std::make_pair(glm::dvec3{Camera.Pos}, glm::vec3{Camera.Angle}); + scene::Layers.mark(scene::layer_handle{1}, "//$c", {format("%.3f %.3f %.3f %.5f %.5f", Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, Camera.Angle.x, Camera.Angle.y)}); + } + auto const terrainadded{false == rootstatements.empty()}; + auto const controls{turntable_controls(rootstatements)}; + std::vector trailing; + starter_trainset(trailing); + auto const result = scene::Layers.save(rootstatements, trailing); + if (result.success && controls) + m_turntable_included = true; + if (result.success && trailing.size() == 3) + { + std::istringstream header{trailing.front()}; + std::string keyword, timetable, track; + double offset{0.0}; + header >> keyword >> timetable >> track >> offset; + if (auto *path{simulation::Paths.find(track)}; path != nullptr && simulation::Vehicles.find("wmb10-6457") == nullptr) + simulation::State.insert_trainset("wmb10-6457", path, offset, trailing[1] + "\n"); + } + if (result.success && terrainadded) { scene::Layers.terrain_directive(true); } @@ -2628,7 +2681,7 @@ void editor_mode::drop_model_instances() auto *selected = dynamic_cast(m_node); if (selected == nullptr || selected->Model() == nullptr) { - ui()->set_status("Select a model in the scene first", true); + ui()->set_status(STR("Select a model in the scene first"), true); return; } if (selected->radius() >= 50.0f) @@ -2773,6 +2826,11 @@ void editor_mode::render_new_scenery_popup() } bool editor_mode::restart_for_new_scenery() +{ + return restart_editor(std::string{}); +} + +bool editor_mode::restart_editor(std::string const &Scenery) { #ifdef _WIN32 _putenv("EU07_EDITOR_SELFTEST="); @@ -2780,6 +2838,8 @@ bool editor_mode::restart_for_new_scenery() if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0) return false; std::wstring command{L"\"" + std::wstring{path} + L"\" -edit"}; + if (false == Scenery.empty()) + command += L" \"" + std::filesystem::path(Scenery).wstring() + L"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; @@ -2795,7 +2855,8 @@ bool editor_mode::restart_for_new_scenery() return false; path[size] = '\0'; char edit[]{"-edit"}; - char *arguments[]{path, edit, nullptr}; + std::string scenery{Scenery}; + char *arguments[]{path, edit, scenery.empty() ? nullptr : scenery.data(), nullptr}; posix_spawnattr_t attributes; posix_spawnattr_init(&attributes); posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSID); @@ -2805,11 +2866,78 @@ bool editor_mode::restart_for_new_scenery() if (result != 0) return false; #endif - WriteLog("Editor: started again for a new scenery"); + WriteLog(Scenery.empty() ? std::string{"Editor: started again for a new scenery"} : "Editor: started again with scenery " + Scenery); Application.queue_quit(true); return true; } +void editor_mode::render_open_scenery_popup() +{ + auto const lower = [](std::string Text) { + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; + }; + if (m_openscenery_asked) + { + m_openscenery_asked = false; + m_openscenery_list.clear(); + m_openscenery_choice.clear(); + std::error_code error; + for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + { + auto const name{entry.path().filename().string()}; + if (entry.is_regular_file(error) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$') + m_openscenery_list.emplace_back(name); + } + std::sort(m_openscenery_list.begin(), m_openscenery_list.end(), [&](std::string const &A, std::string const &B) { return lower(A) < lower(B); }); + ImGui::OpenPopup(STR_C("Open scenery##restart")); + } + if (false == ImGui::BeginPopupModal(STR_C("Open scenery##restart"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + return; + ImGui::TextUnformatted(STR_C("The editor starts again with the scenery chosen below.")); + ImGui::SetNextItemWidth(360.0f); + if (ImGui::IsWindowAppearing()) + ImGui::SetKeyboardFocusHere(); + ImGui::InputTextWithHint("##opensearch", STR_C("search"), m_openscenery_search, sizeof(m_openscenery_search)); + std::string const text{lower(m_openscenery_search)}; + ImGui::BeginChild("##openlist", ImVec2(360.0f, ImGui::GetTextLineHeightWithSpacing() * 14.0f), true); + for (auto const &name : m_openscenery_list) + { + if (false == text.empty() && lower(name).find(text) == std::string::npos) + continue; + if (ImGui::Selectable(name.c_str(), name == m_openscenery_choice, ImGuiSelectableFlags_AllowDoubleClick)) + { + m_openscenery_choice = name; + if (ImGui::IsMouseDoubleClicked(0) && false == restart_editor(name)) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + } + } + ImGui::EndChild(); + 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.")); + auto const chosen{false == m_openscenery_choice.empty()}; + if (chosen && editsession && ImGui::Button(STR_C("Save and open"))) + { + if (save() && false == restart_editor(m_openscenery_choice)) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + if (chosen && editsession) + ImGui::SameLine(); + if (chosen && ImGui::Button(STR_C("Open"))) + { + if (false == restart_editor(m_openscenery_choice)) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + if (chosen) + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); +} + void editor_mode::render_orthophoto_window() { if (false == m_orthophoto_window) @@ -2822,14 +2950,14 @@ void editor_mode::render_orthophoto_window() void editor_mode::render_object_menu() { - if (ImGui::BeginMenu("Objects")) + if (ImGui::BeginMenu(STR_C("Objects"))) { auto const *selected = dynamic_cast(m_node); - if (ImGui::MenuItem("Put all instances of the selected model on the ground", nullptr, false, selected != nullptr && selected->Model() != nullptr)) + if (ImGui::MenuItem(STR_C("Put all instances of the selected model on the ground"), nullptr, false, selected != nullptr && selected->Model() != nullptr)) drop_model_instances(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - ImGui::SetTooltip("%s", "Select a model in the scene. Every instance of the same model file is moved straight up or down\n" - "to the ground under it: terrain, large models and terrain made in the editor."); + ImGui::SetTooltip("%s", STR_C("Select a model in the scene. Every instance of the same model file is moved straight up or down\n" + "to the ground under it: terrain, large models and terrain made in the editor.")); ImGui::EndMenu(); } } @@ -2865,7 +2993,7 @@ void editor_mode::undo_last_area_fill() remove_from_hierarchy(model); simulation::State.delete_model(model); } - m_fill_status = "Removed " + std::to_string(created.size()) + " objects of the last fill"; + m_fill_status = format(STR_C("Removed %zu objects of the last fill"), created.size()); } void editor_mode::run_area_fill() @@ -2880,7 +3008,7 @@ void editor_mode::run_area_fill() if (m_fill_points.size() < 3) { - m_fill_status = "Outline needs at least 3 points"; + m_fill_status = STR("Outline needs at least 3 points"); return; } @@ -2894,7 +3022,7 @@ void editor_mode::run_area_fill() package.erase(std::remove_if(package.begin(), package.end(), [](std::string const &Template) { return Template.empty(); }), package.end()); if (package.empty()) { - m_fill_status = "Selected model package is empty"; + m_fill_status = STR("Selected model package is empty"); return; } @@ -2910,7 +3038,7 @@ void editor_mode::run_area_fill() auto const target = static_cast(std::min(std::round(area / 10000.0 * std::max(0.0f, m_fill_density)), max_objects)); if (target == 0) { - m_fill_status = "Density too low for this area, nothing to place"; + m_fill_status = STR("Density too low for this area, nothing to place"); return; } @@ -3015,7 +3143,7 @@ void editor_mode::run_area_fill() // a new fill replaces the "last fill" undo set; the previous objects stay in the scene m_fill_last = std::move(created); - m_fill_status = "Placed " + std::to_string(m_fill_last.size()) + " of " + std::to_string(target) + " objects"; + m_fill_status = format(STR_C("Placed %zu of %zu objects"), m_fill_last.size(), static_cast(target)); if (points.size() < target) m_fill_status += " (limited by min. spacing)"; if (unsupported > 0) @@ -3073,46 +3201,46 @@ void editor_mode::draw_area_fill_outline() const void editor_mode::render_area_fill() { - ImGui::TextDisabled("LMB: add outline point Backspace: remove last point"); + ImGui::TextDisabled(STR_C("LMB: add outline point Backspace: remove last point")); double const area = polygon_area_xz(m_fill_points); - ImGui::Text("Points: %zu Area: %.0f m2 (%.2f ha)", m_fill_points.size(), area, area / 10000.0); - if (ImGui::Button("Remove last point") && !m_fill_points.empty()) + ImGui::Text(STR_C("Points: %zu Area: %.0f m2 (%.2f ha)"), m_fill_points.size(), area, area / 10000.0); + if (ImGui::Button(STR_C("Remove last point")) && !m_fill_points.empty()) m_fill_points.pop_back(); ImGui::SameLine(); - if (ImGui::Button("Clear outline")) + if (ImGui::Button(STR_C("Clear outline"))) m_fill_points.clear(); ImGui::Separator(); // model package: a hand-assembled list, a user-defined set or a node bank group auto &bank = ui()->nodebank(); - bank.set_combo("Model set", m_fill_source, "Custom list"); + bank.set_combo(STR_C("Model set"), m_fill_source, STR_C("Custom list")); if (m_fill_source.kind == model_set_ref::source::manual) bank.manual_list("fillset", m_fill_custom, m_fill_custom_idx); else - ImGui::TextDisabled("%zu templates in set", bank.set_entries(m_fill_source).size()); + ImGui::TextDisabled(STR_C("%zu templates in set"), bank.set_entries(m_fill_source).size()); ImGui::Separator(); ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Density (objects/ha)", &m_fill_density, 10.0f, 100.0f, "%.1f"); + ImGui::InputFloat(STR_C("Density (objects/ha)"), &m_fill_density, 10.0f, 100.0f, "%.1f"); m_fill_density = std::max(0.0f, m_fill_density); ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Min. spacing (m)", &m_fill_min_spacing, 0.5f, 2.0f, "%.1f"); + ImGui::InputFloat(STR_C("Min. spacing (m)"), &m_fill_min_spacing, 0.5f, 2.0f, "%.1f"); m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing); ImGui::SetNextItemWidth(200.0f); ImGui::DragFloatRange2("Scale", &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f"); - ImGui::Checkbox("Random rotation", &m_fill_random_rotation); + ImGui::Checkbox(STR_C("Random rotation"), &m_fill_random_rotation); if (!m_fill_random_rotation) ui()->render_rotation_controls(); - ImGui::Checkbox("Large models count as ground (terrain tiles)", &m_fill_models_as_ground); + ImGui::Checkbox(STR_C("Large models count as ground (terrain tiles)"), &m_fill_models_as_ground); - ImGui::Text("Estimated objects: %.0f", std::round(area / 10000.0 * m_fill_density)); + ImGui::Text(STR_C("Estimated objects: %.0f"), std::round(area / 10000.0 * m_fill_density)); - if (ImGui::Button("Fill area")) + if (ImGui::Button(STR_C("Fill area"))) run_area_fill(); ImGui::SameLine(); - std::string const undolabel = "Undo last fill (" + std::to_string(m_fill_last.size()) + ")"; + std::string const undolabel = format(STR_C("Undo last fill (%zu)"), m_fill_last.size()); if (ImGui::Button(undolabel.c_str()) && !m_fill_last.empty()) undo_last_area_fill(); @@ -3242,12 +3370,12 @@ void editor_mode::make_array() auto const step = glm::length(array_offset(*base)); if (step < array_tool::minimal_step) { - m_array.status = "The offsets come to nothing, the copies would sit in the model"; + m_array.status = STR("The offsets come to nothing, the copies would sit in the model"); return; } if (array_count(step) < 2) { - m_array.status = (m_array.settings.fit == 0 ? "An array of one is the model itself, nothing to make" : "The length takes no copy, nothing to make"); + m_array.status = (m_array.settings.fit == 0 ? STR("An array of one is the model itself, nothing to make") : STR("The length takes no copy, nothing to make")); return; } @@ -3316,11 +3444,11 @@ void editor_mode::shape_array() m_array.angles = base->Angles(); m_array.scale = base->Scale(); if (copies.size() < places.size()) - m_array.status = "Only " + std::to_string(copies.size()) + " of " + std::to_string(places.size()) + " copies could be made"; + m_array.status = format(STR_C("Only %zu of %zu copies could be made"), copies.size(), places.size()); else if (copies.empty()) - m_array.status = "No copies with these settings"; + m_array.status = STR("No copies with these settings"); else - m_array.status = "The model and " + std::to_string(copies.size()) + (copies.size() == 1 ? " copy" : " copies") + "; Ctrl+Z takes the array back"; + m_array.status = format(STR_C("The model and %zu copies; Ctrl+Z takes the array back"), copies.size()); } void editor_mode::update_array() @@ -3395,79 +3523,79 @@ void editor_mode::render_array() selected = nullptr; ImGui::PushID("array"); - ImGui::TextDisabled("Copies of the selected model, set out in a row"); - ImGui::RadioButton("Fixed count", &settings.fit, 0); + ImGui::TextDisabled(STR_C("Copies of the selected model, set out in a row")); + ImGui::RadioButton(STR_C("Fixed count"), &settings.fit, 0); ImGui::SameLine(); - ImGui::RadioButton("Fit length", &settings.fit, 1); + ImGui::RadioButton(STR_C("Fit length"), &settings.fit, 1); ImGui::SetNextItemWidth(120.0f); if (settings.fit == 0) { - ImGui::InputInt("Count", &settings.count); + ImGui::InputInt(STR_C("Count"), &settings.count); settings.count = std::clamp(settings.count, 1, array_tool::limit); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Number of the models in the array, the selected one included"); + ImGui::SetTooltip("%s", STR_C("Number of the models in the array, the selected one included")); } else { - ImGui::InputFloat("Length (m)", &settings.length, 1.0f, 10.0f, "%.2f"); + ImGui::InputFloat(STR_C("Length (m)"), &settings.length, 1.0f, 10.0f, "%.2f"); settings.length = std::max(0.0f, settings.length); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "As many copies are made as fit in this length, measured along the row"); + ImGui::SetTooltip("%s", STR_C("As many copies are made as fit in this length, measured along the row")); } - ImGui::Checkbox("Relative offset", &settings.relative); + ImGui::Checkbox(STR_C("Relative offset"), &settings.relative); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n" - "1 along an axis puts the copies end to end"); + ImGui::SetTooltip("%s", STR_C("Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n" + "1 along an axis puts the copies end to end")); if (settings.relative) { ImGui::SetNextItemWidth(200.0f); - ImGui::DragFloat3("x size", &settings.relative_offset.x, 0.01f, 0.0f, 0.0f, "%.3f"); + ImGui::DragFloat3(STR_C("x size"), &settings.relative_offset.x, 0.01f, 0.0f, 0.0f, "%.3f"); } - ImGui::Checkbox("Constant offset", &settings.constant); + ImGui::Checkbox(STR_C("Constant offset"), &settings.constant); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Distance between the copies in metres, along the axes of the model: x, y (up), z.\n" - "With both offsets on the copies are set apart by their sum"); + ImGui::SetTooltip("%s", STR_C("Distance between the copies in metres, along the axes of the model: x, y (up), z.\n" + "With both offsets on the copies are set apart by their sum")); if (settings.constant) { ImGui::SetNextItemWidth(200.0f); ImGui::DragFloat3("m", &settings.constant_offset.x, 0.05f, 0.0f, 0.0f, "%.3f"); } ImGui::SetNextItemWidth(120.0f); - ImGui::DragFloat("Turn per copy (deg)", &settings.turn, 0.1f, -180.0f, 180.0f, "%.2f"); + ImGui::DragFloat(STR_C("Turn per copy (deg)"), &settings.turn, 0.1f, -180.0f, 180.0f, "%.2f"); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Each copy is turned by this much around the vertical axis against the one before it,\n" - "and the row bends with them into an arc"); - ImGui::Checkbox("Follow the ground", &settings.ground); + ImGui::SetTooltip("%s", STR_C("Each copy is turned by this much around the vertical axis against the one before it,\n" + "and the row bends with them into an arc")); + ImGui::Checkbox(STR_C("Follow the ground"), &settings.ground); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "The copies are put as high over the ground as the model is, instead of level with it.\n" - "Not for the models large enough to count as ground themselves"); + ImGui::SetTooltip("%s", STR_C("The copies are put as high over the ground as the model is, instead of level with it.\n" + "Not for the models large enough to count as ground themselves")); if (selected != nullptr) { auto const size = array_size(*selected) * selected->Scale(); auto const step = glm::length(array_offset(*selected)); - ImGui::TextDisabled("Model %.2f x %.2f x %.2f m, %d in the array, %.2f m apart", size.x, size.y, size.z, array_count(step), step); + ImGui::TextDisabled(STR_C("Model %.2f x %.2f x %.2f m, %d in the array, %.2f m apart"), size.x, size.y, size.z, array_count(step), step); } if (array_live()) { // NOTE: kept away from the place of the other button, a click too many on which would make the array twice - ImGui::TextDisabled("The array follows the settings"); + ImGui::TextDisabled(STR_C("The array follows the settings")); ImGui::SameLine(); - if (ImGui::Button("Done")) + if (ImGui::Button(STR_C("Done"))) m_array.base = nullptr; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "The copies stay the way they are. Selecting or changing anything else does the same"); + ImGui::SetTooltip("%s", STR_C("The copies stay the way they are. Selecting or changing anything else does the same")); } else if (selected != nullptr) { - if (ImGui::Button("Make array")) + if (ImGui::Button(STR_C("Make array"))) make_array(); } else { - ImGui::TextDisabled("Select a model to make an array of"); + ImGui::TextDisabled(STR_C("Select a model to make an array of")); } if (false == m_array.status.empty()) ImGui::TextUnformatted(m_array.status.c_str()); @@ -3482,32 +3610,32 @@ void editor_mode::render_gizmo_options() { ImGui::SameLine(); ImGui::SetNextItemWidth(120.0f); - ImGui::SliderFloat("Extent (m), wheel", &Global.EditorOrthoExtent, 5.0f, 5000.0f, "%.0f", 3.0f); + ImGui::SliderFloat(STR_C("Extent (m), wheel"), &Global.EditorOrthoExtent, 5.0f, 5000.0f, "%.0f", 3.0f); } - ImGui::Checkbox("Enabled", &m_gizmo_enabled); + ImGui::Checkbox(STR_C("Enabled"), &m_gizmo_enabled); if (!m_gizmo_enabled) return; if (m_terrain_sculpt || m_chunk_edit) { - ImGui::TextDisabled("Suspended while editing terrain"); + ImGui::TextDisabled(STR_C("Suspended while editing terrain")); return; } // lets the user pick the transform mode without keyboard shortcuts int op = static_cast(m_gizmo_op); - ImGui::RadioButton("Translate (Q)", &op, static_cast(gizmo_operation::translate)); + ImGui::RadioButton(STR_C("Translate (Q)"), &op, static_cast(gizmo_operation::translate)); ImGui::SameLine(); - ImGui::RadioButton("Rotate (W)", &op, static_cast(gizmo_operation::rotate)); + ImGui::RadioButton(STR_C("Rotate (W)"), &op, static_cast(gizmo_operation::rotate)); ImGui::SameLine(); - ImGui::RadioButton("Scale (E)", &op, static_cast(gizmo_operation::scale)); + ImGui::RadioButton(STR_C("Scale (E)"), &op, static_cast(gizmo_operation::scale)); m_gizmo_op = static_cast(op); if (m_gizmo_op != gizmo_operation::scale) // ImGuizmo always scales in local space - ImGui::Checkbox("Local space (R)", &m_gizmo_local); + ImGui::Checkbox(STR_C("Local space (R)"), &m_gizmo_local); if (m_gizmo_op == gizmo_operation::translate) { ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Snap (hold Ctrl)", &m_gizmo_snap); + ImGui::InputFloat(STR_C("Snap (hold Ctrl)"), &m_gizmo_snap); if (m_gizmo_snap < 0.0f) m_gizmo_snap = 0.0f; } @@ -3516,11 +3644,11 @@ void editor_mode::render_gizmo_options() auto const located = editor_includes::parameter_with_role(m_include.info, "pos.x") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.y") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.z") != 0; ImGui::TextDisabled("%s", false == located ? "Include selected, no position parameters described" : - false == m_include.placement ? "Include selected, no gizmo under rotate or scale" : - "Include selected, axes as its template allows"); + false == m_include.placement ? STR_C("Include selected, no gizmo under rotate or scale") : + STR_C("Include selected, axes as its template allows")); } else if (!m_node) - ImGui::TextDisabled("No node selected"); + ImGui::TextDisabled(STR_C("No node selected")); } void editor_mode::render_gizmo() @@ -3874,6 +4002,48 @@ void editor_mode::enter() } } + if (Global.editor_session && false == m_opened_at_track) + { + m_opened_at_track = true; + auto const saved{scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$c")}; + if (false == saved.empty()) + { + std::istringstream values(saved.front()); + glm::dvec3 position{0.0}; + glm::vec3 angle{0.0f}; + if (values >> position.x >> position.y >> position.z >> angle.x >> angle.y) + { + Camera.Pos = position; + Camera.Angle = angle; + Camera.m_owner = nullptr; + FreeFlyModeFlag = true; + Global.pCamera = Camera; + m_saved_camera = {position, angle}; + WriteLog("Editor: camera where it was at the last save"); + } + } + for (auto *track : simulation::Paths.sequence()) + { + if (m_saved_camera.has_value() || track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + auto const &path{track->m_paths.front()}; + auto const start{path.points[segment_data::point::start]}; + auto const end{path.points[segment_data::point::end]}; + auto const centre{0.5 * (start + end)}; + glm::dvec3 along{end.x - start.x, 0.0, end.z - start.z}; + along = glm::length(along) > 1e-6 ? glm::normalize(along) : glm::dvec3{0.0, 0.0, 1.0}; + glm::dvec3 const side{-along.z, 0.0, along.x}; + Camera.Pos = centre - along * 30.0 + side * 15.0 + glm::dvec3{0.0, 20.0, 0.0}; + auto const look{glm::normalize(centre - Camera.Pos)}; + Camera.Angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + Camera.m_owner = nullptr; + FreeFlyModeFlag = true; + Global.pCamera = Camera; + WriteLog("Editor: camera at the first track, " + track->name()); + break; + } + } + Global.ControlPicking = true; EditorModeFlag = true; @@ -3930,6 +4100,17 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in #endif bool anyModifier = Mods & (GLFW_MOD_SHIFT | GLFW_MOD_CONTROL | GLFW_MOD_ALT); + if (Action == GLFW_RELEASE && Key >= 0 && Key <= GLFW_KEY_LAST) + { + input::keys[Key] = GLFW_RELEASE; + if (Key == GLFW_KEY_LEFT_SHIFT || Key == GLFW_KEY_RIGHT_SHIFT) + input::key_shift = false; + if (Key == GLFW_KEY_LEFT_CONTROL || Key == GLFW_KEY_RIGHT_CONTROL) + input::key_ctrl = false; + if (Key == GLFW_KEY_LEFT_ALT || Key == GLFW_KEY_RIGHT_ALT) + input::key_alt = false; + } + // first give UI a chance to handle the key if (!anyModifier && m_userinterface->on_key(Key, Action)) return; @@ -3953,6 +4134,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_W: m_gizmo_op = gizmo_operation::rotate; break; case GLFW_KEY_E: m_gizmo_op = gizmo_operation::scale; break; case GLFW_KEY_R: m_gizmo_local = !m_gizmo_local; break; + case GLFW_KEY_B: handled = ui()->mode() != nodebank_panel::TRACK && bend_shortcut(); break; default: handled = false; break; } if (handled) @@ -4202,6 +4384,20 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } + if (Button == GLFW_MOUSE_BUTTON_LEFT && is_press(Action) && false == m_bend.picking && ui()->mode() != nodebank_panel::TRACK && (bend_drag_start(Mods) || bend_click())) + { + m_input.mouse.button(Button, Action); + return; + } + + if (m_bend.picking && Button == GLFW_MOUSE_BUTTON_LEFT && ui()->mode() != nodebank_panel::TRACK) + { + if (is_press(Action)) + bend_pick(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); + m_input.mouse.button(Button, Action); + return; + } + if (Button == GLFW_MOUSE_BUTTON_LEFT) { @@ -4362,9 +4558,9 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const void editor_mode::render_change_history(){ - ImGui::Begin("Editor History", &m_change_history, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Begin(STR_C("Editor History"), &m_change_history, ImGuiWindowFlags_AlwaysAutoResize); int maxsize = m_max_history_size; - if (ImGui::InputInt("Max history size", &maxsize)) + if (ImGui::InputInt(STR_C("Max history size"), &maxsize)) { m_max_history_size = std::max(0, maxsize); if ((int)m_history.size() > m_max_history_size && m_max_history_size >= 0) @@ -4379,14 +4575,14 @@ void editor_mode::render_change_history(){ } float dist = kMaxPlacementDistance; - if (ImGui::InputFloat("Max placement distance", &dist)) + if (ImGui::InputFloat(STR_C("Max placement distance"), &dist)) { kMaxPlacementDistance = std::max(0.0f, dist); } ImGui::Separator(); - ImGui::Text("History (newest at end): %zu entries", m_history.size()); + ImGui::Text(STR_C("History (newest at end): %zu entries"), m_history.size()); ImGui::BeginChild("history_list", ImVec2(400, 200), true); for (int i = 0; i < (int)m_history.size(); ++i) { @@ -4410,7 +4606,7 @@ void editor_mode::render_change_history(){ ImGui::EndChild(); ImGui::Separator(); - if (ImGui::Button("Clear History")) + if (ImGui::Button(STR_C("Clear History"))) { m_history.clear(); g_redo.clear(); @@ -4419,7 +4615,7 @@ void editor_mode::render_change_history(){ ImGui::SameLine(); ImGui::SameLine(); - if (ImGui::Button("Undo Selected")) + if (ImGui::Button(STR_C("Undo Selected"))) { if (m_selected_history_idx >= 0 && m_selected_history_idx < (int)m_history.size()) { @@ -4467,3 +4663,34 @@ void editor_mode::set_focus_active(bool isActive) { m_focus_active = isActive; } + +void editor_mode::starter_trainset(std::vector &Trailing) +{ + static std::regex const trainset{R"((^|\n)[ \t]*trainset[ \t])", std::regex::icase}; + for (std::size_t handle = 1; handle <= scene::Layers.size(); ++handle) + { + auto const &layer{scene::Layers.layer(static_cast(handle))}; + if (layer.created) + continue; + std::ifstream input{Global.asCurrentSceneryPath + layer.name, std::ios_base::binary}; + std::string const content{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + if (std::regex_search(content, trainset)) + return; + } + std::error_code error; + if (false == std::filesystem::exists("dynamic/pkp/wmb10_v1/wmb10.mmd", error)) + return; + for (auto const *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || (track->iCategoryFlag & 1) == 0 || track->name().empty() || track->m_paths.empty()) + continue; + auto const length{geometry::bezier{track->m_paths.front()}.plan_length()}; + if (length < 15.0) + continue; + Trailing.push_back("trainset none " + track->name() + " " + to_string(std::min(length, 0.5 * length + 5.0), 2) + " 0"); + Trailing.push_back("node -1 0 wmb10-6457 dynamic pkp/wmb10_v1 wmb10-6457 wmb10 0 headdriver 0 0 enddynamic"); + Trailing.push_back("endtrainset"); + WriteLog("Editor: the scenery has no trainset, a WMB10 is put on " + track->name() + " to drive"); + return; + } +} diff --git a/application/editormode.h b/application/editormode.h index 47f3a28f..ee0fade6 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -32,6 +32,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -127,7 +128,7 @@ class editor_mode : public application_mode, private editor_track::observer struct stored_profile; struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend }; // copy of a model an array made struct array_copy { @@ -313,6 +314,17 @@ class editor_mode : public application_mode, private editor_track::observer bool m_newscenery_asked{false}; void render_new_scenery_popup(); bool restart_for_new_scenery(); + bool restart_editor(std::string const &Scenery); + bool m_openscenery_asked{false}; + bool m_opened_at_track{false}; + bool m_turntable_included{false}; + bool turntable_controls(std::vector &Rootstatements); + void starter_trainset(std::vector &Trailing); + std::optional> m_saved_camera; + char m_openscenery_search[64]{}; + std::vector m_openscenery_list; + std::string m_openscenery_choice; + void render_open_scenery_popup(); 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}; @@ -702,6 +714,7 @@ class editor_mode : public application_mode, private editor_track::observer void scan_turntables(); std::string turntable_name_for_new() const; bool turntable_read(TTrack *Table); + std::vector turntable_includes(TTrack const &Table); glm::dvec3 turntable_centre() const; std::vector turntable_lines() const; double turntable_snap(double const Heading) const; @@ -756,6 +769,7 @@ class editor_mode : public application_mode, private editor_track::observer void lay_enter(); bool lay_heading(glm::dvec2 &Heading) const; std::vector lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const; + alignment::result lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const; void render_switch_ui(); bool start_switch_placement(); void finish_switch_placement(); @@ -1192,7 +1206,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector parameter_labels; std::vector parameter_placement; // filled in with zeros: the template is placed in its own space std::vector> 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 + int length_mode{2}; // 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 @@ -1255,6 +1269,65 @@ class editor_mode : public application_mode, private editor_track::observer void sweeps_committed(std::vector const &Tracks); void sweeps_split(TTrack &Original, TTrack &Created); + struct bend_source + { + TAnimModel *model{nullptr}; + scene::instance_handle instance{0}; + std::string file; + std::string skin{"none"}; + std::vector parameters; + glm::dmat4 transform{1.0}; + glm::dvec3 scale{1.0}; + scene::layer_handle layer{null_handle}; + }; + struct bend_tool + { + int axis{0}; + bool turned{false}; + int along_anchor{1}; + int side_anchor{0}; + int height_anchor{0}; + glm::dvec3 point{0.0}; + bool picking{false}; + void const *tracks_for{nullptr}; + glm::dvec3 tracks_at{0.0}; + std::vector> tracks; + int track{0}; + sweep_node *edited{nullptr}; + std::string signature; + bool marker_valid{false}; + glm::dvec3 marker{0.0}; + std::string status; + int dragging{0}; + sweep_node::state drag_state; + glm::dvec3 drag_anchor{0.0}; + double drag_station{0.0}; + std::string drag_label; + sweep_node *hover{nullptr}; + bool over_marker{false}; + std::string label; + std::string details; + }; + bend_tool m_bend; + bool bend_drag_start(int const Mods); + void bend_drag_update(); + sweep_node *sweep_under(glm::dvec3 const &Point) const; + bool bend_click(); + bool bend_shortcut(); + bool bend_source_of_selection(bend_source &Source) const; + void bend_find_tracks(glm::dvec3 const &Point, void const *Key); + bool bend_definition(bend_source const &Source, TTrack &Track, sweep_node::state &State, std::vector &Tracks); + void bend_selection(); + void bend_edit(sweep_node::state const &State); + void bend_reanchor(sweep_node::state &State, int const Side, int const Height, glm::dvec3 const &Point) const; + double bend_fixed(sweep_node const &Sweep, glm::dvec3 const &Point) const; + void bend_stretch(sweep_node const &Sweep, double const Fixed, double const Length, glm::dvec3 const &Point, sweep_node::state &State) const; + void bend_pick(glm::dvec3 const &Point); + glm::dvec3 bend_marker(sweep_node const &Sweep) const; + void restore_bend(EditorSnapshot &Snapshot, bool const Undo); + void render_bend(); + void draw_bend_overlay() const; + // script of editor actions given in the EU07_EDITOR_SELFTEST file, run a step at a time, for testing without a person struct selftest { diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 16cc864b..da941b4c 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "simulation/simulation.h" #include "utilities/Logs.h" +#include "world/Sweep.h" #include "world/Track.h" #include "imgui/imgui.h" #include "utilities/translation.h" @@ -324,6 +325,33 @@ template void visit_triangles(TSubModel *Submodel, glm::dmat4 } } +// the triangle enters the gauge when any point of a grid spread over it does; the deepest such point goes to the hit +void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, editor_mode::gauge_hit &Hit) +{ + auto const middle{(A + B + C) / 3.0}; + if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)}))) + return; + auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})}; + auto const steps{std::clamp(static_cast(std::ceil(edge / 0.5)), 1, 24)}; + for (int u = 0; u <= steps; ++u) + for (int v = 0; u + v <= steps; ++v) + { + auto const point{A + (B - A) * (static_cast(u) / steps) + (C - A) * (static_cast(v) / steps)}; + auto const found{Space.intrusion(point)}; + if (found.depth <= 0.0) + continue; + if (found.depth > Hit.depth) + { + Hit.depth = found.depth; + Hit.point = point; + Hit.track = found.span->track; + Hit.path = found.span->path; + Hit.direction = found.direction; + } + return; + } +} + // the deepest place of the model in the gauge, added to the hits when it enters it void scan_model(corridor const &Space, TAnimModel &Instance, std::vector &Hits) { @@ -332,31 +360,23 @@ void scan_model(corridor const &Space, TAnimModel &Instance, std::vectorGetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { - auto const middle{(A + B + C) / 3.0}; - if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)}))) - return; - auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})}; - auto const steps{std::clamp(static_cast(std::ceil(edge / 0.5)), 1, 24)}; - for (int u = 0; u <= steps; ++u) - for (int v = 0; u + v <= steps; ++v) - { - auto const point{A + (B - A) * (static_cast(u) / steps) + (C - A) * (static_cast(v) / steps)}; - auto const found{Space.intrusion(point)}; - if (found.depth <= 0.0) - continue; - if (found.depth > hit.depth) - { - hit.depth = found.depth; - hit.point = point; - hit.track = found.span->track; - hit.path = found.span->path; - hit.direction = found.direction; - } - return; - } - }); + visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { scan_triangle(Space, A, B, C, hit); }); + if (hit.depth > 0.0) + Hits.push_back(std::move(hit)); +} + +// the same for the copies of a model laid along a curve +void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) +{ + if (Sweep.m_editorremoved || false == Space.nearby(Sweep.location(), Sweep.radius())) + return; + editor_mode::gauge_hit hit{Sweep.name(), Sweep.location()}; + for (auto const &shape : Sweep.create_shapes()) + { + auto const &vertices{shape.data().vertices}; + for (std::size_t i = 0; i + 2 < vertices.size(); i += 3) + scan_triangle(Space, vertices[i].position, vertices[i + 1].position, vertices[i + 2].position, hit); + } if (hit.depth > 0.0) Hits.push_back(std::move(hit)); } @@ -372,6 +392,9 @@ void scan_models(corridor const &Space, std::vector &Hit for (auto *instance : simulation::Instances.sequence()) if (instance != nullptr) scan_model(Space, *instance, Hits); + for (auto *sweep : simulation::Sweeps.sequence()) + if (sweep != nullptr) + scan_sweep(Space, *sweep, Hits); sort_hits(Hits); } @@ -579,14 +602,22 @@ void editor_mode::step_gauge_map() { auto &scan{*m_gauge.scan}; auto const &instances{simulation::Instances.sequence()}; + auto const &sweeps{simulation::Sweeps.sequence()}; + auto const total{instances.size() + sweeps.size()}; auto const until{std::chrono::steady_clock::now() + gauge_slice}; - while (scan.next < instances.size() && std::chrono::steady_clock::now() < until) + while (scan.next < total && std::chrono::steady_clock::now() < until) { - if (auto *instance{instances[scan.next++]}) - scan_model(*scan.space, *instance, m_gauge.map.hits); + auto const next{scan.next++}; + if (next < instances.size()) + { + if (auto *instance{instances[next]}) + scan_model(*scan.space, *instance, m_gauge.map.hits); + } + else if (auto *sweep{sweeps[next - instances.size()]}) + scan_sweep(*scan.space, *sweep, m_gauge.map.hits); } - scan.progress = instances.empty() ? 1.f : static_cast(scan.next) / instances.size(); - if (scan.next >= instances.size()) + scan.progress = total == 0 ? 1.f : static_cast(scan.next) / total; + if (scan.next >= total) finish_gauge_map(); } diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 8142eef9..ca07b718 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -14,12 +14,24 @@ http://mozilla.org/MPL/2.0/. #include "application/editorprojection.h" #include "simulation/simulation.h" +#include "scene/scenelayers.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "world/Track.h" +#include "world/MemCell.h" +#include "world/Event.h" #include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include +#include +#include #include "editor/editorFormat.hpp" +#include #include #include #include @@ -32,6 +44,23 @@ http://mozilla.org/MPL/2.0/. // sweep =... 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) +// selectmodel the model instance nearest the point +// bend =... the selected model bent along a track: track (index on the list), axis (auto, z, x), +// turned (0, 1), along (start, origin, middle, end, point), side (origin, near, far, +// middle, point), height (origin, bottom, top, edge, point), point=,,, +// pick=,, (a point of the scene picked as the reference point), go (0, 1) +// selectinclude the include placed nearest the point +// bendset =... the bent model being edited: length, lateral, height, along, side, height +// bendprobe logs the bent model being edited +// undo, redo +// key [ctrl] a key or right button event, as the window gives it +// lineside the tab of the objects along the track in the track window +// event queues the event +// memcell logs the memory cell +// bendpanel unfolds the bend section of the toolset +// openscenerypopup the window of File / Open scenery, its list searched for zz_ +// openscenery the editor starts again with the scenery +// checkspans logs whether the text the scenery files hold at each node's place starts and ends it // spread 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 @@ -123,6 +152,7 @@ void editor_mode::selftest_step() settings.bend = get("mode", "bend") != "repeat"; settings.step = std::stod(get("step", "0")); settings.widen = get("widen", "0") == "1"; + settings.platform = get("platform", "0") == "1"; settings.face = get("face", "1") == "1"; settings.tilt = get("tilt", "0") == "1"; settings.from = std::stod(get("from", "0")); @@ -135,6 +165,10 @@ void editor_mode::selftest_step() tool.open = true; sweep_curve(); sweep_measure(); + if (values.count("widen")) + settings.widen = get("widen", "0") == "1"; + if (values.count("platform")) + settings.platform = get("platform", "0") == "1"; if (get("create", "1") == "1") { sweep_create(); @@ -202,6 +236,37 @@ void editor_mode::selftest_step() at.widening[definition.lateral >= 0.0 ? 1 : 0], std::max(0.0, at.cant[definition.lateral >= 0.0 ? 1 : 0]))); } } + else if (command == "profile") + { + std::string name; + double station{0.0}; + words >> name >> station; + auto *sweep{simulation::Sweeps.find(name)}; + if (sweep == nullptr) + { + WriteLog("SELFTEST profile: no such sweep"); + continue; + } + auto const &definition{sweep->definition()}; + std::map> columns; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + double lateral; + auto const at{sweep->project(vertex.position, lateral)}; + if (std::abs(at - station) > 1.0) + continue; + auto const axis{sweep->frame_at(at)}; + auto const height{vertex.position.y - axis.position.y - 0.18 - sweep_node::rail_head(axis.roll, definition.lateral)}; + auto &column{columns.try_emplace(static_cast(std::floor(lateral / 0.1)), std::numeric_limits::max(), -std::numeric_limits::max()).first->second}; + column.first = std::min(column.first, height); + column.second = std::max(column.second, height); + } + std::string text; + for (auto const &[index, column] : columns) + text += format(" [%.1f %.2f..%.2f]", index * 0.1, column.first, column.second); + WriteLog(format("SELFTEST profile %s at %.0f m:", name.c_str(), station) + text); + } else if (command == "fouling") { std::map values; @@ -309,6 +374,46 @@ void editor_mode::selftest_step() 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 == "lay") + { + std::string name, side; + words >> name >> side; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->m_paths.empty()) + { + WriteLog("SELFTEST lay: no track " + name); + return; + } + auto const &path{track->m_paths.front()}; + auto const atend{side == "end"}; + editor_track::snap_target start; + start.track = track; + start.point = {0, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + start.position = path.points[atend ? segment_data::point::end : segment_data::point::start]; + auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + start.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[atend ? segment_data::point::start : segment_data::point::end] - start.position); + m_lay.points = {start.position}; + m_lay.start = start; + for (glm::dvec3 point{0.0, start.position.y, 0.0}; words >> point.x >> point.z;) + m_lay.points.push_back(point); + lay_finish({}); + WriteLog("SELFTEST lay: " + m_lay.status); + } + else if (command == "analyse") + { + std::string name; + words >> name; + editor_track::curve curve; + if (auto *track{simulation::Paths.find(name)}; track != nullptr && editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + std::string arcs; + for (auto const &arc : curve.arcs) + arcs += format(" R%.0f/%.1fdeg/after %.1fm", arc.radius, glm::degrees(arc.turn), arc.transition); + WriteLog(format("SELFTEST analyse %s: R %.0f, compound %d, in %.1f m, out %.1f m, turn %.1f deg,", name.c_str(), curve.radius, curve.compound ? 1 : 0, curve.transition_in, curve.transition_out, glm::degrees(curve.turn)) + arcs); + } + else + WriteLog("SELFTEST analyse " + name + ": no curve"); + } else if (command == "projtest") { screen_projection const projection; @@ -362,6 +467,330 @@ void editor_mode::selftest_step() { save(); } + else if (command == "turntable") + { + glm::dvec3 centre{0.0}; + double yaw{0.0}; + words >> centre.x >> centre.y >> centre.z >> yaw; + turntable_place(centre, yaw); + for (double angle; words >> angle;) + { + m_turntable.heading = m_turntable.table_yaw + angle; + m_turntable.exit_length = 10.0; + track_intent intent; + intent.what = track_intent::kind::turntable_exit; + turntable_press(intent); + } + WriteLog("SELFTEST turntable: " + (m_turntable.error.empty() ? m_turntable.status : m_turntable.error)); + } + else if (command == "selectmodel") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TAnimModel *nearest{nullptr}; + auto best{50.0}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr) + continue; + auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; + if (distance < best) + { + best = distance; + nearest = model; + } + } + m_node = nearest; + select_include(0); + ui()->set_node(m_node); + WriteLog("SELFTEST selectmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); + } + else if (command == "selectinclude") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + scene::instance_handle nearest{0}; + auto best{50.0}; + m_node = nullptr; + for (scene::instance_handle instance = 1; instance <= scene::Layers.instance_count(); ++instance) + { + if (false == scene::Layers.removable(instance)) + continue; + select_include(instance); + bend_source source; + if (false == bend_source_of_selection(source)) + continue; + auto const at{glm::dvec3(source.transform[3])}; + auto const distance{glm::length(glm::dvec2{at.x - point.x, at.z - point.z})}; + if (distance < best) + { + best = distance; + nearest = instance; + } + } + select_include(nearest); + WriteLog("SELFTEST selectinclude: " + (nearest != 0 ? *scene::Layers.instance(nearest).file : std::string{"none"})); + } + else if (command == "bend" || command == "bendset") + { + std::map 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 const vector = [](std::string Text) { + std::replace(Text.begin(), Text.end(), ',', ' '); + std::istringstream numbers(Text); + glm::dvec3 result{0.0}; + numbers >> result.x >> result.y >> result.z; + return result; + }; + auto const index = [](std::string const &Text, std::vector const &Names) { + return static_cast(std::find(Names.begin(), Names.end(), Text) - Names.begin()); + }; + auto &tool{m_bend}; + auto const along{values.count("along") ? index(values["along"], {"start", "origin", "middle", "end", "point"}) : tool.along_anchor}; + auto const side{values.count("side") ? index(values["side"], {"origin", "near", "far", "middle", "point"}) : tool.side_anchor}; + auto const height{values.count("height") ? index(values["height"], {"origin", "bottom", "top", "edge", "point"}) : tool.height_anchor}; + auto const point{values.count("point") ? vector(values["point"]) : tool.point}; + if (command == "bend") + { + tool.edited = nullptr; + tool.along_anchor = along; + tool.side_anchor = side; + tool.height_anchor = height; + tool.point = point; + tool.axis = values.count("axis") ? index(values["axis"], {"auto", "z", "x"}) : 0; + tool.turned = values["turned"] == "1"; + bend_source source; + if (false == bend_source_of_selection(source)) + { + std::string reason; + auto const editable{m_node != nullptr && scene::Layers.editable(m_node, &reason)}; + WriteLog(format("SELFTEST bend: nothing to bend (node %p template %d editable %d %s, instance %d)", static_cast(m_node), m_node != nullptr && m_node->from_template() ? 1 : 0, editable ? 1 : 0, reason.c_str(), static_cast(m_instance))); + continue; + } + tool.tracks_for = nullptr; + bend_find_tracks(glm::dvec3(source.transform[3]), source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))); + tool.track = values.count("track") ? std::stoi(values["track"]) : 0; + if (values.count("pick")) + bend_pick(vector(values["pick"])); + if (tool.tracks.empty()) + { + WriteLog("SELFTEST bend: no track nearby"); + continue; + } + sweep_node::state state; + std::vector tracks; + bend_definition(source, *tool.tracks[tool.track].first, state, tracks); + WriteLog(format("SELFTEST bend: track %s, %zu paths, anchor before %.3f %.3f %.3f, lateral %.3f height %.3f from %.3f to %.3f, axis %s flip %d; %s", tool.tracks[tool.track].first->name().c_str(), tracks.size(), + tool.marker.x, tool.marker.y, tool.marker.z, state.lateral, state.height, state.from, state.to, state.along_x ? "x" : "z", state.flip ? 1 : 0, tool.status.c_str())); + if (values["go"] == "1") + { + bend_selection(); + WriteLog("SELFTEST bend: " + tool.status); + } + } + else if (auto *sweep{tool.edited}) + { + auto state{sweep->definition()}; + tool.along_anchor = along; + if (side != tool.side_anchor || height != tool.height_anchor || point != tool.point) + { + tool.side_anchor = side; + tool.height_anchor = height; + tool.point = point; + bend_reanchor(state, side, height, point); + } + if (values.count("lateral")) + state.lateral = std::stod(values["lateral"]); + if (values.count("up")) + state.height = std::stod(values["up"]); + if (values.count("widen")) + state.widen = values["widen"] == "1"; + if (values.count("pick")) + { + bend_pick(vector(values["pick"])); + state = sweep->definition(); + } + if (values.count("length")) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), std::stod(values["length"]), state.point, state); + } + bend_edit(state); + WriteLog("SELFTEST bendset done"); + } + } + else if (command == "bendprobe") + { + auto *sweep{m_bend.edited}; + if (sweep == nullptr) + { + WriteLog("SELFTEST bendprobe: nothing bent"); + continue; + } + glm::dvec3 low, high, shift; + sweep->bounds(low, high, shift); + auto const &state{sweep->definition()}; + auto const marker{bend_marker(*sweep)}; + glm::dvec3 boxlow{std::numeric_limits::max()}, boxhigh{-std::numeric_limits::max()}; + std::size_t vertices{0}; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + boxlow = glm::min(boxlow, vertex.position); + boxhigh = glm::max(boxhigh, vertex.position); + ++vertices; + } + WriteLog(format("SELFTEST bendprobe %s: stations %.3f - %.3f, along the reference line %.3f m, model %.3f m, lateral %.3f height %.3f anchors %d/%d point %.3f %.3f %.3f, marker %.3f %.3f %.3f, box %.2f %.2f %.2f - %.2f %.2f %.2f, %zu vertices, removed %d", + sweep->name().c_str(), sweep->start(), sweep->end(), sweep->distance_at(sweep->end()) - sweep->distance_at(sweep->start()), high.x - low.x, state.lateral, state.height, state.side_anchor, state.height_anchor, state.point.x, + state.point.y, state.point.z, marker.x, marker.y, marker.z, boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z, vertices, sweep->m_editorremoved ? 1 : 0)); + } + else if (command == "modelbox") + { + auto *model{dynamic_cast(m_node)}; + bend_source source; + if (model == nullptr || false == bend_source_of_selection(source)) + { + WriteLog("SELFTEST modelbox: no model selected"); + continue; + } + scene::node_data data; + sweep_node probe{data}; + sweep_node::state state; + state.model = source.file; + state.skin = source.skin; + state.scale = source.scale; + probe.define(state); + glm::dvec3 boxlow{std::numeric_limits::max()}, boxhigh{-std::numeric_limits::max()}; + for (auto const &item : probe.items()) + { + auto const transform{source.transform * glm::scale(glm::dmat4(1.0), source.scale) * item.transform}; + std::vector pending{item.model->GetSMRoot()}; + while (false == pending.empty()) + { + auto *submodel{pending.back()}; + pending.pop_back(); + for (; submodel != nullptr; submodel = submodel->Next) + { + if (submodel->Child != nullptr) + pending.push_back(submodel->Child); + if (submodel->eType != GL_TRIANGLES || submodel->fSquareMinDist > 0.f || (submodel->m_geometry.handle.bank == 0 && submodel->m_geometry.handle.chunk == 0)) + continue; + glm::dmat4 local{1.0}; + for (auto *parent = submodel; parent != nullptr; parent = parent->Parent) + if ((parent->iFlags & 0xC000) != 0 && parent->GetMatrix() != nullptr) + local = glm::dmat4(glm::make_mat4(parent->GetMatrix()->readArray())) * local; + for (auto const &vertex : GfxRenderer->Vertices(submodel->m_geometry.handle)) + { + auto const world{glm::dvec3(transform * local * glm::dvec4(glm::dvec3(vertex.position), 1.0))}; + boxlow = glm::min(boxlow, world); + boxhigh = glm::max(boxhigh, world); + } + } + } + } + WriteLog(format("SELFTEST modelbox: %.2f %.2f %.2f - %.2f %.2f %.2f", boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z)); + } + else if (command == "key") + { + std::string key, action, mods; + words >> key >> action >> mods; + if (key == "RMB") + m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, action == "press" ? GLFW_PRESS : GLFW_RELEASE); + else + { + auto const code{key == "S" ? GLFW_KEY_S : key == "W" ? GLFW_KEY_W : key == "CTRL" ? GLFW_KEY_LEFT_CONTROL : 0}; + on_key(code, 0, action == "press" ? GLFW_PRESS : GLFW_RELEASE, mods == "ctrl" ? GLFW_MOD_CONTROL : 0); + } + WriteLog(format("SELFTEST key %s %s %s: camera %.2f %.2f %.2f, key S %d, right button %d", key.c_str(), action.c_str(), mods.c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, static_cast(input::keys[GLFW_KEY_S]), m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT))); + } + else if (command == "lineside") + { + std::string tab; + words >> tab; + show_track_tab(track_tab::lineside); + (tab == "vehicle" ? m_vehicle.expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; + } + else if (command == "event") + { + std::string name; + words >> name; + auto *event{simulation::Events.FindEvent(name)}; + if (event != nullptr) + simulation::Events.AddToQuery(event, nullptr); + WriteLog("SELFTEST event " + name + (event != nullptr ? " queued" : ": no such event")); + } + else if (command == "memcell") + { + std::string name; + words >> name; + auto *cell{simulation::Memory.find(name)}; + WriteLog("SELFTEST memcell " + name + (cell != nullptr ? format(": %s %.1f %.1f", cell->Text().c_str(), cell->Value1(), cell->Value2()) : std::string{": none"})); + } + else if (command == "bendpanel") + { + ui()->expand_bend(); + } + else if (command == "newscenerypopup") + { + m_newscenery_asked = true; + } + else if (command == "openscenerypopup") + { + m_openscenery_asked = true; + std::snprintf(m_openscenery_search, sizeof(m_openscenery_search), "%s", "zz_"); + } + else if (command == "openscenery") + { + std::string name; + words >> name; + WriteLog(restart_editor(name) ? "SELFTEST openscenery: started " + name : "SELFTEST openscenery: failed"); + } + else if (command == "gauge") + { + std::string name; + words >> name; + if (auto *track{simulation::Paths.find(name)}) + { + scan_gauge(*track); + WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast(m_gauge.line.hits.size()))); + for (auto const &hit : m_gauge.line.hits) + WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z)); + } + else + WriteLog("SELFTEST gauge " + name + ": no such track"); + } + else if (command == "deletetrack") + { + delete_selected_track(); + WriteLog("SELFTEST deletetrack"); + } + else if (command == "ttincludes") + { + std::string name; + words >> name; + auto const *track{simulation::Paths.find(name)}; + WriteLog(format("SELFTEST ttincludes %s: track %s, includes %d", name.c_str(), track == nullptr ? "none" : track->m_editorremoved ? "removed" : "present", + track != nullptr ? static_cast(turntable_includes(*track).size()) : -1)); + } + else if (command == "undo") + { + undo_last(); + WriteLog("SELFTEST undo"); + } + else if (command == "redo") + { + redo_last(); + WriteLog("SELFTEST redo"); + } + else if (command == "checkspans") + { + WriteLog("SELFTEST spans: " + scene::Layers.check_sources()); + } else if (command == "log") { std::string text; diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp index a52369a4..c7f589e3 100644 --- a/application/editormodesweep.cpp +++ b/application/editormodesweep.cpp @@ -21,12 +21,17 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/Logs.h" #include "world/Sweep.h" +#include "model/AnimModel.h" #include "world/Track.h" #include "imgui/imgui.h" #include "utilities/translation.h" +#include #include +#include +#include #include +#include #include #include #include @@ -48,6 +53,49 @@ double const kSweepGrab{25.0}; // m from the track, a press this close marks the std::size_t const kSweepModelList{400}; double const kRailTop{0.18}; // m from the points of a track to the top of its rails +glm::dvec3 flat(glm::dvec3 Vector) +{ + Vector.y = 0.0; + return glm::length(Vector) > 1e-9 ? glm::normalize(Vector) : glm::dvec3{0.0, 0.0, 1.0}; +} + +// the edges of the platforms in peronowe/ are named by the count of the edges and the height: 1k96w200m, 2k30w..., 1kraw200m55cm, +// old_1k76w..., old2_peron1k30w..., old_peron1k_prosty_h70...; the rest there are the benches, the lamps and such. their side facing the track is the edge +double platform_height(std::string File) +{ + std::transform(File.begin(), File.end(), File.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + std::replace(File.begin(), File.end(), '\\', '/'); + auto const slash{File.rfind('/')}; + if (slash == std::string::npos || File.substr(0, slash).find("peron") == std::string::npos) + return 0.0; + static std::regex const pattern{R"(^(?:old2?_)?(?:(?:peron)?\dk(\d\d)|\dkraw\d+m(\d\d)cm|peron.*_h(\d\d))(?:[^\d]|$))"}; + std::smatch match; + auto const name{File.substr(slash + 1)}; + if (false == std::regex_search(name, match, pattern)) + return 0.0; + for (std::size_t i = 1; i < match.size(); ++i) + if (match[i].matched) + return std::stoi(match[i].str()) / 100.0; + return 0.0; +} + +bool platform_model(std::string const &File) +{ + return platform_height(File) > 0.0; +} + +// the edge of the platform 1.725 m from the axis on the side it stands, set back further by the widening of the gauge +void platform_edge(sweep_node::state &State, std::string const &Model, double const Base) +{ + State.lateral = std::copysign(1.725, State.lateral == 0.0 ? 1.0 : State.lateral); + State.side_anchor = 1; + State.height_anchor = 3; + if (auto const height{platform_height(Model)}; height > 0.0) + State.height = Base + height; + State.widen = true; + State.platform = true; +} + segment_data reversed(segment_data const &Path) { segment_data result{Path}; @@ -225,6 +273,13 @@ void editor_mode::sweep_measure() if (model == tool.measured) return; tool.measured = model; + if (tool.edited == nullptr || tool.edited->definition().model != model) + { + if (platform_model(model)) + platform_edge(tool.settings, model, 0.0); + else + tool.settings.platform = tool.settings.widen = false; + } tool.model_length = 0.0; if (model.empty()) return; @@ -266,6 +321,13 @@ void editor_mode::sweep_create() sweep_curve(); if (tool.curve.empty() || tool.model[0] == '\0') return; + for (auto wanted{sweep_definition().to}; wanted > tool.curve_length && tool.outline.size() > 1;) + { + auto const end{tool.outline.back()}; + auto const ahead{end + flat(end - tool.outline[tool.outline.size() - 2]) * (wanted - tool.curve_length)}; + if (false == sweep_extend(true, ahead)) + break; + } static int counter{0}; scene::node_data data; data.type = "sweep"; @@ -293,6 +355,7 @@ void editor_mode::sweep_create() m_history.push_back(std::move(snap)); g_redo.clear(); tool.edited = sweep; + m_bend.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")}; @@ -589,6 +652,27 @@ void editor_mode::sweeps_committed(std::vector const &Tracks) entry.first->define(entry.second); entry.first->mark_dirty(); } + for (auto &entry : states) + { + auto *sweep{entry.first}; + if (false == sweep->bridge()) + continue; + sweep->mark_dirty(); + m_sweep_links.erase(std::remove_if(m_sweep_links.begin(), m_sweep_links.end(), [&](sweep_link const &Link) { return Link.sweep == sweep; }), m_sweep_links.end()); + auto const &pieces{sweep->definition().pieces}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (std::size_t i = 0; i < pieces.size(); ++i) + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + { + auto const along{same_piece(pieces[i], track->m_paths[path])}; + if (along || same_piece(pieces[i], reversed(track->m_paths[path]))) + m_sweep_links.push_back({sweep, i, track, path, false == along}); + } + } + } } void editor_mode::sweeps_split(TTrack &Original, TTrack &Created) @@ -620,34 +704,10 @@ void editor_mode::sweeps_split(TTrack &Original, TTrack &Created) 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 alongside{sweeps_near(*track)}; - if (false == alongside.empty()) - { - ImGui::TextDisabled("%s", STR_C("By this track:")); - for (auto *sweep : alongside) - { - 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; }; @@ -726,7 +786,8 @@ void editor_mode::render_sweep_ui() { auto const ¶meter{info.parameter(id)}; auto const placement{parameter.role.rfind("pos.", 0) == 0 || parameter.role.rfind("rot.", 0) == 0}; - auto const value{placement ? std::string{"0"} : parameter.value.empty() ? std::string{"none"} : parameter.value}; + auto const &choices{tool.parameter_choices[id - 1]}; + auto const value{placement ? std::string{"0"} : false == parameter.value.empty() ? parameter.value : choices.empty() ? std::string{"none"} : choices.front()}; 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; @@ -797,45 +858,17 @@ void editor_mode::render_sweep_ui() 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; + platform_edge(settings, tool.model, 0.0); 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; + if (false == tool.curve.empty() && tool.model_length > 0.0 && ImGui::Button(STR_C("Lay it along the selected path"))) 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"))) @@ -881,6 +914,16 @@ void editor_mode::render_sweep_ui() 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")); + if (ImGui::Checkbox(STR_C("Platform"), &settings.platform)) + { + if (settings.platform) + platform_edge(settings, tool.model, 0.0); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts the edge facing the track 1.725 m from its axis, set back by the widening of the gauge in the curves.\n" + "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" + "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5")); ImGui::TextDisabled("%s", STR_C("Length")); ImGui::SameLine(); edited(ImGui::RadioButton(STR_C("as the original"), &tool.length_mode, 0)); @@ -909,8 +952,6 @@ void editor_mode::render_sweep_ui() 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()); @@ -953,3 +994,1045 @@ void editor_mode::draw_sweep_overlay() const if (started && projection.project(previous, screen)) drawlist->AddCircle(screen, 6.0f, IM_COL32(255, 160, 40, 230), 12, 2.0f); } + +namespace +{ + +double const kBendReach{60.0}; +double const kBendMargin{1.0}; + +glm::dmat3 axes_matrix(sweep_node const &Sweep) +{ + return glm::dmat3(Sweep.axes({1.0, 0.0, 0.0}), Sweep.axes({0.0, 1.0, 0.0}), Sweep.axes({0.0, 0.0, 1.0})); +} + +double along_anchor(sweep_node const &Sweep, int const Anchor, glm::dvec3 const &Low, glm::dvec3 const &High, glm::dvec3 const &Point) +{ + auto const &state{Sweep.definition()}; + switch (Anchor) + { + case 0: return Low.x; + case 2: return 0.5 * (Low.x + High.x); + case 3: return High.x; + case 4: return Sweep.axes(state.scale * Point).x; + default: return 0.0; + } +} + +int copies_of(sweep_node const &Sweep, double const Own) +{ + auto const &state{Sweep.definition()}; + auto const stretch{Sweep.distance_at(Sweep.end()) - Sweep.distance_at(Sweep.start())}; + auto const length{state.step > 0.01 ? state.step : Own}; + if (length < 1e-3) + return 1; + return state.bend ? std::max(1, static_cast(std::round(stretch / length))) : static_cast(std::floor(stretch / length + 1e-6)) + 1; +} + +} // namespace + +bool editor_mode::bend_source_of_selection(bend_source &Source) const +{ + Source = bend_source{}; + if (auto *model = dynamic_cast(m_node)) + { + if (model->from_template() || false == scene::Layers.editable(model)) + return false; + std::string text; + model->export_as_text(text); + std::istringstream words(text); + std::string token; + double number; + while (words >> token && token != "model") + ; + words >> number >> number >> number >> number >> Source.file; + words >> std::ws; + if (words.peek() == '"') + { + words.get(); + std::getline(words, Source.skin, '"'); + } + else + words >> Source.skin; + if (Source.file.empty() || Source.file == "none") + return false; + auto const angles{glm::dvec3(model->Angles())}; + Source.transform = glm::translate(glm::dmat4(1.0), model->location()); + Source.transform = glm::rotate(Source.transform, glm::radians(angles.y), glm::dvec3{0.0, 1.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(angles.x), glm::dvec3{1.0, 0.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(angles.z), glm::dvec3{0.0, 0.0, 1.0}); + Source.scale = glm::dvec3(model->Scale()); + Source.layer = model->layer(); + Source.model = model; + return true; + } + if (m_instance == 0 || false == m_include.placement || false == m_include.issue.empty() || false == scene::Layers.removable(m_instance)) + return false; + auto info{m_include.info}; + editor_includes::suggest(m_include.target, info); + glm::dvec3 position{0.0}, rotation{0.0}; + Source.parameters = m_include.values; + for (std::size_t i = 0; i < Source.parameters.size(); ++i) + { + auto const &role{info.parameter(static_cast(i) + 1).role}; + if (role.rfind("pos.", 0) != 0 && role.rfind("rot.", 0) != 0) + continue; + auto const value{std::atof(Source.parameters[i].c_str())}; + auto const axis{role.back() == 'x' ? 0 : role.back() == 'y' ? 1 : 2}; + (role[0] == 'p' ? position : rotation)[axis] = value; + Source.parameters[i] = "0"; + } + auto const &included{scene::Layers.instance(m_instance)}; + Source.file = m_include.target; + Source.transform = glm::translate(glm::dmat4(1.0), included.context.offset + position); + Source.transform = glm::rotate(Source.transform, glm::radians(rotation.y), glm::dvec3{0.0, 1.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(rotation.x), glm::dvec3{1.0, 0.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(rotation.z), glm::dvec3{0.0, 0.0, 1.0}); + Source.layer = included.layer; + Source.instance = m_instance; + return true; +} + +void editor_mode::bend_find_tracks(glm::dvec3 const &Point, void const *Key) +{ + auto &tool{m_bend}; + if (Key == tool.tracks_for && glm::distance(Point, tool.tracks_at) < 1.0) + return; + tool.tracks_for = Key; + tool.tracks_at = Point; + tool.tracks.clear(); + tool.track = 0; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType == tt_Table || track->m_paths.empty()) + continue; + auto best{std::numeric_limits::max()}; + for (auto const &path : track->m_paths) + { + if (plan_distance(path.points[segment_data::point::start], Point) > 2000.0 + kBendReach) + continue; + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 2.0), 8, 400)}; + for (int k = 0; k <= count; ++k) + best = std::min(best, plan_distance(curve.point(static_cast(k) / count), Point)); + } + if (best <= kBendReach) + tool.tracks.emplace_back(track, best); + } + std::sort(tool.tracks.begin(), tool.tracks.end(), [](auto const &A, auto const &B) { return A.second < B.second; }); + if (tool.tracks.size() > 12) + tool.tracks.resize(12); +} + +bool editor_mode::bend_definition(bend_source const &Source, TTrack &Track, sweep_node::state &State, std::vector &Tracks) +{ + auto &tool{m_bend}; + State = sweep_node::state{}; + State.model = Source.file; + State.skin = Source.skin.empty() ? "none" : Source.skin; + State.parameters = Source.parameters; + State.scale = Source.scale; + State.face = false; + State.platform = State.widen = platform_model(Source.file); + State.side_anchor = tool.side_anchor; + State.height_anchor = tool.height_anchor; + State.point = tool.point; + auto const origin{glm::dvec3(Source.transform[3])}; + int path{0}; + { + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + bezier const curve{Track.m_paths[i]}; + for (int k = 0; k <= 64; ++k) + if (auto const distance{plan_distance(curve.point(k / 64.0), origin)}; distance < best) + { + best = distance; + path = i; + } + } + } + std::vector records{{&Track, path, true}}; + State.pieces = {Track.m_paths[path]}; + scene::node_data data; + sweep_node probe{data}; + auto const define = [&]() { probe.define(State); }; + define(); + double lateral; + auto const forward{flat(probe.frame_at(probe.project(origin, lateral)).forward)}; + glm::dmat3 const rotation{Source.transform}; + auto const ex{flat(rotation * glm::dvec3{1.0, 0.0, 0.0})}; + auto const ez{flat(rotation * glm::dvec3{0.0, 0.0, 1.0})}; + State.along_x = tool.axis == 2 || (tool.axis == 0 && std::abs(glm::dot(ex, forward)) > std::abs(glm::dot(ez, forward))); + auto const direction{State.along_x ? ex : ez}; + State.flip = (glm::dot(direction, forward) < 0.0) != tool.turned; + State.lateral = lateral != 0.0 ? lateral : 1e-3; + define(); + glm::dvec3 low, high, shift; + if (false == probe.bounds(low, high, shift)) + { + tool.status = STR("The model has no triangles to bend, or it can't be loaded"); + return false; + } + if (State.platform && probe.edge_far()) + { + State.flip = false == State.flip; + define(); + probe.bounds(low, high, shift); + } + auto const inverse{glm::inverse(axes_matrix(probe))}; + auto const world = [&](glm::dvec3 const &Local) { return glm::dvec3(Source.transform * glm::dvec4(inverse * Local, 1.0)); }; + auto const ends{std::array{world({low.x, 0.0, 0.0}), world({high.x, 0.0, 0.0})}}; + auto const beyond = [&](glm::dvec3 const &Point, bool const Atend) { + auto const at{probe.frame_at(Atend ? probe.length() : 0.0)}; + auto const along{glm::dot(glm::dvec3{Point.x - at.position.x, 0.0, Point.z - at.position.z}, flat(at.forward))}; + return Atend ? along > -kBendMargin : along < kBendMargin; + }; + auto const extend = [&](bool const Atend, glm::dvec3 const &Target) { + auto const &edge{Atend ? records.back() : records.front()}; + auto const &edgepiece{Atend ? State.pieces.back() : State.pieces.front()}; + auto const point{Atend ? edgepiece.points[segment_data::point::end] : edgepiece.points[segment_data::point::start]}; + std::optional best; + segment_data bestpiece; + auto bestdistance{std::numeric_limits::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(other->m_paths.size())) + continue; + if (std::any_of(records.begin(), records.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::max()}; + for (int k = 0; k <= 32; ++k) + distance = std::min(distance, plan_distance(curve.point(k / 32.0), Target)); + if (distance < bestdistance) + { + bestdistance = distance; + best = sweep_tool::piece{other, end.path, forward}; + bestpiece = piece; + } + } + if (false == best.has_value()) + return false; + if (Atend) + { + records.push_back(*best); + State.pieces.push_back(bestpiece); + } + else + { + records.insert(records.begin(), *best); + State.pieces.insert(State.pieces.begin(), bestpiece); + } + define(); + return true; + }; + for (int i = 0; i < 40 && probe.length() < kSweepRouteReach; ++i) + { + auto grown{false}; + for (auto const &point : ends) + { + if (beyond(point, false) && extend(false, point)) + grown = true; + else if (beyond(point, true) && extend(true, point)) + grown = true; + } + if (false == grown) + break; + } + glm::dvec3 anchor{0.0}; + double station{0.0}; + auto const place = [&]() { + for (int pass = 0; pass < 3; ++pass) + { + define(); + probe.bounds(low, high, shift); + auto const a{along_anchor(probe, tool.along_anchor, low, high, State.point)}; + anchor = world({a, -shift.y, -shift.z}); + double side; + station = probe.project(anchor, side); + for (int k = 0; k < 2; ++k) + { + auto const at{probe.frame_at(station)}; + station += glm::dot(glm::dvec3{anchor.x - at.position.x, 0.0, anchor.z - at.position.z}, flat(at.forward)); + } + auto const at{probe.frame_at(station)}; + side = glm::dot(glm::dvec3{anchor.x - at.position.x, 0.0, anchor.z - at.position.z}, flat(at.left)); + auto const flipped{(side >= 0.0) != (State.lateral >= 0.0)}; + State.lateral = side != 0.0 ? side : 1e-3; + State.height = anchor.y - at.position.y - probe.rise(at); + if (false == flipped) + break; + } + define(); + probe.bounds(low, high, shift); + auto const a{along_anchor(probe, tool.along_anchor, low, high, State.point)}; + auto const first{probe.distance_at(station) - (a - low.x)}; + State.from = probe.station_at(first); + State.to = probe.station_at(first + high.x - low.x); + }; + place(); + // the straight model leaves the track where it curves on, short of the length it takes bent along it + for (int i = 0; i < 40 && probe.length() < kSweepRouteReach; ++i) + { + auto const ahead = [&](bool const Atend) { + auto const at{probe.frame_at(Atend ? probe.length() : 0.0)}; + auto const reach{Atend ? State.to - probe.length() : State.from}; + return at.position + flat(at.forward) * reach; + }; + if ((State.to > probe.length() + 0.01 && extend(true, ahead(true))) || (State.from < -0.01 && extend(false, ahead(false)))) + place(); + else + break; + } + if (State.platform) + { + platform_edge(State, Source.file, kRailTop); + define(); + anchor = bend_marker(probe); + } + if (State.to <= 0.0) + { + tool.status = STR("The model lies before the start of the track"); + return false; + } + tool.marker = anchor; + tool.marker_valid = true; + Tracks.clear(); + for (auto const &record : records) + Tracks.push_back(record.track); + auto const residual{glm::degrees(std::acos(std::clamp(std::abs(glm::dot(direction, forward)), 0.0, 1.0)))}; + tool.status = residual > 1.0 ? format(STR_C("The model stands %.1f deg off the track, bent it'll follow the track"), residual) : std::string{}; + return true; +} + +void editor_mode::bend_selection() +{ + auto &tool{m_bend}; + bend_source source; + if (false == bend_source_of_selection(source) || tool.tracks.empty()) + return; + auto *track{tool.tracks[std::clamp(tool.track, 0, static_cast(tool.tracks.size()) - 1)].first}; + sweep_node::state state; + std::vector tracks; + if (false == bend_definition(source, *track, state, tracks)) + return; + static int counter{0}; + scene::node_data data; + data.type = "sweep"; + data.range_max = -1.0; + data.layer = scene::Layers.accepts(source.layer) ? source.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(state); + sweep->mark_dirty(); + simulation::Sweeps.insert(sweep); + scene::Layers.count(sweep->layer(), scene::layer_item::model); + sweep->show(); + for (auto *path : tracks) + if (path != nullptr && false == path->m_editorremoved) + sweeps_captured(*path); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Bend; + snap.sweeps_toggled.push_back(sweep); + snap.layer = source.layer; + if (source.model != nullptr) + { + auto *model{source.model}; + model->export_as_text(snap.serialized); + snap.node_name = model->name(); + snap.position = model->location(); + snap.rotation = model->Angles(); + snap.scale = model->Scale(); + snap.uuid = model->uuid; + nullify_history_pointers(model); + remove_from_hierarchy(model); + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + simulation::State.delete_model(model); + } + else + { + snap.instance = source.instance; + snap.node_name = source.file; + scene::Layers.removed(source.instance, true); + select_include(0); + } + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + tool.edited = sweep; + tool.signature.clear(); + tool.status = format(STR_C("%s bent along %.1f m of the track, Ctrl+Z takes it back"), source.file.c_str(), sweep->distance_at(sweep->end()) - sweep->distance_at(sweep->start())); + WriteLog("Editor: " + tool.status, logtype::generic); +} + +void editor_mode::restore_bend(EditorSnapshot &Snapshot, bool const Undo) +{ + if (Snapshot.sweeps_toggled.empty()) + return; + auto *sweep{Snapshot.sweeps_toggled.front()}; + sweep->m_editorremoved = Undo; + if (Undo) + sweep->hide(); + else + sweep->show(); + sweep->mark_dirty(); + if (Snapshot.instance != 0) + { + if (scene::Layers.tracked(Snapshot.instance) && false == scene::Layers.instance(Snapshot.instance).dead) + scene::Layers.removed(Snapshot.instance, false == Undo); + select_include(0); + } + else if (Undo) + { + auto *created{simulation::State.create_model(Snapshot.serialized, Snapshot.node_name, Snapshot.position)}; + if (created != nullptr) + { + scene::Layers.move(created, Snapshot.layer); + created->location(Snapshot.position); + created->Angles(Snapshot.rotation); + created->Scale(Snapshot.scale); + created->uuid = Snapshot.uuid; + add_to_hierarchy(created); + Snapshot.node_ptr = created; + m_node = created; + ui()->set_node(m_node); + } + } + else if (auto *model{dynamic_cast(find_node_by_any(Snapshot.node_ptr, Snapshot.uuid.to_string(), Snapshot.node_name))}) + { + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + Snapshot.node_ptr = nullptr; + m_node = nullptr; + ui()->set_node(nullptr); + } + m_bend.edited = Undo ? nullptr : sweep; + m_bend.signature.clear(); +} + +void editor_mode::bend_edit(sweep_node::state const &State) +{ + auto *sweep{m_bend.edited}; + if (sweep == nullptr || sweep->m_editorremoved || 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(); +} + +double editor_mode::bend_fixed(sweep_node const &Sweep, glm::dvec3 const &Point) const +{ + auto const from{Sweep.distance_at(Sweep.start())}; + auto const stretch{Sweep.distance_at(Sweep.end()) - from}; + glm::dvec3 low, high, shift; + if (false == Sweep.bounds(low, high, shift)) + return from; + switch (m_bend.along_anchor) + { + case 0: return from; + case 2: return from + 0.5 * stretch; + case 3: return from + stretch; + default: break; + } + auto const own{high.x - low.x}; + auto const scale{Sweep.definition().bend && own > 1e-3 ? stretch / copies_of(Sweep, own) / own : 1.0}; + return from + (along_anchor(Sweep, m_bend.along_anchor, low, high, Point) - low.x) * scale; +} + +void editor_mode::bend_stretch(sweep_node const &Sweep, double const Fixed, double const Length, glm::dvec3 const &Point, sweep_node::state &State) const +{ + glm::dvec3 low, high, shift; + auto first{Fixed}; + switch (m_bend.along_anchor) + { + case 0: break; + case 2: first = Fixed - 0.5 * Length; break; + case 3: first = Fixed - Length; break; + default: + if (Sweep.bounds(low, high, shift)) + { + auto const own{high.x - low.x}; + auto const step{State.step > 0.01 ? State.step : own}; + auto const copies{State.bend && step > 1e-3 ? std::max(1, static_cast(std::round(Length / step))) : 1}; + auto const scale{State.bend && own > 1e-3 ? Length / copies / own : 1.0}; + first = Fixed - (along_anchor(Sweep, m_bend.along_anchor, low, high, Point) - low.x) * scale; + } + break; + } + State.from = Sweep.station_at(first); + State.to = Sweep.station_at(first + Length); +} + +void editor_mode::bend_reanchor(sweep_node::state &State, int const Side, int const Height, glm::dvec3 const &Point) const +{ + scene::node_data data; + sweep_node probe{data}; + probe.define(State); + glm::dvec3 low, high, before; + if (false == probe.bounds(low, high, before)) + return; + auto const stretch{probe.distance_at(probe.end()) - probe.distance_at(probe.start())}; + auto const fixed{probe.station_at(bend_fixed(probe, Point))}; + auto next{State}; + next.side_anchor = Side; + next.height_anchor = Height; + next.point = Point; + probe.define(next); + glm::dvec3 after; + if (false == probe.bounds(low, high, after)) + return; + next.lateral = State.lateral + before.y - after.y; + next.height = State.height + before.z - after.z; + probe.define(next); + auto const whole{State.to < 0.0 && State.from == 0.0}; + bend_stretch(probe, probe.distance_at(fixed), stretch, Point, next); + if (whole) + { + next.from = 0.0; + next.to = -1.0; + } + State = next; +} + +glm::dvec3 editor_mode::bend_marker(sweep_node const &Sweep) const +{ + glm::dvec3 low, high, shift; + if (false == Sweep.bounds(low, high, shift)) + return glm::dvec3{0.0}; + auto const &state{Sweep.definition()}; + auto const from{Sweep.distance_at(Sweep.start())}; + auto const stretch{Sweep.distance_at(Sweep.end()) - from}; + auto const own{high.x - low.x}; + auto const copies{copies_of(Sweep, own)}; + auto const scale{state.bend && own > 1e-3 ? stretch / copies / own : 1.0}; + auto const at{Sweep.frame_at(Sweep.station_at(bend_fixed(Sweep, state.point)))}; + return at.position + at.left * (state.lateral + Sweep.setback(at)) + glm::dvec3{0.0, state.height + Sweep.rise(at), 0.0}; +} + +void editor_mode::bend_pick(glm::dvec3 const &Point) +{ + auto &tool{m_bend}; + tool.picking = false; + if (auto *sweep{tool.edited}; sweep != nullptr && false == sweep->m_editorremoved) + { + glm::dvec3 low, high, shift; + if (false == sweep->bounds(low, high, shift)) + return; + auto const &state{sweep->definition()}; + double lateral; + auto const station{sweep->project(Point, lateral)}; + auto const at{sweep->frame_at(station)}; + auto const from{sweep->distance_at(sweep->start())}; + auto const stretch{sweep->distance_at(sweep->end()) - from}; + auto const own{high.x - low.x}; + auto const copies{copies_of(*sweep, own)}; + auto const copy{stretch / copies}; + auto const scale{state.bend && own > 1e-3 ? copy / own : 1.0}; + auto const along{std::fmod(std::max(0.0, sweep->distance_at(station) - from), copy)}; + glm::dvec3 const local{low.x + along / scale, lateral - state.lateral - sweep->setback(at) - shift.y, + Point.y - at.position.y - state.height - sweep->rise(at) - shift.z}; + auto const raw{glm::inverse(axes_matrix(*sweep)) * local / state.scale}; + tool.point = raw; + tool.along_anchor = tool.side_anchor = tool.height_anchor = 4; + auto next{state}; + bend_reanchor(next, 4, 4, raw); + bend_edit(next); + } + else + { + bend_source source; + if (false == bend_source_of_selection(source)) + return; + auto const local{glm::dvec3(glm::inverse(source.transform) * glm::dvec4(Point, 1.0))}; + tool.point = local / source.scale; + tool.along_anchor = tool.side_anchor = tool.height_anchor = 4; + tool.signature.clear(); + } + tool.status = format(STR_C("Reference point %.2f %.2f %.2f in the model"), tool.point.x, tool.point.y, tool.point.z); +} + +void editor_mode::render_bend() +{ + auto &tool{m_bend}; + if (tool.edited != nullptr && tool.edited->m_editorremoved) + tool.edited = nullptr; + ImGui::PushID("bend"); + char const *alongs[] = {STR_C("start"), STR_C("model origin"), STR_C("middle"), STR_C("end"), STR_C("picked point")}; + char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle"), STR_C("picked point")}; + char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track"), STR_C("picked point")}; + auto anchors = [&](bool &Changed) { + ImGui::SetNextItemWidth(170.0f); + Changed |= ImGui::Combo(STR_C("along the track"), &tool.along_anchor, alongs, IM_ARRAYSIZE(alongs)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Reference point: the model bends about it, the line through it keeps its length, the offsets are its place")); + ImGui::SetNextItemWidth(170.0f); + Changed |= ImGui::Combo(STR_C("across"), &tool.side_anchor, sides, IM_ARRAYSIZE(sides)); + ImGui::SetNextItemWidth(170.0f); + Changed |= ImGui::Combo(STR_C("up"), &tool.height_anchor, heights, IM_ARRAYSIZE(heights)); + if (tool.along_anchor == 4 || tool.side_anchor == 4 || tool.height_anchor == 4) + { + ImGui::SetNextItemWidth(220.0f); + glm::vec3 point{tool.point}; + if (ImGui::DragFloat3(STR_C("point (x, y, z of the model)"), &point.x, 0.01f, 0.0f, 0.0f, "%.3f")) + { + tool.point = glm::dvec3(point); + Changed = true; + } + } + if (ImGui::Button(tool.picking ? STR_C("Click the point on the model...") : STR_C("Pick the point on the model"))) + tool.picking = false == tool.picking; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The next click in the view sets the reference point where it lands on the model")); + }; + + if (auto *sweep{tool.edited}) + { + auto state{sweep->definition()}; + glm::dvec3 low, high, shift; + auto const measured{sweep->bounds(low, high, shift)}; + auto const own{measured ? high.x - low.x : 0.0}; + auto const from{sweep->distance_at(sweep->start())}; + auto const stretch{sweep->distance_at(sweep->end()) - from}; + ImGui::Text("%s %s", sweep->name().c_str(), state.model.c_str()); + if (own > 0.0) + ImGui::TextDisabled(STR_C("%.2f m along the reference line, %d copies (the model is %.2f m)"), stretch, copies_of(*sweep, own), own); + bool changed{false}; + tool.side_anchor = state.side_anchor; + tool.height_anchor = state.height_anchor; + tool.point = state.point; + auto const side{tool.side_anchor}, height{tool.height_anchor}; + auto const point{tool.point}; + bool anchored{false}; + anchors(anchored); + if (anchored && (side != tool.side_anchor || height != tool.height_anchor || point != tool.point)) + { + bend_reanchor(state, tool.side_anchor, tool.height_anchor, tool.point); + changed = true; + } + ImGui::PushItemWidth(110.0f); + changed |= ImGui::InputDouble(STR_C("from the track axis (m, + right)"), &state.lateral, 0.05, 0.5, "%.3f"); + double overhead{state.height - kRailTop}; + if (ImGui::InputDouble(STR_C("over the rail head (m)"), &overhead, 0.01, 0.1, "%.3f")) + { + state.height = overhead + kRailTop; + changed = true; + } + double length{stretch}; + if (ImGui::InputDouble(STR_C("length (m)"), &length, 0.5, 5.0, "%.2f") && length > 0.1) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), length, state.point, state); + changed = true; + } + ImGui::PopItemWidth(); + if (own > 0.0 && ImGui::SmallButton(STR_C("As long as the model"))) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), own, state.point, state); + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Along the whole track"))) + { + state.from = 0.0; + state.to = -1.0; + changed = true; + } + int mode{state.bend ? 0 : 1}; + changed |= ImGui::RadioButton(STR_C("Bent"), &mode, 0); + ImGui::SameLine(); + changed |= ImGui::RadioButton(STR_C("Repeated"), &mode, 1); + state.bend = mode == 0; + ImGui::SameLine(); + ImGui::SetNextItemWidth(80.0f); + changed |= ImGui::InputDouble(STR_C("copy every (m, 0: its length)"), &state.step, 0.0, 0.0, "%.2f"); + state.step = std::max(0.0, state.step); + changed |= ImGui::Checkbox(STR_C("Turned around"), &state.flip); + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("Mirrored"), &state.mirror); + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("Leans with the cant"), &state.tilt); + changed |= ImGui::Checkbox(STR_C("Set back by the widening of the structure gauge in the curves"), &state.widen); + if (ImGui::Checkbox(STR_C("Platform"), &state.platform)) + { + if (state.platform) + { + bend_reanchor(state, 1, 3, state.point); + platform_edge(state, state.model, kRailTop); + } + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts the edge facing the track 1.725 m from its axis, set back by the widening of the gauge in the curves.\n" + "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" + "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5")); + if (changed) + bend_edit(state); + if (ImGui::Button(STR_C("Done"))) + tool.edited = nullptr; + ImGui::SameLine(); + if (ImGui::Button(STR_C("Delete"))) + { + sweep_delete(*sweep); + tool.edited = nullptr; + } + } + else + { + bend_source source; + if (bend_source_of_selection(source)) + { + void const *key{source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))}; + bend_find_tracks(glm::dvec3(source.transform[3]), key); + ImGui::Text("%s", source.file.c_str()); + if (tool.tracks.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f), STR_C("No track within %.0f m of the model"), kBendReach); + else + { + tool.track = std::clamp(tool.track, 0, static_cast(tool.tracks.size()) - 1); + auto const label = [](std::pair const &Entry) { return format("%s (%.1f m)", Entry.first->name().empty() ? "(unnamed)" : Entry.first->name().c_str(), Entry.second); }; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::BeginCombo(STR_C("track"), label(tool.tracks[tool.track]).c_str())) + { + for (int i = 0; i < static_cast(tool.tracks.size()); ++i) + if (ImGui::Selectable(label(tool.tracks[i]).c_str(), i == tool.track)) + tool.track = i; + ImGui::EndCombo(); + } + char const *axes[] = {STR_C("the one nearest the track"), "z", "x"}; + ImGui::SetNextItemWidth(170.0f); + ImGui::Combo(STR_C("axis of the model along the track"), &tool.axis, axes, IM_ARRAYSIZE(axes)); + ImGui::Checkbox(STR_C("Turned around"), &tool.turned); + bool anchored{false}; + anchors(anchored); + auto const signature{format("%p %d %d %d %d %d %d %.4f %.4f %.4f", key, tool.track, tool.axis, tool.turned ? 1 : 0, tool.along_anchor, tool.side_anchor, tool.height_anchor, tool.point.x, tool.point.y, tool.point.z)}; + if (signature != tool.signature) + { + tool.signature = signature; + tool.marker_valid = false; + sweep_node::state state; + std::vector tracks; + bend_definition(source, *tool.tracks[tool.track].first, state, tracks); + } + if (ImGui::Button(STR_C("Bend it along the track (B)"))) + bend_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The model stays where it stands and bends with the track; Ctrl+Z gives it back")); + } + } + else + { + tool.marker_valid = false; + tool.picking = false; + ImGui::TextDisabled("%s", STR_C("Select a model by a track, or click a bent one")); + } + auto *track{selected_track()}; + auto const nearby{track != nullptr ? sweeps_near(*track) : std::vector{}}; + std::vector> listed; + if (track != nullptr) + for (auto *sweep : nearby) + listed.emplace_back(sweep, 0.0); + else + for (auto *sweep : simulation::Sweeps.sequence()) + if (sweep != nullptr && false == sweep->m_editorremoved) + { + auto const distance{glm::distance(sweep->location(), glm::dvec3{Global.pCamera.Pos})}; + if (distance < 0.5 * (sweep->end() - sweep->start()) + 300.0) + listed.emplace_back(sweep, distance); + } + if (false == listed.empty()) + { + ImGui::Separator(); + ImGui::TextDisabled("%s", track != nullptr ? STR_C("Bent models by this track:") : STR_C("Bent models nearby:")); + for (auto const &entry : listed) + { + auto *sweep{entry.first}; + ImGui::PushID(sweep); + if (ImGui::Selectable(format("%s %s, %.0f m", sweep->name().c_str(), sweep->definition().model.c_str(), sweep->end() - sweep->start()).c_str(), false)) + { + tool.edited = sweep; + tool.picking = false; + } + ImGui::PopID(); + } + } + if (track != nullptr && ImGui::TreeNodeEx(STR_C("New model along the selected track"))) + { + m_sweep.open = true; + render_sweep_ui(); + ImGui::TreePop(); + } + else + m_sweep.open = false; + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); + ImGui::PopID(); +} + +void editor_mode::draw_bend_overlay() const +{ + auto const &tool{m_bend}; + if (false == tool.label.empty()) + { + auto const &io{ImGui::GetIO()}; + auto *foreground{ImGui::GetForegroundDrawList()}; + auto const labelsize{ImGui::CalcTextSize(tool.label.c_str())}; + auto const detailsize{tool.details.empty() ? ImVec2(0.0f, 0.0f) : ImGui::CalcTextSize(tool.details.c_str())}; + ImVec2 const size{std::max(labelsize.x, detailsize.x), labelsize.y + (tool.details.empty() ? 0.0f : detailsize.y + 4.0f)}; + ImVec2 position{io.MousePos.x + 18.0f, io.MousePos.y + 18.0f}; + position.x = std::min(position.x, io.DisplaySize.x - size.x - 10.0f); + position.y = std::min(position.y, io.DisplaySize.y - size.y - 10.0f); + foreground->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 170), 4.0f); + foreground->AddText(position, IM_COL32(40, 220, 255, 255), tool.label.c_str()); + if (false == tool.details.empty()) + foreground->AddText(ImVec2(position.x, position.y + labelsize.y + 4.0f), IM_COL32(255, 255, 255, 235), tool.details.c_str()); + } + if (auto const *model{dynamic_cast(m_node)}; model != nullptr && tool.edited == nullptr && false == tool.tracks.empty() && tool.tracks_for == static_cast(model)) + { + screen_projection const projection; + ImVec2 at; + if (projection.project(model->location(), at)) + { + auto const text{format(STR_C("B: bend along %s"), tool.tracks.front().first->name().c_str())}; + auto *foreground{ImGui::GetForegroundDrawList()}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + foreground->AddRectFilled(ImVec2(at.x + 10.0f, at.y - size.y - 14.0f), ImVec2(at.x + size.x + 22.0f, at.y - 6.0f), IM_COL32(0, 0, 0, 150), 4.0f); + foreground->AddText(ImVec2(at.x + 16.0f, at.y - size.y - 10.0f), IM_COL32(40, 220, 255, 255), text.c_str()); + } + } + glm::dvec3 marker; + if (tool.edited != nullptr && false == tool.edited->m_editorremoved) + marker = bend_marker(*tool.edited); + else if (tool.marker_valid) + marker = tool.marker; + else + return; + screen_projection const projection; + ImVec2 screen; + if (false == projection.project(marker, screen)) + return; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + if (tool.over_marker || tool.dragging != 0) + drawlist->AddCircleFilled(screen, 9.0f, IM_COL32(40, 220, 255, 120), 16); + drawlist->AddCircle(screen, 9.0f, IM_COL32(40, 220, 255, 255), 16, 2.5f); + drawlist->AddLine(ImVec2(screen.x - 14.0f, screen.y), ImVec2(screen.x + 14.0f, screen.y), IM_COL32(40, 220, 255, 255), 2.0f); + drawlist->AddLine(ImVec2(screen.x, screen.y - 14.0f), ImVec2(screen.x, screen.y + 14.0f), IM_COL32(40, 220, 255, 255), 2.0f); +} + +bool editor_mode::bend_drag_start(int const Mods) +{ + auto &tool{m_bend}; + auto *sweep{tool.edited}; + if (sweep == nullptr || sweep->m_editorremoved || ImGui::GetIO().WantCaptureMouse) + return false; + auto const marker{bend_marker(*sweep)}; + screen_projection const projection; + ImVec2 screen; + if (false == projection.project(marker, screen)) + return false; + auto const &mouse{ImGui::GetIO().MousePos}; + if ((mouse.x - screen.x) * (mouse.x - screen.x) + (mouse.y - screen.y) * (mouse.y - screen.y) > 14.0f * 14.0f) + return false; + tool.dragging = (Mods & GLFW_MOD_SHIFT) != 0 ? 2 : (Mods & GLFW_MOD_CONTROL) != 0 ? 3 : 1; + tool.drag_state = sweep->definition(); + tool.drag_anchor = marker; + double lateral; + tool.drag_station = sweep->project(marker, lateral); + tool.drag_label.clear(); + return true; +} + +void editor_mode::bend_drag_update() +{ + auto &tool{m_bend}; + tool.label.clear(); + tool.details.clear(); + tool.hover = nullptr; + tool.over_marker = false; + auto const &io{ImGui::GetIO()}; + if (auto *model{dynamic_cast(m_node)}; model != nullptr && tool.edited == nullptr && false == model->from_template() && scene::Layers.editable(model)) + bend_find_tracks(model->location(), model); + if (tool.dragging == 0 && false == io.WantCaptureMouse) + { + if (tool.picking) + tool.label = STR("Click: the reference point, where it lands on the model"); + else + { + if (auto *sweep{tool.edited}; sweep != nullptr && false == sweep->m_editorremoved) + { + screen_projection const projection; + ImVec2 screen; + if (projection.project(bend_marker(*sweep), screen)) + tool.over_marker = (io.MousePos.x - screen.x) * (io.MousePos.x - screen.x) + (io.MousePos.y - screen.y) * (io.MousePos.y - screen.y) < 14.0f * 14.0f; + } + if (tool.over_marker) + { + tool.label = STR("Drag: offset from the track"); + tool.details = STR("Shift: height Ctrl: along the track"); + } + else if (ui()->mode() == nodebank_panel::MODIFY && m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) != GLFW_PRESS) + { + tool.hover = sweep_under(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); + if (tool.hover != nullptr && tool.hover != tool.edited) + { + tool.label = STR("Click: edit the bent model"); + tool.details = format("%s %s, %.1f m", tool.hover->name().c_str(), tool.hover->definition().model.c_str(), tool.hover->distance_at(tool.hover->end()) - tool.hover->distance_at(tool.hover->start())); + } + } + } + } + if (tool.dragging == 0) + return; + if (false == tool.drag_label.empty()) + tool.label = tool.drag_label; + auto *sweep{tool.edited}; + if (sweep == nullptr || sweep->m_editorremoved) + { + tool.dragging = 0; + return; + } + if (false == ImGui::GetIO().MouseDown[0]) + { + tool.dragging = 0; + tool.drag_label.clear(); + if (sweep->definition() == tool.drag_state) + return; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = sweep->name(); + snap.sweeps.emplace_back(sweep, tool.drag_state); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + sweep->mark_dirty(); + return; + } + screen_projection const projection; + auto const &mouse{ImGui::GetIO().MousePos}; + auto const step{0.01}; + auto const snap = [&](double const Value) { return std::round(Value / step) * step; }; + auto state{tool.drag_state}; + if (tool.dragging == 2) + { + glm::dvec3 origin, direction; + projection.ray(mouse, origin, direction); + glm::dvec3 normal{Global.pCamera.Pos.x - tool.drag_anchor.x, 0.0, Global.pCamera.Pos.z - tool.drag_anchor.z}; + if (glm::length(normal) < 1e-6) + return; + normal = glm::normalize(normal); + auto const facing{glm::dot(direction, normal)}; + if (std::abs(facing) < 1e-6) + return; + auto const hit{origin + direction * (glm::dot(tool.drag_anchor - origin, normal) / facing)}; + state.height = kRailTop + snap(tool.drag_state.height - kRailTop + hit.y - tool.drag_anchor.y); + tool.drag_label = format(STR_C("%.2f m over the rail head"), state.height - kRailTop); + } + else + { + glm::dvec3 point; + if (false == projection.on_level(mouse, tool.drag_anchor.y, point)) + return; + double lateral; + auto const station{sweep->project(point, lateral)}; + if (tool.dragging == 1) + { + auto const at{sweep->frame_at(station)}; + state.lateral = snap(lateral - sweep->setback(at)); + if (std::abs(state.lateral) < step) + state.lateral = std::copysign(step, tool.drag_state.lateral); + tool.drag_label = format(STR_C("%.2f m from the track axis"), std::abs(state.lateral)); + } + else + { + auto const shift{snap(station - tool.drag_station)}; + state.from = tool.drag_state.from + shift; + if (tool.drag_state.to >= 0.0) + state.to = tool.drag_state.to + shift; + tool.drag_label = format(STR_C("%+.2f m along the track"), shift); + } + } + if (false == (state == sweep->definition())) + sweep->define(state); +} + +sweep_node *editor_mode::sweep_under(glm::dvec3 const &Point) const +{ + sweep_node *best{nullptr}; + auto bestdistance{std::numeric_limits::max()}; + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->m_editorremoved) + continue; + auto const &state{sweep->definition()}; + if (plan_distance(Point, sweep->location()) > 0.5 * sweep->length() + std::abs(state.lateral) + 30.0) + continue; + double lateral; + auto const station{sweep->project(Point, lateral)}; + if (station < sweep->start() - 0.5 || station > sweep->end() + 0.5) + continue; + glm::dvec3 low, high, shift; + if (false == sweep->bounds(low, high, shift)) + continue; + auto const at{sweep->frame_at(station)}; + auto const across{lateral - state.lateral - sweep->setback(at) - shift.y}; + auto const up{Point.y - at.position.y - state.height - sweep->rise(at) - shift.z}; + if (across < low.y - 0.3 || across > high.y + 0.3 || up < low.z - 0.3 || up > high.z + 0.3) + continue; + auto const distance{std::abs(across - 0.5 * (low.y + high.y))}; + if (distance < bestdistance) + { + bestdistance = distance; + best = sweep; + } + } + return best; +} + +bool editor_mode::bend_click() +{ + auto &tool{m_bend}; + if (tool.hover == nullptr || tool.hover == tool.edited || tool.over_marker) + return false; + tool.edited = tool.hover; + tool.picking = false; + tool.signature.clear(); + m_node = nullptr; + m_dragging = false; + select_include(0); + ui()->set_node(nullptr); + ui()->expand_bend(); + return true; +} + +bool editor_mode::bend_shortcut() +{ + auto &tool{m_bend}; + bend_source source; + if (false == bend_source_of_selection(source)) + return false; + void const *key{source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))}; + bend_find_tracks(glm::dvec3(source.transform[3]), key); + if (tool.tracks.empty()) + { + ui()->set_status(format(STR_C("No track within %.0f m of the model"), kBendReach), true); + return true; + } + bend_selection(); + ui()->expand_bend(); + return true; +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 2e78c5ec..fe4db564 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -81,6 +81,99 @@ void item_tooltip(char const *Text) ImGui::SetTooltip("%s", STR_C(Text)); } +// the curve a track ends with at Joint, as it goes on beyond the joint, and a transition curve leading out of it to the straight +struct easing +{ + double curvature{0.0}; // positive turning from x towards z + double length{0.0}; + glm::dvec3 end{0.0}; + glm::dvec2 direction{0.0}; + std::vector pieces; +}; + +easing ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits) +{ + easing result; + for (auto const &path : Neighbour.m_paths) + for (auto const atend : {false, true}) + { + if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > 0.05) + continue; + if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) + return result; + bezier const curve{path}; + auto const first{curve.first(atend ? 1.0 : 0.0)}; + auto const second{curve.second(atend ? 1.0 : 0.0)}; + auto const speed{std::hypot(first.x, first.z)}; + if (speed < 1e-9) + return result; + auto curvature{(first.x * second.z - first.z * second.x) / (speed * speed * speed)}; + glm::dvec2 direction{first.x / speed, first.z / speed}; + double roll{path.rolls[1]}; + if (false == atend) + { + curvature = -curvature; + direction = -direction; + roll = -path.rolls[0]; + } + result.curvature = curvature; + if (std::abs(curvature) < 1e-4) + return result; + result.length = std::max(20.0, alignment::recommend(Speed, 1.0 / std::abs(curvature), Limits).transition); + auto const count{static_cast(std::ceil(result.length / 10.0))}; + auto const piece{result.length / count}; + glm::dvec3 point{Joint}; + for (int i = 0; i < count; ++i) + { + segment_data part; + part.points[segment_data::point::start] = point; + auto const from{direction}; + int const steps{static_cast(std::ceil(piece / 0.05))}; + auto const step{piece / steps}; + for (int j = 0; j < steps; ++j) + { + auto const along{i * piece + (j + 0.5) * step}; + auto const angle{curvature * (1.0 - along / result.length) * step}; + auto const half{turned(direction, angle * 0.5)}; + point += glm::dvec3{half.x * step, Grade * step, half.y * step}; + direction = turned(direction, angle); + } + part.points[segment_data::point::end] = point; + part.points[segment_data::point::control1] = glm::dvec3{from.x, Grade, from.y} * (piece / 3.0); + part.points[segment_data::point::control2] = -glm::dvec3{direction.x, Grade, direction.y} * (piece / 3.0); + part.rolls = {static_cast(roll * (1.0 - i * piece / result.length)), static_cast(roll * (1.0 - (i + 1) * piece / result.length))}; + result.pieces.push_back(part); + } + result.end = point; + result.direction = direction; + return result; + } + return result; +} + +segment_data turned_around(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; +} + +// curvature of the shape at Chainage, positive turning from x towards z +double curvature_at(alignment::result const &Shape, double const Chainage) +{ + auto const a{std::clamp(Chainage - 0.5, 0.0, Shape.length)}; + auto const b{std::clamp(Chainage + 0.5, 0.0, Shape.length)}; + if (b - a < 1e-3) + return 0.0; + auto const from{plan_of(alignment::evaluate(Shape, a).direction)}; + auto const to{plan_of(alignment::evaluate(Shape, b).direction)}; + return signed_angle(glm::normalize(from), glm::normalize(to)) / (b - a); +} + // circular curve from a point along a direction, cut into pieces of 90 degrees at most. Side: 1 to the left, -1 to the right void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex) { @@ -477,7 +570,7 @@ std::array const &editor_mode::track_modes() {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it"}, {"Straight", "G", track_tab::straights, "The whole straight through the selected path: drag its ends or the middle, break it, shift it"}, {"Curve", "C", track_tab::route, "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves"}, - {"Objects", "B", track_tab::lineside, "Along the track: platforms and other models, hectometre posts, fouling point markers, a parallel track, a vehicle to drive"}, + {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, a parallel track, a vehicle to drive"}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"}, {"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints"}, @@ -612,8 +705,6 @@ std::string editor_mode::track_mode_name() const case track_tab::speed: return STR_C("Speed check: LMB on a row shows the path"); case track_tab::joints: return STR_C("Joints: LMB on a row shows the place"); case track_tab::lineside: - if (m_lineside_tab == 0) - return m_sweep.dragging ? STR_C("Model along the track: release where it ends") : STR_C("Model along the track: LMB by the track where it starts, drag to the end"); return STR_C("Objects along the track"); case track_tab::infra: return STR_C("Infrastructure along the track"); case track_tab::turntable: @@ -3310,6 +3401,13 @@ void editor_mode::delete_selected_track() ui()->set_status(STR_C("The switch is replaced by its main track, Shift+Del removes all of it"), false); } push_track_snapshot({}, created, {track}); + // the model and the events of a turntable can't stay without its track + if (track->eType == tt_Table) + for (auto const instance : turntable_includes(*track)) + { + scene::Layers.removed(instance, true); + m_history.back().instances.push_back(instance); + } if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end()) m_route = {}; m_node = nullptr; @@ -3368,8 +3466,6 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"Drag a point", "in the profile window: changes the grade"}, {"Double click", "on the line: adds a point"}, {"Apply", "writes the heights into the tracks"}}; else if (m_track_window_open && m_track_tab == track_tab::speed) hints = {{"LMB on a row", "show the path"}}; - else if (m_track_window_open && m_track_tab == track_tab::lineside && m_lineside_tab == 0) - hints = {{"LMB by the track", "where it starts, the side of the track it goes on"}, {"Drag", "to where it ends, a click: one as long as the original"}, {"Release", "lays it"}, {"LMB away from the track", "selects another track"}}; else if (m_track_window_open && m_track_tab == track_tab::lineside) hints = {{"LMB on a track", "the line the objects go along"}}; else if (m_track_window_open && m_track_tab == track_tab::joints) @@ -4118,12 +4214,6 @@ std::vector editor_mode::lay_pieces(std::vector const Length = 0.0; if (Points.size() < 2) return {}; - alignment::design design; - design.norms = m_route.design.norms; - design.shape = m_route.design.shape; - design.speed = tool.style.velocity > 0.0 ? tool.style.velocity : 100.0; - design.start = Points.front(); - design.end = Points.back(); auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) @@ -4131,8 +4221,75 @@ std::vector editor_mode::lay_pieces(std::vector const Error = STR_C("Two points lie on one another"); return {}; } - design.start_direction = glm::normalize(startdirection); - design.end_direction = glm::normalize(enddirection); + alignment::design design; + auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)}; + if (false == shape.valid) + { + Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front(); + return {}; + } + Error = shape.warnings.empty() ? std::string{} : shape.warnings.front(); + auto const count = [&](alignment::result const &Shape, alignment::design const &Design) { + return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); + }; + // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them + auto const grade{(Points.back().y - Points.front().y) / std::max(1.0, plan_distance(Points.front(), Points.back()))}; + auto const eased = [&](editor_track::snap_target const &Joint, double const Chainage, double const Sign) { + if (Joint.track == nullptr || Joint.track->eType != tt_Normal) + return easing{}; + auto result{ease_out_of(*Joint.track, Joint.position, grade * Sign, design.speed, design.norms)}; + auto const own{Sign * curvature_at(shape, Chainage)}; + if (std::abs(own - result.curvature) <= 0.05 * std::abs(result.curvature)) + result.pieces.clear(); + return result; + }; + auto const in{eased(tool.start, 0.0, 1.0)}; + auto const out{eased(End, shape.length, -1.0)}; + if (false == in.pieces.empty() || false == out.pieces.empty()) + { + auto points{Points}; + auto starting{design.start_direction}; + auto ending{design.end_direction}; + if (false == in.pieces.empty()) + { + points.front() = in.end; + starting = in.direction; + } + if (false == out.pieces.empty()) + { + points.back() = out.end; + ending = -out.direction; + } + alignment::design easeddesign; + auto const easedshape{lay_shape(points, starting, ending, easeddesign)}; + if (easedshape.valid) + { + auto result{in.pieces}; + auto const middle{alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign))}; + result.insert(result.end(), middle.begin(), middle.end()); + for (auto piece{out.pieces.rbegin()}; piece != out.pieces.rend(); ++piece) + result.push_back(turned_around(*piece)); + Length = easedshape.length + (in.pieces.empty() ? 0.0 : in.length) + (out.pieces.empty() ? 0.0 : out.length); + Error = easedshape.warnings.empty() ? std::string{} : easedshape.warnings.front(); + return result; + } + Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front()); + } + Length = shape.length; + return alignment::pieces(shape, design, count(shape, design)); +} + +alignment::result editor_mode::lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &design) const +{ + auto const &tool{m_lay}; + design = {}; + design.norms = m_route.design.norms; + design.shape = m_route.design.shape; + design.speed = tool.style.velocity > 0.0 ? tool.style.velocity : 100.0; + design.start = Points.front(); + design.end = Points.back(); + design.start_direction = Startdirection; + design.end_direction = Enddirection; alignment::vertex corner; corner.radius = std::max(1.0, tool.radius); @@ -4171,16 +4328,7 @@ std::vector editor_mode::lay_pieces(std::vector const design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); } } - auto const shape{alignment::compute(design)}; - if (false == shape.valid) - { - Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front(); - return {}; - } - Error = shape.warnings.empty() ? std::string{} : shape.warnings.front(); - Length = shape.length; - auto const count{std::max(alignment::minimum_pieces(shape, design), static_cast(std::ceil(shape.length / std::max(5.0, tool.piece_length))))}; - return alignment::pieces(shape, design, count); + return alignment::compute(design); } void editor_mode::lay_click() @@ -4232,6 +4380,23 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) tool.status = format(STR_C("Laid %zu paths, %.1f m"), created.size(), length); if (false == error.empty()) tool.status += "\n" + error; + { + std::vector> run; + if (tool.start.track != nullptr && tool.start.track->eType == tt_Normal) + run.emplace_back(tool.start.track, tool.start.point.kind == editor_track::point_kind::end); + for (auto *track : created) + run.emplace_back(track, true); + if (End.track != nullptr && End.track->eType == tt_Normal) + run.emplace_back(End.track, End.point.kind == editor_track::point_kind::start); + editor_track::curve curve; + if (run.size() > created.size() && editor_track::analyse_curve(run, m_route.design.norms.gauge, curve) && curve.compound) + for (std::size_t i = 1; i < curve.arcs.size(); ++i) + if (curve.arcs[i].transition < 1.0) + { + tool.status += "\n" + format(STR_C("Compound curve: R %.0f m meets R %.0f m with no transition curve between them"), curve.arcs[i - 1].radius, curve.arcs[i].radius); + break; + } + } if (scene::Layers.active() == null_handle && style == nullptr) tool.status += "\nThe scenery isn't opened for editing, the new track won't be saved"; WriteLog("Editor: " + tool.status, logtype::generic); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index d7472c61..22140d33 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -232,8 +232,6 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) } if (m_track_tab == track_tab::turntable) return turntable_intent(ground, shift); - if (m_track_tab == track_tab::lineside && sweep_grabs(ground)) - return make(kind::sweep, "Press: where the model starts, drag to its end"); if (m_track_tab == track_tab::path) { if (shift && control && m_hover.track != nullptr && rail(*m_hover.track)) @@ -598,11 +596,6 @@ void editor_mode::render_track_context() show_track_tab(track_tab::lineside); m_parallel.expand = true; } - if (ImGui::MenuItem(STR_C("Model along the track"))) - { - show_track_tab(track_tab::lineside); - m_sweep.expand = true; - } if (ImGui::MenuItem(STR_C("Place a vehicle here"))) { show_track_tab(track_tab::lineside); @@ -1332,8 +1325,7 @@ void editor_mode::render_lineside_ui() bool *expand; std::function render; }; - std::array const tabs{{ - {"Platforms and models", &m_sweep.open, &m_sweep.expand, [this]() { render_sweep_ui(); }}, + std::array const tabs{{ {"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }}, {"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }}, {"Parallel track", &m_parallel.open, &m_parallel.expand, [this]() { render_parallel_ui(); }}, diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp index 25cfb987..f92b1b15 100644 --- a/application/editormodeturntable.cpp +++ b/application/editormodeturntable.cpp @@ -27,7 +27,9 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include +#include #include #include #include @@ -536,6 +538,26 @@ void editor_mode::turntable_find_fits() tool.status = tool.fits.empty() ? format(STR_C("No free ends within %.0f m of the rim"), tool.reach) : std::string{}; } +std::vector editor_mode::turntable_includes(TTrack const &Table) +{ + scan_turntables(); + std::vector 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 kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; + std::string target; + std::vector values; + if (kind != m_turntable.templates.end() && editor_includes::parse_directive(scene::Layers.directive(handle), target, values) && kind->track <= static_cast(values.size()) && ToLower(values[kind->track - 1]) == Table.name()) + result.push_back(handle); + } + return result; +} + void editor_mode::turntable_build(std::vector const &Fits) { auto &tool{m_turntable}; @@ -1023,3 +1045,122 @@ void editor_mode::draw_turntable_overlay() const label(projection, cursor, format("%.1f deg", yaw)); } } + +bool editor_mode::turntable_controls(std::vector &Rootstatements) +{ + if (scene::Layers.empty()) + return false; + std::string root{scene::Layers.layer(scene::layer_handle{1}).name}; + if (auto const slash{root.find_last_of("/\\")}; slash != std::string::npos) + root.erase(0, slash + 1); + if (auto const dot{root.rfind('.')}; dot != std::string::npos) + root.erase(dot); + if (root.empty()) + return false; + auto const file{root + "_obrotnice.scm"}; + std::ostringstream text; + auto tables{0}; + for (auto const *table : simulation::Paths.sequence()) + { + if (table == nullptr || table->eType != tt_Table || table->m_editorremoved || table->m_paths.empty() || table->name().empty()) + continue; + auto const &name{table->name()}; + std::vector slots; + std::string events; + scan_turntables(); + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count() && slots.empty(); ++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 kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; + if (kind == m_turntable.templates.end() || kind->angles == 0) + continue; + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || kind->track > static_cast(values.size()) || ToLower(values[kind->track - 1]) != name) + continue; + { + std::ifstream input{Global.asCurrentSceneryPath + kind->file, std::ios_base::binary}; + events.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + events = ToLower(events); + std::replace_if(events.begin(), events.end(), [](char const Character) { return Character == '\t' || Character == '\r' || Character == '\n'; }, ' '); + auto const parameter{"(p" + std::to_string(kind->track) + ")"}; + for (auto at{events.find(parameter)}; at != std::string::npos; at = events.find(parameter, at + name.size())) + events.replace(at, parameter.size(), name); + } + std::vector seen; + for (int i = 0; i < kind->positions && kind->angles - 1 + i < static_cast(values.size()); ++i) + if (double angle; numeric(values[kind->angles - 1 + i], angle) && std::none_of(seen.begin(), seen.end(), [&](double const Other) { return std::abs(circular(Other - angle)) < 0.01; })) + { + seen.push_back(angle); + slots.push_back(i + 1); + } + } + if (slots.empty()) + for (int position = 1; position <= 64; ++position) + slots.push_back(position); + std::vector positions; + for (auto const position : slots) + { + // the events of a turntable placed in this session get loaded only with the scenery loaded again + auto const event{name + "_tor" + std::to_string(position)}; + if (events.find(" " + event + " ") != std::string::npos || simulation::Events.FindEvent(event) != nullptr) + positions.push_back(position); + } + if (positions.size() < 2) + continue; + ++tables; + auto const count{positions.size()}; + auto const centre{centre_of(*table)}; + auto const at{format("%.3f %.3f %.3f", centre.x, centre.y, centre.z)}; + text << "node -1 0 " << name << "_keylock memcell " << at << " none 0 0 none endmemcell\r\n"; + text << "event " << name << "_lock updatevalues 0 " << name << "_keylock * 1 * endevent\r\n"; + text << "event " << name << "_unlock updatevalues 1.5 " << name << "_keylock * 0 * endevent\r\n"; + for (auto const forward : {true, false}) + { + auto const step{std::string{forward ? "_next" : "_prev"}}; + text << "event " << name << step << " multiple 0 " << name << "_keylock " << name << "_lock " << name << "_unlock " << name << step << '0'; + for (auto const position : positions) + text << ' ' << name << step << position; + text << " condition memcompare * 0 * endevent\r\n"; + // the turntable starts with no position set, the first step takes it to the first one + text << "event " << name << step << "0 multiple 0 " << name << ' ' << name << "_tor" << positions.front() << " condition memcompare * 0 * endevent\r\n"; + for (std::size_t i = 0; i < count; ++i) + { + auto const target{positions[forward ? (i + 1) % count : (i + count - 1) % count]}; + text << "event " << name << step << positions[i] << " multiple 0 " << name << ' ' << name << "_tor" << target << " condition memcompare * " << positions[i] << " * endevent\r\n"; + } + } + text << "node -1 0 " << name << "_key eventlauncher " << at << " 150 t 0 " << name << "_next " << name << "_prev end\r\n"; + } + auto const path{Global.asCurrentSceneryPath + file}; + std::error_code error; + auto const exists{std::filesystem::exists(path, error)}; + if (tables == 0 && false == exists) + return false; + auto const content{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one\r\n" + text.str()}; + std::string current; + { + std::ifstream input{path, std::ios_base::binary}; + current.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + } + if (current != content) + { + std::ofstream output{path, std::ios_base::binary | std::ios_base::trunc}; + output << content; + } + auto included{m_turntable_included}; + for (std::size_t handle = 1; handle <= scene::Layers.size() && false == included; ++handle) + { + auto name{scene::Layers.layer(static_cast(handle)).name}; + std::replace(name.begin(), name.end(), '\\', '/'); + included = ToLower(name) == ToLower(file) || ToLower(name).ends_with("/" + ToLower(file)); + } + if (included) + return false; + Rootstatements.push_back("include " + file + " end"); + return true; +} diff --git a/application/editormodevehicle.cpp b/application/editormodevehicle.cpp index f2c39b1f..4abc7ce2 100644 --- a/application/editormodevehicle.cpp +++ b/application/editormodevehicle.cpp @@ -183,10 +183,15 @@ 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 (selected != nullptr && selected == tool.track && m_hover.track == selected && ImGui::IsMouseClicked(0) && false == ImGui::GetIO().WantCaptureMouse) + vehicle_start(*selected, m_hover.point); + else if (selected != nullptr && selected != tool.track) + vehicle_start(*selected, m_hover.track == selected ? m_hover.point : bezier{selected->m_paths.front()}.point(0.5)); if (tool.track == nullptr) + { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: where the vehicle stands")); return; + } if (tool.bank == nullptr) { tool.bank = std::make_shared(); diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index d744b994..4f7ede62 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -54,30 +54,30 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) switch (Mode) { case nodebank_panel::MODIFY: - ImGui::TextDisabled("LMB: select node F: focus End: drop to ground Del: delete"); + ImGui::TextDisabled(STR_C("LMB: select node F: focus End: drop to ground Del: delete")); break; case nodebank_panel::COPY: - ImGui::TextDisabled("LMB: copy the clicked model to the node bank"); + ImGui::TextDisabled(STR_C("LMB: copy the clicked model to the node bank")); break; case nodebank_panel::ADD: { - ImGui::TextDisabled("LMB: insert a model at the cursor"); - ImGui::Checkbox("Random model from set", &m_insertrandom); + ImGui::TextDisabled(STR_C("LMB: insert a model at the cursor")); + ImGui::Checkbox(STR_C("Random model from set"), &m_insertrandom); if (m_insertrandom) { - m_nodebankpanel.set_combo("Set##insert", m_insertset, nullptr); + m_nodebankpanel.set_combo(STR_C("Set##insert"), m_insertset, nullptr); auto const count{m_nodebankpanel.set_entries(m_insertset).size()}; - ImGui::TextDisabled("%zu templates in set%s", count, count == 0 ? ", the node bank selection is used" : ""); + ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : ""); } else { - ImGui::TextDisabled("Inserts the template selected in the node bank"); + ImGui::TextDisabled(STR_C("Inserts the template selected in the node bank")); } m_functionspanel.render_controls(); break; } case nodebank_panel::BRUSH: - ImGui::TextDisabled("Hold LMB: paint models along the cursor path"); + ImGui::TextDisabled(STR_C("Hold LMB: paint models along the cursor path")); m_brushobjects.render_options(m_nodebankpanel); m_functionspanel.render_controls(); break; @@ -92,22 +92,32 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) void editor_ui::render_header_sections() { - if (ImGui::CollapsingHeader("Gizmo", ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::CollapsingHeader(STR_C("Gizmo"), ImGuiTreeNodeFlags_DefaultOpen)) { if (m_gizmooptions) m_gizmooptions(); } - if (ImGui::CollapsingHeader("Node properties", ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::CollapsingHeader(STR_C("Node properties"), ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Indent(); m_itempropertiespanel.render_body(); ImGui::Unindent(); } - if (ImGui::CollapsingHeader("Array")) + if (ImGui::CollapsingHeader(STR_C("Array"))) { if (m_arrayoptions) m_arrayoptions(); } + if (m_bendexpand) + { + ImGui::SetNextItemOpen(true); + m_bendexpand = false; + } + if (ImGui::CollapsingHeader(STR_C("Bend along the track"))) + { + if (m_bendoptions) + m_bendoptions(); + } } void editor_ui::render_menu_contents() @@ -118,9 +128,13 @@ void editor_ui::render_menu_contents() 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")); + if (ImGui::MenuItem(STR_C("Open scenery...")) && m_openscenery) + m_openscenery(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The editor starts again with another scenery of the scenery folder")); // 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) + if (ImGui::MenuItem(STR_C("Save"), STR_C("Ctrl+S"), false, editsession) && m_save) { m_save(); } @@ -128,7 +142,7 @@ void editor_ui::render_menu_contents() { ImGui::SetTooltip("%s", STR_C("Start the simulator with -edit to save changes to the scenery files")); } - if (ImGui::MenuItem(STR_C("Export scenery dump"), "Ctrl+Shift+F11") && m_export) + if (ImGui::MenuItem(STR_C("Export scenery dump"), STR_C("Ctrl+Shift+F11")) && m_export) { m_export(); } @@ -174,6 +188,8 @@ void editor_ui::render_rotation_controls() void editor_ui::set_node(scene::basic_node *Node) { m_node = Node; + m_itempropertiespanel.update(m_node); + m_functionspanel.update(m_node); } void editor_ui::set_include(include_selection *Include) diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 9c0bcdf7..f390d932 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -71,9 +71,18 @@ class editor_ui : public ui_layer { m_arrayoptions = std::move(Renderer); } + void set_bend_options(std::function Renderer) + { + m_bendoptions = std::move(Renderer); + } + void expand_bend() + { + m_bendexpand = true; + } // actions of the file menu, performed by the editor mode void set_file_actions(std::function Save, std::function Export); void set_new_scenery(std::function New) { m_newscenery = std::move(New); } + void set_open_scenery(std::function Open) { m_openscenery = std::move(Open); } // shows outcome of an operation in the layers window void set_status(std::string const &Status, bool const Error = false); @@ -91,6 +100,7 @@ class editor_ui : public ui_layer std::function m_save; std::function m_export; std::function m_newscenery; + std::function m_openscenery; brush_object_list m_brushobjects; bool m_insertrandom{false}; // insert mode picks a random template from m_insertset model_set_ref m_insertset; @@ -98,6 +108,8 @@ class editor_ui : public ui_layer std::function m_menuoptions; std::function m_gizmooptions; std::function m_arrayoptions; + std::function m_bendoptions; + bool m_bendexpand{false}; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any bool m_track{false}; }; diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 9aa6f145..adc28980 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "simulation/simulation.h" #include "editor/editorModelSets.hpp" +#include "editor/editorFormat.hpp" namespace { @@ -369,11 +370,11 @@ void itemproperties_panel::render_name_editor() m_namednode = m_node; std::snprintf(m_namebuffer, sizeof(m_namebuffer), "%s", picked->name().c_str()); } - auto const entered{ImGui::InputTextWithHint("name", "(none)", m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)}; + auto const entered{ImGui::InputTextWithHint(STR_C("name"), STR_C("(none)"), m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)}; m_nameactive = ImGui::IsItemActive(); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it"); + ImGui::SetTooltip(STR_C("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it")); } if (entered || ImGui::IsItemDeactivatedAfterEdit()) { @@ -420,7 +421,7 @@ void itemproperties_panel::render_transform_editor() if (typeid(*m_node) != typeid(TAnimModel)) { return; } auto *picked = static_cast(m_node); - if (false == ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) + if (false == ImGui::CollapsingHeader(STR_C("Transform"), ImGuiTreeNodeFlags_DefaultOpen)) { return; } @@ -442,7 +443,7 @@ void itemproperties_panel::render_transform_editor() clamp_circular(picked->Angles().x), clamp_circular(picked->Angles().y), clamp_circular(picked->Angles().z)}; - if (ImGui::DragFloat3("rotation (deg)", &angles.x, 0.5f, 0.0f, 360.0f, "%.3f")) + if (ImGui::DragFloat3(STR_C("rotation (deg)"), &angles.x, 0.5f, 0.0f, 360.0f, "%.3f")) { picked->Angles(angles); } @@ -451,15 +452,15 @@ void itemproperties_panel::render_transform_editor() // Scale — per-axis float, 1.000 display. Clamped to a reasonable positive range. { glm::vec3 scale = picked->Scale(); - if (ImGui::DragFloat3("scale (x,y,z)", &scale.x, 0.01f, 0.001f, 100.0f, "%.3f")) + if (ImGui::DragFloat3(STR_C("scale (x,y,z)"), &scale.x, 0.01f, 0.001f, 100.0f, "%.3f")) { picked->Scale(scale); } } - if (ImGui::Button("reset rotation")) { picked->Angles(glm::vec3(0.0f)); } + if (ImGui::Button(STR_C("reset rotation"))) { picked->Angles(glm::vec3(0.0f)); } ImGui::SameLine(); - if (ImGui::Button("reset scale")) { picked->Scale(glm::vec3(1.0f)); } + if (ImGui::Button(STR_C("reset scale"))) { picked->Scale(glm::vec3(1.0f)); } } bool itemproperties_panel::render_group() @@ -474,7 +475,7 @@ bool itemproperties_panel::render_group() return false; } - if (false == ImGui::CollapsingHeader("Parent Group")) + if (false == ImGui::CollapsingHeader(STR_C("Parent Group"))) { return false; } @@ -501,14 +502,14 @@ std::string *brush_object_list::GetRandomObject() void brush_object_list::render_options(nodebank_panel &Bank) { - ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m"); - ImGui::Checkbox("Random model from set", &useRandom); + ImGui::SliderFloat(STR_C("Spacing"), &spacing, 0.1f, 20.0f, "%.1f m"); + ImGui::Checkbox(STR_C("Random model from set"), &useRandom); if (false == useRandom) { - ImGui::TextDisabled("Paints the template selected in the node bank"); + ImGui::TextDisabled(STR_C("Paints the template selected in the node bank")); return; } - Bank.set_combo("Preset", source, "Manual list"); + Bank.set_combo(STR_C("Preset"), source, STR_C("Manual list")); if (source.kind == model_set_ref::source::manual) { Bank.manual_list("brushset", Objects, idx); @@ -516,7 +517,7 @@ void brush_object_list::render_options(nodebank_panel &Bank) else { auto const count{Bank.set_entries(source).size()}; - ImGui::TextDisabled("%zu templates in set%s", count, count == 0 ? ", the node bank selection is used" : ""); + ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : ""); } } @@ -608,7 +609,7 @@ void nodebank_panel::render() if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) { - if (ImGui::Button("Reload node bank")) + if (ImGui::Button(STR_C("Reload node bank"))) { nodebank_reload(); } @@ -624,7 +625,7 @@ void nodebank_panel::render() } // edit modes as tabs, each with its own settings - std::pair const modes[] = {{"Select", MODIFY}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}}; + std::pair const modes[] = {{STR_C("Select"), MODIFY}, {STR_C("Insert"), ADD}, {STR_C("Brush"), BRUSH}, {STR_C("Area fill"), FILL}, {STR_C("Copy to bank"), COPY}}; if (ImGui::BeginTabBar("##editmodes", ImGuiTabBarFlags_FittingPolicyResizeDown)) { for (auto const &tab : modes) @@ -646,7 +647,7 @@ void nodebank_panel::render() ImGui::Separator(); ImGui::PushItemWidth(-1); - ImGui::InputTextWithHint("Search", "Search node bank", m_nodesearch, IM_ARRAYSIZE(m_nodesearch)); + ImGui::InputTextWithHint(STR_C("Search"), STR_C("Search node bank"), m_nodesearch, IM_ARRAYSIZE(m_nodesearch)); // the list takes the rest of the window, but keeps a usable height when the sections above are expanded (the window scrolls then) auto const listheight{std::max(ImGui::GetContentRegionAvail().y, ImGui::GetTextLineHeightWithSpacing() * 10.0f)}; if (ImGui::ListBoxHeader("##nodebank", ImVec2(-1, listheight))) @@ -678,7 +679,7 @@ void nodebank_panel::render() } } // scenery templates. placing one adds an include directive to a scenery file, so they take scenery opened for editing - if (Global.editor_session && ImGui::CollapsingHeader("Scenery templates (.inc)")) + if (Global.editor_session && ImGui::CollapsingHeader(STR_C("Scenery templates (.inc)"))) { if (false == EditorIncludes.scanned()) { @@ -738,11 +739,11 @@ bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const changed = true; } } - ImGui::TextDisabled("User sets"); + ImGui::TextDisabled(STR_C("User sets")); auto const &sets{EditorModelSets.sets()}; if (sets.empty()) { - ImGui::TextDisabled(" (none yet, see Model sets...)"); + ImGui::TextDisabled(STR_C(" (none yet, see Model sets...)")); } for (auto const &set : sets) { @@ -753,7 +754,7 @@ bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const changed = true; } } - ImGui::TextDisabled("Node bank groups"); + ImGui::TextDisabled(STR_C("Node bank groups")); auto const groups{group_names()}; for (int idx = 0; idx < static_cast(groups.size()); ++idx) { @@ -781,33 +782,33 @@ void nodebank_panel::manual_list(char const *Id, std::vector &List, } if (List.empty()) { - ImGui::TextDisabled("(empty)"); + ImGui::TextDisabled(STR_C("(empty)")); } ImGui::ListBoxFooter(); } - if (ImGui::Button("Add selected")) + if (ImGui::Button(STR_C("Add selected"))) { if (node_selected()) List.push_back(*m_selectedtemplate); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Adds the template selected in the node bank list"); + ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list")); } ImGui::SameLine(); - if (ImGui::Button("Remove") && Selected >= 0 && Selected < static_cast(List.size())) + if (ImGui::Button(STR_C("Remove")) && Selected >= 0 && Selected < static_cast(List.size())) { List.erase(List.begin() + Selected); Selected = -1; } ImGui::SameLine(); - if (ImGui::Button("Clear")) + if (ImGui::Button(STR_C("Clear"))) { List.clear(); Selected = -1; } ImGui::SameLine(); - if (ImGui::Button("Save as set") && false == List.empty()) + if (ImGui::Button(STR_C("Save as set")) && false == List.empty()) { open_sets_window(EditorModelSets.create("New set", List)); } @@ -891,7 +892,7 @@ void nodebank_panel::open_sets_window(int const Setid) void nodebank_panel::render_sets_window() { ImGui::SetNextWindowSize(ImVec2S(600, 380), ImGuiCond_FirstUseEver); - if (false == ImGui::Begin("Model sets", &m_setsopen, ImGuiWindowFlags_NoCollapse)) + if (false == ImGui::Begin(STR_C("Model sets"), &m_setsopen, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return; @@ -919,10 +920,10 @@ void nodebank_panel::render_sets_window() } if (sets.empty()) { - ImGui::TextDisabled("No sets yet"); + ImGui::TextDisabled(STR_C("No sets yet")); } ImGui::EndChild(); - if (ImGui::Button("New")) + if (ImGui::Button(STR_C("New"))) { open_sets_window(EditorModelSets.create("New set")); } @@ -930,27 +931,27 @@ void nodebank_panel::render_sets_window() if (edited != nullptr) { ImGui::SameLine(); - if (ImGui::Button("Duplicate")) + if (ImGui::Button(STR_C("Duplicate"))) { open_sets_window(EditorModelSets.create(edited->name + " copy", edited->templates)); edited = EditorModelSets.find(m_setsselected); } ImGui::SameLine(); - if (ImGui::Button("Delete")) + if (ImGui::Button(STR_C("Delete"))) { ImGui::OpenPopup("##deleteset"); } if (ImGui::BeginPopup("##deleteset")) { - ImGui::Text("Delete set \"%s\"?", edited->name.c_str()); - if (ImGui::Button("Delete##confirm")) + ImGui::Text(STR_C("Delete set \"%s\"?"), edited->name.c_str()); + if (ImGui::Button(STR_C("Delete##confirm"))) { EditorModelSets.remove(edited->id); edited = nullptr; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel")) + if (ImGui::Button(STR_C("Cancel"))) { ImGui::CloseCurrentPopup(); } @@ -965,8 +966,8 @@ void nodebank_panel::render_sets_window() ImGui::BeginGroup(); if (edited == nullptr) { - ImGui::TextDisabled("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list."); - ImGui::TextDisabled("Sets can be chosen in Insert (random model), Brush and Area fill."); + ImGui::TextDisabled(STR_C("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list.")); + ImGui::TextDisabled(STR_C("Sets can be chosen in Insert (random model), Brush and Area fill.")); } else { @@ -980,7 +981,7 @@ void nodebank_panel::render_sets_window() auto const entered{ImGui::InputText("##setname", m_setsname, IM_ARRAYSIZE(m_setsname), ImGuiInputTextFlags_EnterReturnsTrue)}; ImGui::PopItemWidth(); ImGui::SameLine(); - if (ImGui::Button("Rename") || entered) + if (ImGui::Button(STR_C("Rename")) || entered) { EditorModelSets.rename(edited->id, m_setsname); m_setsnameid = 0; // reload, the name could have been adjusted to stay unique @@ -995,42 +996,42 @@ void nodebank_panel::render_sets_window() } if (edited->templates.empty()) { - ImGui::TextDisabled("(empty) select a template in the node bank and press \"Add selected\""); + ImGui::TextDisabled(STR_C("(empty) select a template in the node bank and press \"Add selected\"")); } ImGui::EndChild(); auto const setid{edited->id}; - if (ImGui::Button("Add selected")) + if (ImGui::Button(STR_C("Add selected"))) { if (node_selected()) EditorModelSets.add(setid, {*m_selectedtemplate}); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Adds the template selected in the node bank list"); + ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list")); } ImGui::SameLine(); - if (ImGui::Button("Remove") && m_setsentry >= 0) + if (ImGui::Button(STR_C("Remove")) && m_setsentry >= 0) { EditorModelSets.erase(setid, static_cast(m_setsentry)); m_setsentry = -1; } ImGui::SameLine(); - if (ImGui::Button("Clear")) + if (ImGui::Button(STR_C("Clear"))) { ImGui::OpenPopup("##clearset"); } if (ImGui::BeginPopup("##clearset")) { - ImGui::TextUnformatted("Remove all templates from this set?"); - if (ImGui::Button("Clear##confirm")) + ImGui::TextUnformatted(STR_C("Remove all templates from this set?")); + if (ImGui::Button(STR_C("Clear##confirm"))) { EditorModelSets.clear(setid); m_setsentry = -1; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel")) + if (ImGui::Button(STR_C("Cancel"))) { ImGui::CloseCurrentPopup(); } @@ -1053,7 +1054,7 @@ void nodebank_panel::render_sets_window() } ImGui::PopItemWidth(); ImGui::SameLine(); - if (ImGui::Button("Add group")) + if (ImGui::Button(STR_C("Add group"))) { std::vector templates; for (auto const *entry : set_entries(m_setsgroup)) @@ -1191,16 +1192,16 @@ void functions_panel::render() void functions_panel::render_controls() { // order matches rotation_mode - char const *const modes[] = {"Random", "Fixed", "Default (from template)"}; + char const *const modes[] = {STR_C("Random"), STR_C("Fixed"), STR_C("Default (from template)")}; auto current{static_cast(rot_mode)}; - if (ImGui::Combo("Rotation", ¤t, modes, IM_ARRAYSIZE(modes))) + if (ImGui::Combo(STR_C("Rotation"), ¤t, modes, IM_ARRAYSIZE(modes))) { rot_mode = static_cast(current); } if (rot_mode == FIXED) { - // ImGui::Checkbox("Get rotation from last object", &rot_from_last); - ImGui::SliderFloat("Rotation value", &rot_value, 0.0f, 360.0f, "%.1f deg"); + // ImGui::Checkbox(STR_C("Get rotation from last object"), &rot_from_last); + ImGui::SliderFloat(STR_C("Rotation value"), &rot_value, 0.0f, 360.0f, STR_C("%.1f deg")); } } @@ -1280,7 +1281,7 @@ void itemproperties_panel::render_include() auto &include{*m_include}; include.active = false; - ImGui::Text("include: %s", Bezogonkow(include.target).c_str()); + ImGui::Text(STR_C("include: %s"), Bezogonkow(include.target).c_str()); if (false == include.info.name.empty()) { ImGui::TextDisabled("%s", include.info.name.c_str()); @@ -1294,7 +1295,7 @@ void itemproperties_panel::render_include() } if (include.values.empty()) { - ImGui::TextDisabled("The template takes no parameters."); + ImGui::TextDisabled(STR_C("The template takes no parameters.")); return; } @@ -1339,7 +1340,7 @@ void itemproperties_panel::render_include() ImGui::PopItemWidth(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); - ImGui::TextWrapped("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again."); + ImGui::TextWrapped(STR_C("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again.")); ImGui::PopStyleColor(); } @@ -1365,7 +1366,7 @@ void layers_panel::render() if (scene::Layers.empty()) { // layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping - ImGui::TextDisabled("Scenery layers are available in an edit session.\nStart the simulator with: -edit "); + ImGui::TextDisabled(STR_C("Scenery layers are available in an edit session.\nStart the simulator with: -edit ")); } else { @@ -1375,32 +1376,32 @@ void layers_panel::render() } auto const selected{m_selected != null_handle}; auto const removed{selected && scene::Layers.layer(m_selected).removed}; - std::string reason{"Select a layer first"}; + std::string reason{STR("Select a layer first")}; - if (ImGui::Button("New...")) + if (ImGui::Button(STR_C("New..."))) { m_newname[0] = '\0'; m_newparent = (selected && false == removed && false == scene::Layers.layer(m_selected).binary ? m_selected : scene::layer_handle{1}); m_popuperror.clear(); - ImGui::OpenPopup("New layer"); + ImGui::OpenPopup(STR_C("New layer")); } ImGui::SameLine(); if (removed) { - if (ImGui::Button("Restore")) + if (ImGui::Button(STR_C("Restore"))) { scene::Layers.restore(m_selected); - report("Layer \"" + layer_name(m_selected) + "\" restored"); + report(format(STR_C("Layer \"%s\" restored"), layer_name(m_selected).c_str())); } } - else if (action_button("Remove...", selected && scene::Layers.can_remove(m_selected, reason), reason)) + else if (action_button(STR_C("Remove..."), selected && scene::Layers.can_remove(m_selected, reason), reason)) { - ImGui::OpenPopup("Remove layer"); + ImGui::OpenPopup(STR_C("Remove layer")); } ImGui::SameLine(); // the scenario file is what holds the scenery together, it stays where it is auto const mergeable{selected && false == removed && (scene::Layers.layer(m_selected).created || false == scene::Layers.layer(m_selected).sites.empty())}; - if (action_button("Merge into...", mergeable, selected && false == removed ? "The scenario file can't be merged into another layer" : "Select a layer first")) + if (action_button(STR_C("Merge into..."), mergeable, selected && false == removed ? STR("The scenario file can't be merged into another layer") : STR("Select a layer first"))) { // the layer which includes the selected one is the most likely target m_mergetarget = scene::Layers.resolve(scene::Layers.layer(m_selected).parent); @@ -1408,16 +1409,16 @@ void layers_panel::render() { m_mergetarget = null_handle; } - ImGui::OpenPopup("Merge layer"); + ImGui::OpenPopup(STR_C("Merge layer")); } ImGui::SameLine(); - if (ImGui::Button("Save") && save) + if (ImGui::Button(STR_C("Save")) && save) { save(); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Writes changed nodes and layers to the scenery files (Ctrl+S)"); + ImGui::SetTooltip(STR_C("Writes changed nodes and layers to the scenery files (Ctrl+S)")); } ImGui::Separator(); @@ -1447,7 +1448,7 @@ void layers_panel::render_list() { // the list takes the window except for the room left for the status ImGui::BeginChild("##layers", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing() * 2.5f)); - ImGui::TextDisabled("visible, locked, active, layer file"); + ImGui::TextDisabled(STR_C("visible, locked, active, layer file")); for (auto const handle : scene::Layers.tree()) { @@ -1489,7 +1490,7 @@ void layers_panel::render_list() { if (std::string reason; false == scene::Layers.accepts(handle, &reason)) { - report("Layer \"" + layer_name(handle) + "\" can't take new nodes" + (reason.empty() ? "" : ": " + reason), true); + report(format(STR_C("Layer \"%s\" can't take new nodes"), layer_name(handle).c_str()) + (reason.empty() ? "" : ": " + reason), true); } else { @@ -1498,7 +1499,7 @@ void layers_panel::render_list() } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Active layer: nodes created in the editor are placed in it"); + ImGui::SetTooltip(STR_C("Active layer: nodes created in the editor are placed in it")); } ImGui::SameLine(); auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"}; @@ -1524,7 +1525,7 @@ void layers_panel::render_list() } if (ImGui::IsItemHovered()) { - std::string content{layer.created ? "New layer, its file is created on save" : isremoved ? "Dropped from the scenery on save" : "Scenery file"}; + std::string content{layer.created ? STR("New layer, its file is created on save") : isremoved ? STR("Dropped from the scenery on save") : STR("Scenery file")}; if (scene::Layers.valid(layer.parent)) { content += "\nincluded by: " + layer_name(scene::Layers.resolve(layer.parent)); @@ -1554,10 +1555,10 @@ void layers_panel::render_list() void layers_panel::render_popups() { - if (ImGui::BeginPopupModal("New layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + if (ImGui::BeginPopupModal(STR_C("New layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::TextUnformatted("File of the new layer, in the scenery directory:"); - ImGui::InputTextWithHint("##name", "name.scm", m_newname, IM_ARRAYSIZE(m_newname)); + ImGui::TextUnformatted(STR_C("File of the new layer, in the scenery directory:")); + ImGui::InputTextWithHint("##name", STR_C("name.scm"), m_newname, IM_ARRAYSIZE(m_newname)); if (ImGui::BeginCombo("Included by", layer_name(m_newparent).c_str())) { for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx) @@ -1578,25 +1579,25 @@ void layers_panel::render_popups() { ImGui::TextColored(status_errorcolor, "%s", m_popuperror.c_str()); } - if (ImGui::Button("Create")) + if (ImGui::Button(STR_C("Create"))) { auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)}; if (created != null_handle) { m_selected = created; - report("Layer \"" + layer_name(created) + "\" created and made active. Its file is written on save."); + report(format(STR_C("Layer \"%s\" created and made active. Its file is written on save."), layer_name(created).c_str())); ImGui::CloseCurrentPopup(); } } ImGui::SameLine(); - if (ImGui::Button("Cancel")) + if (ImGui::Button(STR_C("Cancel"))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } - if (ImGui::BeginPopupModal("Remove layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + if (ImGui::BeginPopupModal(STR_C("Remove layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { if (false == scene::Layers.listed(m_selected)) { @@ -1614,30 +1615,30 @@ void layers_panel::render_popups() ++included; } } - ImGui::Text("Remove layer \"%s\" from the scenery?", layer_name(m_selected).c_str()); + ImGui::Text(STR_C("Remove layer \"%s\" from the scenery?"), layer_name(m_selected).c_str()); ImGui::TextUnformatted(layer.created ? "The layer wasn't saved yet, so it leaves no file behind." : "Its include directive is erased on save. The file itself stays on the disk."); if (included > 0) { - ImGui::Text("%d layer(s) it includes are removed with it.", included); + ImGui::Text(STR_C("%d layer(s) it includes are removed with it."), included); } if (auto const items{layer_content(layer)}; false == items.empty()) { ImGui::Separator(); - ImGui::TextUnformatted("The layer holds:"); + ImGui::TextUnformatted(STR_C("The layer holds:")); ImGui::TextUnformatted(items.c_str()); - ImGui::TextDisabled("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells."); + ImGui::TextDisabled(STR_C("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells.")); } - if (ImGui::Button("Remove")) + if (ImGui::Button(STR_C("Remove"))) { auto const name{layer_name(m_selected)}; if (scene::Layers.remove(m_selected)) { - report("Layer \"" + name + "\" removed. It can be restored until the scenery is saved."); + report(format(STR_C("Layer \"%s\" removed. It can be restored until the scenery is saved."), name.c_str())); } ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel")) + if (ImGui::Button(STR_C("Cancel"))) { ImGui::CloseCurrentPopup(); } @@ -1645,7 +1646,7 @@ void layers_panel::render_popups() ImGui::EndPopup(); } - if (ImGui::BeginPopupModal("Merge layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + if (ImGui::BeginPopupModal(STR_C("Merge layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { if (false == scene::Layers.listed(m_selected)) { @@ -1653,7 +1654,7 @@ void layers_panel::render_popups() } else { - ImGui::Text("Move content of layer \"%s\" to:", layer_name(m_selected).c_str()); + ImGui::Text(STR_C("Move content of layer \"%s\" to:"), layer_name(m_selected).c_str()); std::string reason; auto targets{0}; if (ImGui::BeginCombo("##target", layer_name(m_mergetarget).c_str())) @@ -1673,27 +1674,27 @@ void layers_panel::render_popups() } if (targets == 0) { - ImGui::TextDisabled("no layer can take it"); + ImGui::TextDisabled(STR_C("no layer can take it")); } ImGui::EndCombo(); } - ImGui::TextDisabled("The text of the file is moved on save, the emptied file stays on the disk.\n" + ImGui::TextDisabled(STR_C("The text of the file is moved on save, the emptied file stays on the disk.\n" "Merged into the layer which includes it, the content stays where the include was.\n" - "Merged into any other layer it's placed at its end, which changes the load order."); + "Merged into any other layer it's placed at its end, which changes the load order.")); auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)}; - if (action_button("Merge", possible, m_mergetarget != null_handle ? reason : "Pick the target layer")) + if (action_button(STR_C("Merge"), possible, m_mergetarget != null_handle ? reason : STR("Pick the target layer"))) { auto const name{layer_name(m_selected)}; auto const target{m_mergetarget}; if (scene::Layers.merge(m_selected, target)) { - report("Layer \"" + name + "\" merged into \"" + layer_name(target) + "\". The files are changed on save."); + report(format(STR_C("Layer \"%s\" merged into \"%s\". The files are changed on save."), name.c_str(), layer_name(target).c_str())); m_selected = target; } ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel")) + if (ImGui::Button(STR_C("Cancel"))) { ImGui::CloseCurrentPopup(); } @@ -1740,24 +1741,24 @@ void includes_panel::render_list() auto const &entries{EditorIncludes.entries()}; ImGui::PushItemWidth(ImGui::GetFontSize() * 12.0f); - auto changed{ImGui::InputTextWithHint("##filter", "Search templates", m_filter, IM_ARRAYSIZE(m_filter))}; + auto changed{ImGui::InputTextWithHint("##filter", STR_C("Search templates"), m_filter, IM_ARRAYSIZE(m_filter))}; ImGui::PopItemWidth(); ImGui::SameLine(); - changed |= ImGui::Checkbox("Described", &m_describedonly); + changed |= ImGui::Checkbox(STR_C("Described"), &m_describedonly); if (false == scene::Layers.templates().empty()) { // known only for scenery opened for editing ImGui::SameLine(); - changed |= ImGui::Checkbox("Used by the scenery", &m_usedonly); + changed |= ImGui::Checkbox(STR_C("Used by the scenery"), &m_usedonly); } ImGui::SameLine(); - if (ImGui::Button("Rescan")) + if (ImGui::Button(STR_C("Rescan"))) { EditorIncludes.scan(); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Looks through the scenery directory again, for the templates added or changed outside of the editor"); + ImGui::SetTooltip(STR_C("Looks through the scenery directory again, for the templates added or changed outside of the editor")); } if (changed || m_listrevision != EditorIncludes.revision()) @@ -1782,7 +1783,7 @@ void includes_panel::render_list() m_listed.emplace_back(idx); } } - ImGui::TextDisabled("%d template(s) in the scenery directory, %d described, %d ready for the node bank", static_cast(entries.size()), m_described, + ImGui::TextDisabled(STR_C("%d template(s) in the scenery directory, %d described, %d ready for the node bank"), static_cast(entries.size()), m_described, static_cast(EditorIncludes.ready().size())); ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 9.0f), true); @@ -1805,7 +1806,7 @@ void includes_panel::render_list() } if (false == entry.complete && false == entry.issue.empty() && ImGui::IsItemHovered()) { - ImGui::SetTooltip("Not in the node bank: %s", Bezogonkow(entry.issue).c_str()); + ImGui::SetTooltip(STR_C("Not in the node bank: %s"), Bezogonkow(entry.issue).c_str()); } if (clicked) { @@ -1831,13 +1832,13 @@ void includes_panel::render_description() ImGui::Columns(4, "##parameters", false); ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x); ImGui::SetColumnWidth(1, ImGui::CalcTextSize("texture ").x + ImGui::GetFrameHeight() * 2.0f); - ImGui::TextDisabled("param"); + ImGui::TextDisabled(STR_C("param")); ImGui::NextColumn(); - ImGui::TextDisabled("role"); + ImGui::TextDisabled(STR_C("role")); ImGui::NextColumn(); - ImGui::TextDisabled("label"); + ImGui::TextDisabled(STR_C("label")); ImGui::NextColumn(); - ImGui::TextDisabled("default"); + ImGui::TextDisabled(STR_C("default")); ImGui::NextColumn(); for (auto ¶meter : m_info.parameters) { @@ -1883,35 +1884,35 @@ void includes_panel::render_description() ImGui::PushTextWrapPos(0.0f); if (std::string issue; editor_includes::complete(m_info, m_parameters, &issue)) { - ImGui::TextUnformatted("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)."); + ImGui::TextUnformatted(STR_C("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates).")); } else { - ImGui::TextColored(status_errorcolor, "Not for the node bank yet: %s.", issue.c_str()); + ImGui::TextColored(status_errorcolor, STR_C("Not for the node bank yet: %s."), issue.c_str()); } ImGui::PopTextWrapPos(); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\n" + ImGui::SetTooltip(STR_C("A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\n" "Insert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\n" - "The other parameters receive their default values."); + "The other parameters receive their default values.")); } - if (ImGui::Button("Suggest roles")) + if (ImGui::Button(STR_C("Suggest roles"))) { editor_includes::suggest(m_file, m_info); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are"); + ImGui::SetTooltip(STR_C("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are")); } ImGui::SameLine(); - if (ImGui::Button("Add parameter")) + if (ImGui::Button(STR_C("Add parameter"))) { m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1); } ImGui::SameLine(); - if (ImGui::Button("Save to file")) + if (ImGui::Button(STR_C("Save to file"))) { std::string error; m_statuserror = false == editor_includes::save(m_file, m_info, error); @@ -1922,7 +1923,7 @@ void includes_panel::render_description() } } ImGui::SameLine(); - if (ImGui::Button("Reload")) + if (ImGui::Button(STR_C("Reload"))) { open(m_file); } @@ -1944,11 +1945,11 @@ void includes_panel::render() if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); - ImGui::TextWrapped("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; " - "the description is stored in the template file, as //$e comment lines."); + ImGui::TextWrapped(STR_C("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; " + "the description is stored in the template file, as //$e comment lines.")); if (false == Global.editor_session) { - ImGui::TextWrapped("The node bank offers the templates in a scenery opened for editing (-edit)."); + ImGui::TextWrapped(STR_C("The node bank offers the templates in a scenery opened for editing (-edit).")); } ImGui::PopStyleColor(); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 6f4ad28b..1fb3fa0b 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -1320,6 +1320,12 @@ msgstr "Kliknij co najmniej dwa punkty" msgid "Laid %zu paths, %.1f m" msgstr "Ułożono %zu odcinków, %.1f m" +msgid "The curvature changes abruptly where the track joins, there's no room for a transition curve" +msgstr "Na styku z torem krzywizna zmienia się skokowo, nie ma miejsca na krzywą przejściową" + +msgid "Compound curve: R %.0f m meets R %.0f m with no transition curve between them" +msgstr "Łuk koszowy: R %.0f m styka się z R %.0f m bez krzywej przejściowej między nimi" + msgid "More settings" msgstr "Więcej ustawień" @@ -2662,8 +2668,8 @@ msgstr "Parzyste hektometry po prawej, nieparzyste po lewej" msgid "Objects" msgstr "Obiekty" -msgid "Along the track: platforms and other models, hectometre posts, fouling point markers, a parallel track, a vehicle to drive" -msgstr "Przy torze: perony i inne modele, słupki hektometrowe, wskaźniki ukresu, tor równoległy, pojazd do jazdy" +msgid "Along the track: hectometre posts, fouling point markers, a parallel track, a vehicle to drive" +msgstr "Przy torze: słupki hektometrowe, wskaźniki ukresu, tor równoległy, pojazd do jazdy" msgid "Model along the track: release where it ends" msgstr "Model wzdłuż toru: puść tam, gdzie się kończy" @@ -3458,3 +3464,930 @@ msgstr "Otwórz kreator" msgid "The editor couldn't be started again" msgstr "Nie udało się ponownie uruchomić edytora" + +# scenery editor: models bent along the track, opening another scenery + +msgid "Bend along the track" +msgstr "Gięcie wzdłuż toru" + +msgid "Bend it along the track" +msgstr "Wygnij wzdłuż toru" + +msgid "Select a model or an include standing by a track" +msgstr "Zaznacz model albo include stojący przy torze" + +msgid "No track within %.0f m of the model" +msgstr "Brak toru w promieniu %.0f m od modelu" + +msgid "axis of the model along the track" +msgstr "oś modelu wzdłuż toru" + +msgid "the one nearest the track" +msgstr "najbliższa kierunkowi toru" + +msgid "Reference point: the model bends about it, the line through it keeps its length, the offsets are its place" +msgstr "Punkt odniesienia: wokół niego model się wygina, linia przez niego zachowuje długość, odsunięcia podają jego położenie" + +msgid "along the track" +msgstr "wzdłuż toru" + +msgid "across" +msgstr "w poprzek" + +msgid "start" +msgstr "początek" + +msgid "end" +msgstr "koniec" + +msgid "picked point" +msgstr "wskazany punkt" + +msgid "point (x, y, z of the model)" +msgstr "punkt (x, y, z modelu)" + +msgid "Pick the point on the model" +msgstr "Wskaż punkt na modelu" + +msgid "Click the point on the model..." +msgstr "Kliknij punkt na modelu..." + +msgid "The next click in the view sets the reference point where it lands on the model" +msgstr "Następne kliknięcie w widoku ustawia punkt odniesienia tam, gdzie trafi w model" + +msgid "Reference point %.2f %.2f %.2f in the model" +msgstr "Punkt odniesienia %.2f %.2f %.2f w modelu" + +msgid "The model stays where it stands and bends with the track; Ctrl+Z gives it back" +msgstr "Model zostaje tam, gdzie stoi, i wygina się z torem; Ctrl+Z go przywraca" + +msgid "The model stands %.1f deg off the track, bent it'll follow the track" +msgstr "Model stoi %.1f° w stosunku do toru, po wygięciu pójdzie wzdłuż toru" + +msgid "The model has no triangles to bend, or it can't be loaded" +msgstr "Model nie ma trójkątów do wygięcia albo nie da się go wczytać" + +msgid "The model lies before the start of the track" +msgstr "Model leży przed początkiem toru" + +msgid "%s bent along %.1f m of the track, Ctrl+Z takes it back" +msgstr "%s wygięty na %.1f m toru, Ctrl+Z cofa" + +msgid "%.2f m along the reference line, %d copies (the model is %.2f m)" +msgstr "%.2f m wzdłuż linii odniesienia, kopii: %d (model ma %.2f m)" + +msgid "from the track axis (m, + right)" +msgstr "od osi toru (m, + w prawo)" + +msgid "over the rail head (m)" +msgstr "nad główką szyny (m)" + +msgid "length (m)" +msgstr "długość (m)" + +msgid "As long as the model" +msgstr "Długość modelu" + +msgid "Along the whole track" +msgstr "Na całym torze" + +msgid "Bent" +msgstr "Wygięty" + +msgid "copy every (m, 0: its length)" +msgstr "kopia co (m, 0: jej długość)" + +msgid "Done" +msgstr "Gotowe" + +msgid "Bent models by this track:" +msgstr "Wygięte modele przy tym torze:" + +msgid "Bent models nearby:" +msgstr "Wygięte modele w pobliżu:" + +msgid "New model along the selected track" +msgstr "Nowy model wzdłuż zaznaczonego toru" + +msgid "Open scenery..." +msgstr "Otwórz scenerię..." + +msgid "The editor starts again with another scenery of the scenery folder" +msgstr "Edytor uruchamia się ponownie z inną scenerią z folderu scenery" + +msgid "Open scenery##restart" +msgstr "Otwórz scenerię##restart" + +msgid "The editor starts again with the scenery chosen below." +msgstr "Edytor uruchomi się ponownie ze scenerią wybraną poniżej." + +msgid "search" +msgstr "szukaj" + +msgid "Save and open" +msgstr "Zapisz i otwórz" + +msgid "In the view: drag the blue cross sideways to set the offset, with Shift up and down, with Ctrl along the track" +msgstr "W widoku: przeciągnij niebieski celownik w bok, żeby ustawić odsunięcie, z Shift w górę i w dół, z Ctrl wzdłuż toru" + +msgid "Drag: offset from the track; Shift: height; Ctrl: along the track" +msgstr "Przeciągnij: odsunięcie od toru; Shift: wysokość; Ctrl: wzdłuż toru" + +msgid "%.2f m over the rail head" +msgstr "%.2f m nad główką szyny" + +msgid "%.2f m from the track axis" +msgstr "%.2f m od osi toru" + +msgid "%+.2f m along the track" +msgstr "%+.2f m wzdłuż toru" + +# scenery editor: general mode, toolset, layers, settings + +msgid "LMB: select node F: focus End: drop to ground Del: delete" +msgstr "LPM: zaznacz węzeł F: pokaż End: opuść na ziemię Del: usuń" + +msgid "LMB: copy the clicked model to the node bank" +msgstr "LPM: skopiuj kliknięty model do banku węzłów" + +msgid "LMB: insert a model at the cursor" +msgstr "LPM: wstaw model w miejscu kursora" + +msgid "Random model from set" +msgstr "Losowy model z zestawu" + +msgid "%zu templates in set%s" +msgstr "%zu szablonów w zestawie%s" + +msgid "Inserts the template selected in the node bank" +msgstr "Wstawia szablon zaznaczony w banku węzłów" + +msgid "Hold LMB: paint models along the cursor path" +msgstr "Przytrzymaj LPM: maluj modele wzdłuż drogi kursora" + +msgid "Node properties" +msgstr "Właściwości węzła" + +msgid "Array" +msgstr "Tablica" + +msgid "Ctrl+S" +msgstr "Ctrl+S" + +msgid "Ctrl+Shift+F11" +msgstr "Ctrl+Shift+F11" + +msgid "name" +msgstr "nazwa" + +msgid "(none)" +msgstr "(brak)" + +msgid "Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it" +msgstr "Nazwa, po której zdarzenia mogą odwołać się do węzła.\nWęzły bez nazwy wczytują się szybciej, zostaw puste, jeśli nic jej nie potrzebuje" + +msgid "Transform" +msgstr "Przekształcenie" + +msgid "rotation (deg)" +msgstr "obrót (°)" + +msgid "scale (x,y,z)" +msgstr "skala (x,y,z)" + +msgid "reset rotation" +msgstr "zeruj obrót" + +msgid "reset scale" +msgstr "zeruj skalę" + +msgid "Parent Group" +msgstr "Grupa nadrzędna" + +msgid "Spacing" +msgstr "Odstęp" + +msgid "Paints the template selected in the node bank" +msgstr "Maluje szablonem zaznaczonym w banku węzłów" + +msgid "Reload node bank" +msgstr "Wczytaj bank węzłów ponownie" + +msgid "Search" +msgstr "Szukaj" + +msgid "Search node bank" +msgstr "Szukaj w banku węzłów" + +msgid "Scenery templates (.inc)" +msgstr "Szablony scenerii (.inc)" + +msgid "User sets" +msgstr "Zestawy użytkownika" + +msgid " (none yet, see Model sets...)" +msgstr " (jeszcze żadnych, zobacz Zestawy modeli...)" + +msgid "Node bank groups" +msgstr "Grupy banku węzłów" + +msgid "(empty)" +msgstr "(pusto)" + +msgid "Add selected" +msgstr "Dodaj zaznaczony" + +msgid "Adds the template selected in the node bank list" +msgstr "Dodaje szablon zaznaczony na liście banku węzłów" + +msgid "Remove" +msgstr "Usuń" + +msgid "Save as set" +msgstr "Zapisz jako zestaw" + +msgid "Model sets" +msgstr "Zestawy modeli" + +msgid "No sets yet" +msgstr "Brak zestawów" + +msgid "New" +msgstr "Nowy" + +msgid "Duplicate" +msgstr "Powiel" + +msgid "Delete set \"%s\"?" +msgstr "Usunąć zestaw \"%s\"?" + +msgid "Delete##confirm" +msgstr "Usuń##confirm" + +msgid "Create a set with \"New\", or use \"Save as set\" on a brush or area fill list." +msgstr "Utwórz zestaw przyciskiem \"Nowy\" albo użyj \"Zapisz jako zestaw\" na liście pędzla lub wypełniania obszaru." + +msgid "Sets can be chosen in Insert (random model), Brush and Area fill." +msgstr "Zestawy można wybrać we Wstawianiu (losowy model), Pędzlu i Wypełnianiu obszaru." + +msgid "(empty) select a template in the node bank and press \"Add selected\"" +msgstr "(pusto) zaznacz szablon w banku węzłów i naciśnij \"Dodaj zaznaczony\"" + +msgid "Remove all templates from this set?" +msgstr "Usunąć wszystkie szablony z tego zestawu?" + +msgid "Clear##confirm" +msgstr "Wyczyść##confirm" + +msgid "Add group" +msgstr "Dodaj grupę" + +msgid "Rotation" +msgstr "Obrót" + +msgid "Get rotation from last object" +msgstr "Obrót z ostatniego obiektu" + +msgid "Rotation value" +msgstr "Wartość obrotu" + +msgid "%.1f deg" +msgstr "%.1f°" + +msgid "include: %s" +msgstr "include: %s" + +msgid "The template takes no parameters." +msgstr "Szablon nie ma parametrów." + +msgid "Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again." +msgstr "Modele szablonu od razu idą za zmianami, reszta po zapisaniu i ponownym wczytaniu scenerii." + +msgid "Scenery layers are available in an edit session.\nStart the simulator with: -edit " +msgstr "Warstwy scenerii są dostępne w sesji edycji.\nUruchom symulator z: -edit " + +msgid "New..." +msgstr "Nowa..." + +msgid "New layer" +msgstr "Nowa warstwa" + +msgid "Restore" +msgstr "Przywróć" + +msgid "Remove layer" +msgstr "Usuń warstwę" + +msgid "Merge layer" +msgstr "Scal warstwę" + +msgid "Save" +msgstr "Zapisz" + +msgid "Writes changed nodes and layers to the scenery files (Ctrl+S)" +msgstr "Zapisuje zmienione węzły i warstwy do plików scenerii (Ctrl+S)" + +msgid "visible, locked, active, layer file" +msgstr "widoczna, zablokowana, aktywna, plik warstwy" + +msgid "Active layer: nodes created in the editor are placed in it" +msgstr "Aktywna warstwa: tu trafiają węzły tworzone w edytorze" + +msgid "File of the new layer, in the scenery directory:" +msgstr "Plik nowej warstwy, w katalogu scenerii:" + +msgid "name.scm" +msgstr "nazwa.scm" + +msgid "Create" +msgstr "Utwórz" + +msgid "Remove layer \"%s\" from the scenery?" +msgstr "Usunąć warstwę \"%s\" ze scenerii?" + +msgid "%d layer(s) it includes are removed with it." +msgstr "Razem z nią zostaną usunięte warstwy, które dołącza: %d." + +msgid "The layer holds:" +msgstr "Warstwa zawiera:" + +msgid "Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells." +msgstr "Upewnij się, że reszta scenerii nie odwołuje się do jej torów, zdarzeń ani komórek pamięci." + +msgid "Move content of layer \"%s\" to:" +msgstr "Przenieś zawartość warstwy \"%s\" do:" + +msgid "no layer can take it" +msgstr "żadna warstwa nie może jej przyjąć" + +msgid "The text of the file is moved on save, the emptied file stays on the disk.\nMerged into the layer which includes it, the content stays where the include was.\nMerged into any other layer it's placed at its end, which changes the load order." +msgstr "Tekst pliku zostanie przeniesiony przy zapisie, opróżniony plik zostaje na dysku.\nScalona z warstwą, która ją dołącza, zawartość zostaje tam, gdzie był include.\nScalona z inną warstwą trafia na jej koniec, co zmienia kolejność wczytywania." + +msgid "Search templates" +msgstr "Szukaj szablonów" + +msgid "Described" +msgstr "Opisane" + +msgid "Used by the scenery" +msgstr "Użyte w scenerii" + +msgid "Rescan" +msgstr "Przeszukaj ponownie" + +msgid "Looks through the scenery directory again, for the templates added or changed outside of the editor" +msgstr "Przegląda katalog scenerii jeszcze raz, w poszukiwaniu szablonów dodanych lub zmienionych poza edytorem" + +msgid "%d template(s) in the scenery directory, %d described, %d ready for the node bank" +msgstr "Szablonów w katalogu scenerii: %d, opisanych: %d, gotowych do banku węzłów: %d" + +msgid "Not in the node bank: %s" +msgstr "Nie ma w banku węzłów: %s" + +msgid "param" +msgstr "param." + +msgid "role" +msgstr "rola" + +msgid "label" +msgstr "etykieta" + +msgid "default" +msgstr "domyślna" + +msgid "Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)." +msgstr "Kompletny: po zapisaniu szablon pojawi się w banku węzłów (zakładka Wstaw, Szablony scenerii)." + +msgid "Not for the node bank yet: %s." +msgstr "Jeszcze nie do banku węzłów: %s." + +msgid "A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\nInsert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\nThe other parameters receive their default values." +msgstr "Szablon wstawiony z banku węzłów dostaje położenie kursora w parametrach pos.*, obrót ustawiony w zakładce\nWstaw w rot.y, a w parametrze name niepowtarzalną nazwę (wartość domyślna z losowym przyrostkiem).\nPozostałe parametry dostają wartości domyślne." + +msgid "Suggest roles" +msgstr "Podpowiedz role" + +msgid "Sets roles of the parameters still marked as free, where the way the template uses them tells what they are" +msgstr "Ustawia role parametrów nadal oznaczonych jako wolne, gdy sposób ich użycia w szablonie mówi, czym są" + +msgid "Add parameter" +msgstr "Dodaj parametr" + +msgid "Save to file" +msgstr "Zapisz do pliku" + +msgid "Reload" +msgstr "Wczytaj ponownie" + +msgid "The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; the description is stored in the template file, as //$e comment lines." +msgstr "Szablony .inc z katalogu scenerii. Opisz szablon i jego parametry, żeby dodać go do banku węzłów; opis jest zapisywany w pliku szablonu jako linie komentarza //$e." + +msgid "The node bank offers the templates in a scenery opened for editing (-edit)." +msgstr "Bank węzłów oferuje szablony w scenerii otwartej do edycji (-edit)." + +msgid "Editor Settings" +msgstr "Ustawienia edytora" + +msgid "General" +msgstr "Ogólne" + +msgid "Camera movement" +msgstr "Ruch kamery" + +msgid "Transform gizmo (ImGuizmo)" +msgstr "Gizmo przekształceń (ImGuizmo)" + +msgid "Grid cells" +msgstr "Komórki siatki" + +msgid "Cell size (m)" +msgstr "Rozmiar komórki (m)" + +msgid "Base height (m)" +msgstr "Wysokość bazowa (m)" + +msgid "Texture (optional)" +msgstr "Tekstura (opcjonalnie)" + +msgid "Create flat terrain" +msgstr "Utwórz płaski teren" + +msgid "Chunks / side" +msgstr "Fragmenty / bok" + +msgid "Create chunked terrain" +msgstr "Utwórz teren z fragmentów" + +msgid "total %d x %d m, %d chunks" +msgstr "razem %d x %d m, fragmentów: %d" + +msgid "Chunk edit mode (LMB add neighbour / Shift = delete)" +msgstr "Tryb edycji fragmentów (LPM dodaje sąsiedni / Shift = usuń)" + +msgid "Grid chunks: %zu" +msgstr "Fragmenty siatki: %zu" + +msgid "Streaming (open world, follows camera)" +msgstr "Strumieniowanie (otwarty świat, za kamerą)" + +msgid "Stream radius" +msgstr "Promień strumieniowania" + +msgid "Persist edits to disk (16-bit)" +msgstr "Zapisuj zmiany na dysku (16-bit)" + +msgid "Stream terrain" +msgstr "Strumieniuj teren" + +msgid "Resident chunks: %zu (dir: %s)" +msgstr "Wczytane fragmenty: %zu (katalog: %s)" + +msgid "Patches: %zu" +msgstr "Łaty: %zu" + +msgid "Capture selected model as terrain" +msgstr "Zamień zaznaczony model na teren" + +msgid "Capture: select a model instance first" +msgstr "Zamiana: najpierw zaznacz instancję modelu" + +msgid "Brush radius" +msgstr "Promień pędzla" + +msgid "Brush strength" +msgstr "Siła pędzla" + +msgid "Raise centre" +msgstr "Podnieś środek" + +msgid "Lower centre" +msgstr "Obniż środek" + +msgid "Optimize (mesh simplification, all patches)" +msgstr "Optymalizacja (upraszczanie siatki, wszystkie łaty)" + +msgid "Flatness tol (m)" +msgstr "Tolerancja płaskości (m)" + +msgid "Auto-optimize after sculpt" +msgstr "Optymalizuj po rzeźbieniu" + +msgid "Optimize all" +msgstr "Optymalizuj wszystko" + +msgid "Full-res all" +msgstr "Pełna rozdzielczość wszędzie" + +msgid "Triangles: %zu / %zu" +msgstr "Trójkąty: %zu / %zu" + +msgid "Show orthophoto (geoportal.gov.pl)" +msgstr "Pokaż ortofotomapę (geoportal.gov.pl)" + +msgid "Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180" +msgstr "Środek scenerii (0,0,0) w PUWG 1992 / EPSG:2180" + +msgid "X (northing, m)" +msgstr "X (northing, m)" + +msgid "Y (easting, m)" +msgstr "Y (easting, m)" + +msgid "Enter the origin of this scenery first" +msgstr "Najpierw podaj środek tej scenerii" + +msgid "Camera at X %.1f Y %.1f" +msgstr "Kamera w X %.1f Y %.1f" + +msgid "Distance (tiles)" +msgstr "Zasięg (kafle)" + +msgid "Fit to terrain" +msgstr "Dopasuj do terenu" + +msgid "Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, editor terrain).\nPlaces without any ground use the height below." +msgstr "Kładzie zdjęcia na geometrii gruntu (kształty terenu, modele kafli terenu, teren edytora).\nMiejsca bez gruntu dostają wysokość podaną niżej." + +msgid "Refit" +msgstr "Dopasuj ponownie" + +msgid "Samples the ground again, after it was edited" +msgstr "Ponownie próbkuje grunt, po jego edycji" + +msgid "Lift above ground (m)" +msgstr "Uniesienie nad grunt (m)" + +msgid "Raises the fitted imagery above the ground. Keep it below the objects you want to see:\nin the scene everything lower than this ends up under the imagery." +msgstr "Unosi dopasowane zdjęcia nad grunt. Trzymaj je poniżej obiektów, które chcesz widzieć:\nw scenie wszystko, co jest niżej, znajdzie się pod zdjęciami." + +msgid "Height (m)" +msgstr "Wysokość (m)" + +msgid "Covered by objects and terrain" +msgstr "Zasłaniane przez obiekty i teren" + +msgid "Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\nIt is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\ntranslucent, it doesn't catch the mouse.\nNot available with the Better Renderer, which doesn't pick up geometry added in the editor." +msgstr "Rysuje zdjęcia jako część sceny, więc tory, modele i teren przed nimi je zasłaniają.\nJest oświetlone jak scena. W pełni kryjące łapie mysz przy wstawianiu jak każda powierzchnia;\nprzezroczyste nie łapie myszy.\nNiedostępne z Better Renderer, który nie widzi geometrii dodanej w edytorze." + +msgid "Opacity" +msgstr "Krycie" + +msgid "In the scene the opacity is part of the textures: the tiles are read again from the cache\nshortly after the slider stops, which takes a moment." +msgstr "W scenie krycie jest częścią tekstur: kafle są ponownie wczytywane z pamięci podręcznej\nchwilę po puszczeniu suwaka, co trwa moment." + +msgid "Newest" +msgstr "Najnowsze" + +msgid "4K tiles where available" +msgstr "Kafle 4K, gdzie są dostępne" + +msgid "4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\nAreas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory." +msgstr "Kafle 4096 px (~6 cm/px) z ortofotomapy wysokiej rozdzielczości, dla kafli wokół kamery.\nTam, gdzie jej nie ma, zwykłe zdjęcia. Każdy kafel 4K zajmuje ~85 MB pamięci karty." + +msgid "Tiles: %d shown, %d loading, %d failed" +msgstr "Kafle: wyświetlane %d, wczytywane %d, nieudane %d" + +msgid "Retry" +msgstr "Ponów" + +msgid "This build has no HTTP client, only cached tiles are shown" +msgstr "Ten build nie ma klienta HTTP, widać tylko kafle z pamięci podręcznej" + +msgid "Cache: %s" +msgstr "Pamięć podręczna: %s" + +msgid "Imagery: GUGiK, geoportal.gov.pl" +msgstr "Zdjęcia: GUGiK, geoportal.gov.pl" + +msgid "Put all instances of the selected model on the ground" +msgstr "Postaw wszystkie instancje zaznaczonego modelu na ziemi" + +msgid "Select a model in the scene. Every instance of the same model file is moved straight up or down\nto the ground under it: terrain, large models and terrain made in the editor." +msgstr "Zaznacz model w scenie. Każda instancja tego samego pliku modelu zostanie przesunięta pionowo\nna grunt pod nią: teren, duże modele i teren zrobiony w edytorze." + +msgid "LMB: add outline point Backspace: remove last point" +msgstr "LPM: dodaj punkt obrysu Backspace: usuń ostatni punkt" + +msgid "Points: %zu Area: %.0f m2 (%.2f ha)" +msgstr "Punkty: %zu Powierzchnia: %.0f m2 (%.2f ha)" + +msgid "Remove last point" +msgstr "Usuń ostatni punkt" + +msgid "Clear outline" +msgstr "Wyczyść obrys" + +msgid "%zu templates in set" +msgstr "%zu szablonów w zestawie" + +msgid "Density (objects/ha)" +msgstr "Gęstość (obiektów/ha)" + +msgid "Min. spacing (m)" +msgstr "Min. odstęp (m)" + +msgid "Random rotation" +msgstr "Losowy obrót" + +msgid "Large models count as ground (terrain tiles)" +msgstr "Duże modele liczą się jako grunt (kafle terenu)" + +msgid "Estimated objects: %.0f" +msgstr "Szacowana liczba obiektów: %.0f" + +msgid "Fill area" +msgstr "Wypełnij obszar" + +msgid "Copies of the selected model, set out in a row" +msgstr "Kopie zaznaczonego modelu, ustawione w rzędzie" + +msgid "Fixed count" +msgstr "Stała liczba" + +msgid "Fit length" +msgstr "Dopasuj do długości" + +msgid "Count" +msgstr "Liczba" + +msgid "Number of the models in the array, the selected one included" +msgstr "Liczba modeli w tablicy, łącznie z zaznaczonym" + +msgid "As many copies are made as fit in this length, measured along the row" +msgstr "Powstaje tyle kopii, ile mieści się w tej długości, mierzonej wzdłuż rzędu" + +msgid "Relative offset" +msgstr "Odsunięcie względne" + +msgid "Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n1 along an axis puts the copies end to end" +msgstr "Odległość między kopiami jako część rozmiaru modelu, wzdłuż jego osi: x, y (w górę), z.\n1 wzdłuż osi ustawia kopie jedna za drugą" + +msgid "x size" +msgstr "x rozmiaru" + +msgid "Constant offset" +msgstr "Odsunięcie stałe" + +msgid "Distance between the copies in metres, along the axes of the model: x, y (up), z.\nWith both offsets on the copies are set apart by their sum" +msgstr "Odległość między kopiami w metrach, wzdłuż osi modelu: x, y (w górę), z.\nPrzy obu odsunięciach kopie dzieli ich suma" + +msgid "Turn per copy (deg)" +msgstr "Obrót na kopię (°)" + +msgid "Each copy is turned by this much around the vertical axis against the one before it,\nand the row bends with them into an arc" +msgstr "Każda kopia jest obrócona o tyle wokół osi pionowej względem poprzedniej,\na rząd wygina się razem z nimi w łuk" + +msgid "Follow the ground" +msgstr "Za gruntem" + +msgid "The copies are put as high over the ground as the model is, instead of level with it.\nNot for the models large enough to count as ground themselves" +msgstr "Kopie stoją tak wysoko nad gruntem jak model, zamiast na jego poziomie.\nNie dla modeli tak dużych, że same liczą się jako grunt" + +msgid "Model %.2f x %.2f x %.2f m, %d in the array, %.2f m apart" +msgstr "Model %.2f x %.2f x %.2f m, w tablicy %d, co %.2f m" + +msgid "The array follows the settings" +msgstr "Tablica idzie za ustawieniami" + +msgid "The copies stay the way they are. Selecting or changing anything else does the same" +msgstr "Kopie zostają tak, jak są. To samo daje zaznaczenie lub zmiana czegokolwiek innego" + +msgid "Make array" +msgstr "Utwórz tablicę" + +msgid "Select a model to make an array of" +msgstr "Zaznacz model, z którego ma powstać tablica" + +msgid "Extent (m), wheel" +msgstr "Zasięg (m), kółko" + +msgid "Enabled" +msgstr "Włączone" + +msgid "Suspended while editing terrain" +msgstr "Wstrzymane podczas edycji terenu" + +msgid "Translate (Q)" +msgstr "Przesuń (Q)" + +msgid "Rotate (W)" +msgstr "Obróć (W)" + +msgid "Scale (E)" +msgstr "Skaluj (E)" + +msgid "Local space (R)" +msgstr "Układ lokalny (R)" + +msgid "Snap (hold Ctrl)" +msgstr "Krok (przytrzymaj Ctrl)" + +msgid "No node selected" +msgstr "Nic nie zaznaczono" + +msgid "Editor History" +msgstr "Historia edytora" + +msgid "Max history size" +msgstr "Maks. rozmiar historii" + +msgid "Max placement distance" +msgstr "Maks. odległość wstawiania" + +msgid "History (newest at end): %zu entries" +msgstr "Historia (najnowsze na końcu): %zu wpisów" + +msgid "Clear History" +msgstr "Wyczyść historię" + +msgid "Undo Selected" +msgstr "Cofnij zaznaczone" + +# scenery editor: modes, layers and messages + +msgid "Set##insert" +msgstr "Zestaw##insert" + +msgid "File" +msgstr "Plik" + +msgid "Start the simulator with -edit to save changes to the scenery files" +msgstr "Uruchom symulator z -edit , żeby zapisywać zmiany do plików scenerii" + +msgid "Export scenery dump" +msgstr "Eksportuj zrzut scenerii" + +msgid "Writes the whole scene to _export files next to the scenario, the scenery files are left as they are" +msgstr "Zapisuje całą scenę do plików _export obok scenariusza, pliki scenerii zostają bez zmian" + +msgid "Mode windows" +msgstr "Okna trybów" + +msgid "Toolset" +msgstr "Przybornik" + +msgid "Layers" +msgstr "Warstwy" + +msgid "Include database" +msgstr "Baza include'ów" + +msgid "Preset" +msgstr "Gotowy zestaw" + +msgid "Manual list" +msgstr "Lista ręczna" + +msgid "Insert" +msgstr "Wstaw" + +msgid "Brush" +msgstr "Pędzel" + +msgid "Area fill" +msgstr "Wypełnianie obszaru" + +msgid "Copy to bank" +msgstr "Kopiuj do banku" + +msgid "Random" +msgstr "Losowy" + +msgid "Fixed" +msgstr "Stały" + +msgid "Default (from template)" +msgstr "Domyślny (z szablonu)" + +msgid "Select a layer first" +msgstr "Najpierw zaznacz warstwę" + +msgid "Layer \"%s\" restored" +msgstr "Warstwa \"%s\" przywrócona" + +msgid "Remove..." +msgstr "Usuń..." + +msgid "Merge into..." +msgstr "Scal z..." + +msgid "The scenario file can't be merged into another layer" +msgstr "Pliku scenariusza nie da się scalić z inną warstwą" + +msgid "Layer \"%s\" can't take new nodes" +msgstr "Warstwa \"%s\" nie może przyjąć nowych węzłów" + +msgid "New layer, its file is created on save" +msgstr "Nowa warstwa, jej plik powstanie przy zapisie" + +msgid "Dropped from the scenery on save" +msgstr "Wypada ze scenerii przy zapisie" + +msgid "Scenery file" +msgstr "Plik scenerii" + +msgid "Layer \"%s\" created and made active. Its file is written on save." +msgstr "Warstwa \"%s\" utworzona i ustawiona jako aktywna. Jej plik zostanie zapisany przy zapisie." + +msgid "Layer \"%s\" removed. It can be restored until the scenery is saved." +msgstr "Warstwa \"%s\" usunięta. Można ją przywrócić, dopóki sceneria nie zostanie zapisana." + +msgid "Merge" +msgstr "Scal" + +msgid "Pick the target layer" +msgstr "Wybierz warstwę docelową" + +msgid "Layer \"%s\" merged into \"%s\". The files are changed on save." +msgstr "Warstwa \"%s\" scalona z \"%s\". Pliki zmienią się przy zapisie." + +msgid "WSAD (new)" +msgstr "WSAD (nowy)" + +msgid "Arrows (legacy)" +msgstr "Strzałki (stary)" + +msgid "The scenery wasn't opened for editing. Start the simulator with: -edit " +msgstr "Sceneria nie jest otwarta do edycji. Uruchom symulator z: -edit " + +msgid "Select a model in the scene first" +msgstr "Najpierw zaznacz model w scenie" + +msgid "Removed %zu objects of the last fill" +msgstr "Usunięto obiekty ostatniego wypełnienia: %zu" + +msgid "Outline needs at least 3 points" +msgstr "Obrys potrzebuje co najmniej 3 punktów" + +msgid "Selected model package is empty" +msgstr "Wybrany zestaw modeli jest pusty" + +msgid "Density too low for this area, nothing to place" +msgstr "Za mała gęstość na ten obszar, nie ma czego wstawić" + +msgid "Placed %zu of %zu objects" +msgstr "Wstawiono %zu z %zu obiektów" + +msgid "Model set" +msgstr "Zestaw modeli" + +msgid "Custom list" +msgstr "Własna lista" + +msgid "Undo last fill (%zu)" +msgstr "Cofnij ostatnie wypełnienie (%zu)" + +msgid "The offsets come to nothing, the copies would sit in the model" +msgstr "Odsunięcia dają zero, kopie siedziałyby w modelu" + +msgid "An array of one is the model itself, nothing to make" +msgstr "Tablica z jednego to sam model, nie ma czego tworzyć" + +msgid "The length takes no copy, nothing to make" +msgstr "Ta długość nie mieści żadnej kopii, nie ma czego tworzyć" + +msgid "Only %zu of %zu copies could be made" +msgstr "Udało się zrobić tylko %zu z %zu kopii" + +msgid "No copies with these settings" +msgstr "Przy tych ustawieniach nie ma kopii" + +msgid "The model and %zu copies; Ctrl+Z takes the array back" +msgstr "Model i kopie: %zu; Ctrl+Z cofa tablicę" + +msgid "Include selected, no gizmo under rotate or scale" +msgstr "Zaznaczony include, bez gizma przy obrocie lub skali" + +msgid "Include selected, axes as its template allows" +msgstr "Zaznaczony include, osie na tyle, na ile pozwala szablon" + +msgid "B: bend along %s" +msgstr "B: wygnij wzdłuż %s" + +msgid "Click: the reference point, where it lands on the model" +msgstr "Klik: punkt odniesienia tam, gdzie trafi w model" + +msgid "Drag: offset from the track" +msgstr "Przeciągnij: odsunięcie od toru" + +msgid "Shift: height Ctrl: along the track" +msgstr "Shift: wysokość Ctrl: wzdłuż toru" + +msgid "Click: edit the bent model" +msgstr "Klik: edytuj wygięty model" + +msgid "Bend it along the track (B)" +msgstr "Wygnij wzdłuż toru (B)" + +msgid "Select a model by a track, or click a bent one" +msgstr "Zaznacz model przy torze albo kliknij wygięty" + +msgid "LMB on a track: where the vehicle stands" +msgstr "LPM na torze: tam stanie pojazd" + +msgid "Platform" +msgstr "Peron" + +msgid "Puts the edge facing the track 1.725 m from its axis, set back by the widening of the gauge in the curves.\nStands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\nhigher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5" +msgstr "Ustawia krawędź od strony toru 1,725 m od jego osi, odsuniętą o poszerzenie skrajni na łukach.\nStoi na płaszczyźnie główek szyn przedłużonej do krawędzi: niżej po wewnętrznej stronie łuku z przechyłką,\nwyżej po zewnętrznej; po wewnętrznej dodatkowo odsunięty o przechyłkę razy wysokość krawędzi / 1,5" diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 7cfd8b14..e78f9bf1 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -837,7 +837,7 @@ bool node_layers::pending() const namespace { // marks of the lines the editor keeps track of -std::array const editormarks{"//$p", "//$b"}; +std::array const editormarks{"//$p", "//$b", "//$c"}; } // namespace void node_layers::scan(layer_handle const Layer) @@ -1104,3 +1104,28 @@ void node_layers::rebuilt(instance_handle const Instance) } // namespace scene //--------------------------------------------------------------------------- + +std::string scene::node_layers::check_sources() const +{ + std::map files; + std::size_t good{0}, bad{0}; + std::string report; + for (auto const &[node, source] : m_sources) + { + auto &text{files[source.layer]}; + if (text.empty()) + { + std::ifstream file(path(source.layer), std::ios::binary); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + auto const ok{source.span.valid() && static_cast(source.span.end) <= text.size()}; + std::string piece{ok ? text.substr(source.span.begin, source.span.end - source.span.begin) : std::string{}}; + auto const model{dynamic_cast(node) != nullptr}; + auto const fine{ok && piece.rfind("node", 0) == 0 && (false == model || (piece.size() >= 8 && piece.compare(piece.size() - 8, 8, "endmodel") == 0))}; + if (fine) + ++good; + else if (++bad <= 5) + report += " [" + node->name() + " " + std::to_string(source.span.begin) + "-" + std::to_string(source.span.end) + ": " + piece.substr(0, 60) + " ... " + (piece.size() > 30 ? piece.substr(piece.size() - 30) : std::string{}) + "]"; + } + return std::to_string(good) + " good, " + std::to_string(bad) + " bad" + report; +} diff --git a/scene/scenelayers.h b/scene/scenelayers.h index f59a37e0..fc5ec082 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -349,7 +349,8 @@ class node_layers // writes changes made in the editor to the scenery files: changed, created and deleted model instances and // memory cells, and layer changes. the files are left intact unless the whole operation can be performed. // Rootstatements: additional statements to place at the end of the scenario file - save_result save(std::vector const &Rootstatements = {}); + save_result save(std::vector const &Rootstatements = {}, std::vector const &Trailingstatements = {}); + std::string check_sources() const; private: // types diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 7751a860..b6b4ae17 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -630,7 +630,7 @@ bool node_layers::compose(save_state &State, layer_handle const Layer, compositi } // writes changes made in the editor to the scenery files -save_result node_layers::save(std::vector const &Rootstatements) +save_result node_layers::save(std::vector const &Rootstatements, std::vector const &Trailingstatements) { save_result result; if (m_layers.empty()) @@ -1369,7 +1369,7 @@ save_result node_layers::save(std::vector const &Rootstatements) continue; } if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 || placed.count(candidate) != 0 || - markedtext.count(candidate) != 0 || (candidate == root && false == Rootstatements.empty())) + markedtext.count(candidate) != 0 || (candidate == root && (false == Rootstatements.empty() || false == Trailingstatements.empty()))) { outputs.emplace_back(candidate); } @@ -1479,6 +1479,14 @@ save_result node_layers::save(std::vector const &Rootstatements) added.text += statement + eol; } } + if (output == root && false == Trailingstatements.empty()) + { + composition trailing; + for (auto const &statement : Trailingstatements) + trailing.text += statement + eol; + ensure_newline(text.text, eol); + text.append(trailing); + } if (added.text.empty()) { continue; diff --git a/world/Sweep.cpp b/world/Sweep.cpp index ef3af81b..422f03aa 100644 --- a/world/Sweep.cpp +++ b/world/Sweep.cpp @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "utilities/parser.h" #include "vehicle/DynObj.h" +#include "world/Track.h" #include #include @@ -28,6 +29,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace { @@ -36,6 +38,9 @@ double const sample_spacing{0.5}; // m along the curve between the samples std::size_t const vertex_limit{4000000}; // copies beyond this many vertices are left out double const slice_width{1.0}; // m of the model, the faces of a bent copy are cut into double const edge_band{0.3}; // m from the side facing the curve, where the top of the edge is looked for +double const outer_reach{30.0}; +double const overhang_band{0.15}; +double const overhang_depth{0.05}; struct cubic { @@ -75,6 +80,75 @@ double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) return glm::length(glm::dvec2{B.x - A.x, B.z - A.z}); } +// the gauge changes from 26 m ahead of a curve or of a cant ramp (PKP PLK ST-T2), so the tracks beyond the ends are looked at this far +double const lead_reach{26.0}; + +segment_data turned_piece(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_points(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 true; +} + +glm::dvec3 plan_heading(segment_data const &Piece, double const T) +{ + auto const direction{cubic{Piece}.first(T)}; + glm::dvec3 const plan{direction.x, 0.0, direction.z}; + return glm::length(plan) > 1e-9 ? glm::normalize(plan) : glm::dvec3{0.0, 0.0, 1.0}; +} + +// paths going on from the end of the last of the pieces, each turned to lead away from it, up to the lead reach +std::vector paths_beyond(segment_data Last, std::vector const &Own) +{ + std::vector result; + double length{0.0}; + while (length < lead_reach && result.size() < 16) + { + auto const at{Last.points[segment_data::point::end]}; + if (simulation::Region == nullptr || false == simulation::Region->point_inside(at)) + break; + auto const &paths{simulation::Region->section(at).cell(at).m_directories.paths}; + std::optional next; + for (auto *track : paths) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0) + continue; + for (auto const &path : track->m_paths) + for (auto const forward : {true, false}) + { + auto const candidate{forward ? path : turned_piece(path)}; + if (next.has_value() || glm::distance(candidate.points[segment_data::point::start], at) > 0.01 || + glm::dot(plan_heading(Last, 1.0), plan_heading(candidate, 0.0)) < 0.9) + continue; + auto const known = [&](segment_data const &Piece) { return same_points(Piece, candidate) || same_points(turned_piece(Piece), candidate); }; + if (std::any_of(Own.begin(), Own.end(), known) || std::any_of(result.begin(), result.end(), known)) + continue; + next = candidate; + } + } + if (false == next.has_value()) + break; + cubic const curve{*next}; + for (int k = 1; k <= 8; ++k) + length += plan_distance(curve.point((k - 1) / 8.0), curve.point(k / 8.0)); + result.push_back(*next); + Last = *next; + } + return result; +} + // triangles of the model which share the look, in the space of the model struct model_part { @@ -84,10 +158,13 @@ struct model_part std::vector vertices; }; -void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_data const &Skins, std::vector &Parts) +// without the parts lit at night (the glow, the pool of light on the ground), which the instances show only with the lights on +void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_data const &Skins, std::array const &Lit, std::vector &Parts) { for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->Next) { + if (std::find(Lit.begin(), Lit.end(), submodel) != Lit.end()) + continue; auto transform{Transform}; if ((submodel->iFlags & 0xC000) != 0 && submodel->GetMatrix() != nullptr) { @@ -107,7 +184,7 @@ void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_dat part.lighting.diffuse = submodel->f4Diffuse; part.lighting.specular = submodel->f4Specular; part.translucent = material != null_handle && GfxRenderer->Material(material)->get_or_guess_opacity() == 0.0f; - auto const normals{glm::dmat3(transform)}; + auto const normals{glm::transpose(glm::inverse(glm::dmat3(transform)))}; auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)}; auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)}; auto const take = [&](gfx::basic_vertex const &Vertex) { @@ -142,7 +219,7 @@ void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_dat } if (submodel->Child != nullptr) { - gather_parts(submodel->Child, transform, Skins, Parts); + gather_parts(submodel->Child, transform, Skins, Lit, Parts); } } } @@ -207,6 +284,93 @@ std::vector slices(std::vector const &Vertices, std: return result; } +struct laid_model +{ + std::vector parts; + glm::dvec3 origin{0.0}; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + bool bent{false}; +}; + +struct layout +{ + std::vector models; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + glm::dvec3 shift{0.0}; + bool edge_far{false}; +}; + +bool lay_out(sweep_node const &Sweep, layout &Layout) +{ + auto const &definition{Sweep.definition()}; + auto const scaling{glm::scale(glm::dmat4(1.0), definition.scale)}; + for (auto const &entry : Sweep.items()) + { + laid_model model; + auto const transform{scaling * entry.transform}; + gather_parts(entry.model->Root, transform, entry.skins, entry.model->instance_parts().lights_on, model.parts); + model.origin = glm::dvec3(transform[3]); + if (false == model.parts.empty()) + Layout.models.push_back(std::move(model)); + } + if (Layout.models.empty()) + return false; + auto &low{Layout.low}; + auto &high{Layout.high}; + for (auto &model : Layout.models) + { + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{Sweep.axes(vertex.position)}; + model.low = glm::min(model.low, local); + model.high = glm::max(model.high, local); + } + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + for (auto &model : Layout.models) + model.bent = definition.bend && (Layout.models.size() == 1 || model.high.x - model.low.x >= 0.5 * (high.x - low.x)); + if (std::any_of(Layout.models.begin(), Layout.models.end(), [](laid_model const &Model) { return Model.bent; })) + { + low = glm::dvec3{std::numeric_limits::max()}; + high = glm::dvec3{-std::numeric_limits::max()}; + for (auto const &model : Layout.models) + if (model.bent) + { + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + } + auto const nearside{definition.lateral >= 0.0 ? low.y : high.y}; + auto const farside{definition.lateral >= 0.0 ? high.y : low.y}; + double edgetop{-std::numeric_limits::max()}; + double nearbottom{std::numeric_limits::max()}, farbottom{std::numeric_limits::max()}; + for (auto const &model : Layout.models) + if (model.bent || false == definition.bend) + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{Sweep.axes(vertex.position)}; + if (std::abs(local.y - nearside) <= edge_band) + edgetop = std::max(edgetop, local.z); + if (std::abs(local.y - nearside) <= overhang_band) + nearbottom = std::min(nearbottom, local.z); + if (std::abs(local.y - farside) <= overhang_band) + farbottom = std::min(farbottom, local.z); + } + if (edgetop == -std::numeric_limits::max()) + edgetop = high.z; + Layout.edge_far = farbottom > nearbottom + overhang_depth; + auto const point{Sweep.axes(definition.scale * definition.point)}; + double const sides[]{0.0, -nearside, -farside, -0.5 * (low.y + high.y), -point.y}; + double const heights[]{0.0, -low.z, -high.z, -edgetop, -point.z}; + Layout.shift = glm::dvec3{0.0, sides[std::clamp(definition.side_anchor, 0, 4)], heights[std::clamp(definition.height_anchor, 0, 4)]}; + return true; +} + } // namespace // height of the head of the rail on the side of the offset over the points of the track, less the height of the rail; 0 for no side @@ -222,7 +386,7 @@ double sweep_node::rail_head(double const Roll, double const Lateral) bool sweep_node::state::operator==(state const &Other) const { if (model != Other.model || skin != Other.skin || bend != Other.bend || step != Other.step || from != Other.from || to != Other.to || lateral != Other.lateral || height != Other.height || along_x != Other.along_x || - flip != Other.flip || mirror != Other.mirror || tilt != Other.tilt || face != Other.face || widen != Other.widen || parameters != Other.parameters || side_anchor != Other.side_anchor || height_anchor != Other.height_anchor || pieces.size() != Other.pieces.size()) + flip != Other.flip || mirror != Other.mirror || tilt != Other.tilt || face != Other.face || widen != Other.widen || platform != Other.platform || parameters != Other.parameters || side_anchor != Other.side_anchor || height_anchor != Other.height_anchor || point != Other.point || scale != Other.scale || pieces.size() != Other.pieces.size()) { return false; } @@ -285,6 +449,10 @@ void sweep_node::import(cParser &Input, glm::dvec3 const &Offset) { m_state.widen = true; } + else if (token == "platform") + { + m_state.platform = true; + } else if (token == "noface") { m_state.face = false; @@ -302,8 +470,18 @@ void sweep_node::import(cParser &Input, glm::dvec3 const &Offset) { auto const side{Input.getToken()}; auto const height{Input.getToken()}; - m_state.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0; - m_state.height_anchor = height == "bottom" ? 1 : height == "top" ? 2 : height == "edge" ? 3 : 0; + m_state.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : side == "point" ? 4 : 0; + m_state.height_anchor = height == "bottom" ? 1 : height == "top" ? 2 : height == "edge" ? 3 : height == "point" ? 4 : 0; + } + else if (token == "point") + { + Input.getTokens(3); + Input >> m_state.point.x >> m_state.point.y >> m_state.point.z; + } + else if (token == "scale") + { + Input.getTokens(3); + Input >> m_state.scale.x >> m_state.scale.y >> m_state.scale.z; } else if (token == "piece") { @@ -335,49 +513,295 @@ void sweep_node::define(state const &State) } } -void sweep_node::rebuild_samples() +void sweep_node::sample_piece(segment_data const &Piece, std::vector &Samples, double &Station) { - m_samples.clear(); - double station{0.0}; - for (auto const &piece : m_state.pieces) + cubic const curve{Piece}; + auto const count{std::clamp(static_cast(plan_distance(curve.p0, curve.p3) / sample_spacing), 4, 4000)}; + for (int k = Samples.empty() ? 0 : 1; k <= count; ++k) { - cubic const curve{piece}; - auto const count{std::clamp(static_cast(plan_distance(curve.p0, curve.p3) / sample_spacing), 4, 4000)}; - for (int k = m_samples.empty() ? 0 : 1; k <= count; ++k) + auto const t{static_cast(k) / count}; + auto const position{curve.point(t)}; + if (false == Samples.empty()) { - auto const t{static_cast(k) / count}; - auto const position{curve.point(t)}; - if (false == m_samples.empty()) + Station += plan_distance(Samples.back().position, position); + } + auto tangent{curve.first(t)}; + tangent = glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{0.0, 0.0, 1.0}; + Samples.push_back({Station, position, tangent, Piece.rolls[0] + (Piece.rolls[1] - Piece.rolls[0]) * t, {}, {}}); + } +} + +// the structure gauge widens ahead of the curves and of the cant, the same way the gauge check of the editor has it +void sweep_node::gauge_along(std::vector &Samples) +{ + if (Samples.size() < 3) + return; + std::vector chainage, curvature, cant; + for (std::size_t i = 0; i < Samples.size(); ++i) + { + auto const &before{Samples[i == 0 ? 0 : i - 1]}; + auto const &after{Samples[std::min(i + 1, Samples.size() - 1)]}; + glm::dvec2 const a{before.tangent.x, before.tangent.z}; + glm::dvec2 const b{after.tangent.x, after.tangent.z}; + auto const span{after.station - before.station}; + auto const turn{glm::length(a) > 1e-9 && glm::length(b) > 1e-9 ? std::atan2(a.x * b.y - a.y * b.x, glm::dot(a, b)) : 0.0}; + chainage.push_back(Samples[i].station); + curvature.push_back(span > 1e-9 ? turn / span : 0.0); + cant.push_back(1.5 * std::abs(std::sin(glm::radians(Samples[i].roll)))); + } + auto const sections{gauge::sections(gauge::kind::unified, chainage, curvature, cant)}; + for (std::size_t i = 0; i < Samples.size() && i < sections.size(); ++i) + { + Samples[i].widening = sections[i].lower; + Samples[i].cant = sections[i].cant; + } +} + +void sweep_node::beside_switches(std::vector> const &Spans) +{ + auto const side{m_state.lateral >= 0.0 ? 1 : 0}; + auto const sign{m_state.lateral >= 0.0 ? 1.0 : -1.0}; + auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; + auto const total = [&](sample const &Sample) { return Sample.widening[side] + std::max(0.0, Sample.cant[side]) * edgeheight / 1.5; }; + auto const matches = [](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 true; + }; + auto const turned = [](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; + }; + for (std::size_t piece = 0; piece < m_state.pieces.size() && piece < Spans.size(); ++piece) + { + auto const &own{m_state.pieces[piece]}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2 || + plan_distance(track->m_paths.front().points[segment_data::point::start], own.points[segment_data::point::start]) > 300.0) + continue; + for (std::size_t path = 0; path < track->m_paths.size(); ++path) { - station += plan_distance(m_samples.back().position, position); + auto const forward{matches(own, track->m_paths[path])}; + if (false == forward && false == matches(own, turned(track->m_paths[path]))) + continue; + for (std::size_t other = 0; other < track->m_paths.size(); ++other) + { + if (other == path) + continue; + std::vector chain; + double station{0.0}; + if (forward) + { + for (auto const &sample : m_samples) + if (sample.station >= Spans[piece].first - outer_reach && sample.station < Spans[piece].first) + { + station = sample.station; + chain.push_back(sample); + } + sample_piece(track->m_paths[other], chain, station); + } + else + { + sample_piece(turned(track->m_paths[other]), chain, station); + auto const shift{station - Spans[piece].second}; + for (auto const &sample : m_samples) + if (sample.station > Spans[piece].second && sample.station <= Spans[piece].second + outer_reach) + { + chain.push_back(sample); + chain.back().station += shift; + } + } + gauge_along(chain); + for (auto &here : m_samples) + { + if (here.station < Spans[piece].first - outer_reach || here.station > Spans[piece].second + outer_reach) + continue; + glm::dvec3 const left{-here.tangent.z, 0.0, here.tangent.x}; + sample const *nearest{nullptr}; + auto best{std::numeric_limits::max()}; + for (auto const &point : chain) + { + auto const along{std::abs(glm::dot(glm::dvec3{point.position.x - here.position.x, 0.0, point.position.z - here.position.z}, here.tangent))}; + if (along < best) + { + best = along; + nearest = &point; + } + } + if (nearest == nullptr || best > sample_spacing) + continue; + auto const offset{sign * glm::dot(glm::dvec3{nearest->position.x - here.position.x, 0.0, nearest->position.z - here.position.z}, glm::length(left) > 1e-9 ? glm::normalize(left) : left)}; + if (offset > 0.05) + continue; + auto const needed{offset + total(*nearest)}; + if (needed > total(here)) + { + here.widening[side] = needed; + here.cant[side] = 0.0; + } + } + } } - auto tangent{curve.first(t)}; - tangent = glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{0.0, 0.0, 1.0}; - m_samples.push_back({station, position, tangent, piece.rolls[0] + (piece.rolls[1] - piece.rolls[0]) * t, {}, {}}); } } - if (m_samples.size() > 2) +} + +bool sweep_node::bridge() +{ + auto const turned = [](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; + }; + auto const same = [](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 true; + }; + auto const length = [](segment_data const &Piece) { + std::vector samples; + double station{0.0}; + sample_piece(Piece, samples, station); + return station; + }; + auto const heading = [](segment_data const &Piece, double const T) { + auto const direction{cubic{Piece}.first(T)}; + glm::dvec3 const plan{direction.x, 0.0, direction.z}; + return glm::length(plan) > 1e-9 ? glm::normalize(plan) : glm::dvec3{0.0, 0.0, 1.0}; + }; + auto state{m_state}; + auto changed{false}; + double station{0.0}; + for (std::size_t i = 0; i + 1 < state.pieces.size(); ++i) { - // the structure gauge widens ahead of the curves and of the cant, the same way the gauge check of the editor has it - std::vector chainage, curvature, cant; - for (std::size_t i = 0; i < m_samples.size(); ++i) + station += length(state.pieces[i]); + auto const target{state.pieces[i + 1].points[segment_data::point::start]}; + auto at{state.pieces[i].points[segment_data::point::end]}; + if (glm::distance(at, target) < 0.01) + continue; + auto const chord{plan_distance(at, target)}; + std::vector fill; + auto previous{state.pieces[i]}; + auto reached{false}; + for (int step = 0; step < 20 && false == reached; ++step) { - auto const &before{m_samples[i == 0 ? 0 : i - 1]}; - auto const &after{m_samples[std::min(i + 1, m_samples.size() - 1)]}; - glm::dvec2 const a{before.tangent.x, before.tangent.z}; - glm::dvec2 const b{after.tangent.x, after.tangent.z}; - auto const span{after.station - before.station}; - auto const turn{glm::length(a) > 1e-9 && glm::length(b) > 1e-9 ? std::atan2(a.x * b.y - a.y * b.x, glm::dot(a, b)) : 0.0}; - chainage.push_back(m_samples[i].station); - curvature.push_back(span > 1e-9 ? turn / span : 0.0); - cant.push_back(1.5 * std::abs(std::sin(glm::radians(m_samples[i].roll)))); - } - auto const sections{gauge::sections(gauge::kind::unified, chainage, curvature, cant)}; - for (std::size_t i = 0; i < m_samples.size() && i < sections.size(); ++i) - { - m_samples[i].widening = sections[i].lower; - m_samples[i].cant = sections[i].cant; + std::optional best; + auto bestdistance{std::numeric_limits::max()}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (auto const &path : track->m_paths) + for (auto const forward : {true, false}) + { + auto const candidate{forward ? path : turned(path)}; + if (glm::distance(candidate.points[segment_data::point::start], at) > 0.01 || same(candidate, previous) || same(turned(candidate), previous)) + continue; + if (glm::dot(heading(previous, 1.0), heading(candidate, 0.0)) < 0.5) + continue; + auto const distance{glm::distance(candidate.points[segment_data::point::end], target)}; + if (distance < bestdistance) + { + bestdistance = distance; + best = candidate; + } + } + } + if (false == best.has_value()) + break; + fill.push_back(*best); + previous = *best; + at = best->points[segment_data::point::end]; + reached = glm::distance(at, target) < 0.01; } + if (false == reached) + continue; + double added{0.0}; + for (auto const &piece : fill) + added += length(piece); + auto const shift{added - chord}; + if (state.from >= station) + state.from += shift; + if (state.to >= station) + state.to += shift; + state.pieces.insert(state.pieces.begin() + i + 1, fill.begin(), fill.end()); + station += added; + i += fill.size(); + changed = true; + } + if (changed) + { + WriteLog("Sweep \"" + m_name + "\": the gaps between its pieces filled with the paths which join them"); + define(state); + } + return changed; +} + +void sweep_node::refresh() +{ + auto const current{m_state}; + define(current); +} + +void sweep_node::rebuild_samples() +{ + m_boundsvalid = false; + m_samples.clear(); + std::vector> spans; + std::vector before, after; + if ((m_state.widen || m_state.platform) && false == m_state.pieces.empty()) + { + before = paths_beyond(turned_piece(m_state.pieces.front()), m_state.pieces); + after = paths_beyond(m_state.pieces.back(), m_state.pieces); + } + double station{0.0}; + for (auto piece = before.rbegin(); piece != before.rend(); ++piece) + sample_piece(turned_piece(*piece), m_samples, station); + auto const head{station}; + auto const first{m_samples.empty() ? std::size_t{0} : m_samples.size() - 1}; + for (auto const &piece : m_state.pieces) + { + auto const begin{station}; + sample_piece(piece, m_samples, station); + spans.emplace_back(begin - head, station - head); + } + auto const last{m_samples.size()}; + for (auto const &piece : after) + sample_piece(piece, m_samples, station); + gauge_along(m_samples); + // the paths beyond the ends only lead the gauge in, the copies stay on the pieces + m_samples.erase(m_samples.begin() + last, m_samples.end()); + m_samples.erase(m_samples.begin(), m_samples.begin() + first); + for (auto &sample : m_samples) + sample.station -= head; + if ((m_state.widen || m_state.platform) && m_samples.size() > 2) + { + beside_switches(spans); + } + m_distances.clear(); + glm::dvec3 previous{0.0}; + for (auto const &sample : m_samples) + { + frame at; + at.widening = sample.widening; + at.cant = sample.cant; + glm::dvec3 left{-sample.tangent.z, 0.0, sample.tangent.x}; + left = glm::length(left) > 1e-9 ? glm::normalize(left) : glm::dvec3{-1.0, 0.0, 0.0}; + auto const point{sample.position + left * (m_state.lateral + setback(at))}; + m_distances.push_back(m_distances.empty() ? 0.0 : m_distances.back() + std::max(1e-6, plan_distance(previous, point))); + previous = point; } if (false == m_samples.empty()) { @@ -393,12 +817,114 @@ double sweep_node::length() const double sweep_node::start() const { - return std::clamp(m_state.from, 0.0, length()); + return m_state.from; } double sweep_node::end() const { - return m_state.to < 0.0 ? length() : std::clamp(m_state.to, start(), length()); + return m_state.to < 0.0 ? length() : std::max(m_state.to, start()); +} + +double sweep_node::distance_at(double const Station) const +{ + if (m_samples.size() < 2) + return Station; + if (Station <= m_samples.front().station) + return m_distances.front() + Station - m_samples.front().station; + if (Station >= m_samples.back().station) + return m_distances.back() + Station - m_samples.back().station; + auto const after{static_cast(std::upper_bound(m_samples.begin(), m_samples.end(), Station, [](double const Value, sample const &Sample) { return Value < Sample.station; }) - m_samples.begin())}; + auto const before{after - 1}; + auto const span{m_samples[after].station - m_samples[before].station}; + auto const f{span > 1e-9 ? (Station - m_samples[before].station) / span : 0.0}; + return m_distances[before] + (m_distances[after] - m_distances[before]) * f; +} + +double sweep_node::station_at(double const Distance) const +{ + if (m_samples.size() < 2) + return Distance; + if (Distance <= m_distances.front()) + return m_samples.front().station + Distance - m_distances.front(); + if (Distance >= m_distances.back()) + return m_samples.back().station + Distance - m_distances.back(); + auto const after{static_cast(std::upper_bound(m_distances.begin(), m_distances.end(), Distance) - m_distances.begin())}; + auto const before{after - 1}; + auto const span{m_distances[after] - m_distances[before]}; + auto const f{span > 1e-12 ? (Distance - m_distances[before]) / span : 0.0}; + return m_samples[before].station + (m_samples[after].station - m_samples[before].station) * f; +} + +double sweep_node::setback(frame const &At) const +{ + if (false == m_state.widen && false == m_state.platform) + return 0.0; + auto const side{m_state.lateral >= 0.0 ? 1 : 0}; + auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; + auto const widening{At.widening[side] + std::max(0.0, At.cant[side]) * edgeheight / 1.5}; + return m_state.lateral >= 0.0 ? widening : -widening; +} + +double sweep_node::rise(frame const &At) const +{ + if (m_state.tilt) + return rail_head(At.roll, 0.0); + if (false == m_state.platform) + return rail_head(At.roll, m_state.lateral); + // PKP PLK ST-T2 table 3: H = Hi + D/2 -+ D (XB + dbS) / 1500 over the lower rail, with the cant D of the gauge, which + // changes from 20 m (inner side) and 26 m (outer side) ahead of the cant ramp, as the platform has to. taken as the + // turn of the edge with the plane of the rail heads, as the table has it only to the first order, and on the outer + // side, where the edge stays at XB + dbS instead of XB + dbS - dbD, the plane is higher by dbD D / 1500 there + auto const side{m_state.lateral >= 0.0 ? 1 : 0}; + auto const cant{At.cant[side]}; + auto const lower{rail_head(At.roll, 0.0) - 0.75 * std::abs(std::sin(glm::radians(At.roll)))}; + auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; + auto const slope{std::clamp(cant / 1.5, -0.5, 0.5)}; + auto const level{std::sqrt(1.0 - slope * slope)}; + auto const across{std::abs(m_state.lateral) + std::abs(setback(At))}; + return lower + 0.5 * std::abs(cant) - (across - edgeheight * slope) * slope / level + edgeheight * (level - 1.0); +} + +glm::dvec3 sweep_node::axes(glm::dvec3 const &Vector) const +{ + auto const &definition{m_state}; + auto const needsflip{definition.along_x ? definition.lateral < 0.0 : definition.lateral > 0.0}; + auto const flip{definition.flip != (definition.face && needsflip)}; + glm::dvec3 result{definition.along_x ? Vector.x : Vector.z, definition.along_x ? Vector.z : -Vector.x, Vector.y}; + if (flip) + { + result.x = -result.x; + result.y = -result.y; + } + if (definition.mirror) + { + result.y = -result.y; + } + return result; +} + +bool sweep_node::bounds(glm::dvec3 &Low, glm::dvec3 &High, glm::dvec3 &Shift) const +{ + if (false == m_boundsvalid) + { + layout laid; + m_boundsfound = lay_out(*this, laid); + m_low = laid.low; + m_high = laid.high; + m_shift = laid.shift; + m_edgefar = laid.edge_far; + m_boundsvalid = true; + } + Low = m_low; + High = m_high; + Shift = m_shift; + return m_boundsfound; +} + +bool sweep_node::edge_far() const +{ + glm::dvec3 low, high, shift; + return bounds(low, high, shift) && m_edgefar; } sweep_node::frame sweep_node::frame_at(double const Station) const @@ -562,11 +1088,12 @@ double sweep_node::project(glm::dvec3 const &Point, double &Lateral) const double sweep_node::model_length() const { auto const pieces{items()}; + auto const scaling{glm::scale(glm::dmat4(1.0), m_state.scale)}; double low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; for (auto const &entry : pieces) { std::vector parts; - gather_parts(entry.model->Root, entry.transform, entry.skins, parts); + gather_parts(entry.model->Root, scaling * entry.transform, entry.skins, entry.model->instance_parts().lights_on, parts); for (auto const &part : parts) for (auto const &vertex : part.vertices) { @@ -584,93 +1111,16 @@ std::vector sweep_node::create_shapes() const auto const &definition{m_state}; if (m_samples.size() < 2) return shapes; - // the parts of each model of the template, and where the model stands in the template - struct piece - { - std::vector parts; - glm::dvec3 origin{0.0}; - glm::dvec3 low{std::numeric_limits::max()}; - glm::dvec3 high{-std::numeric_limits::max()}; - bool bent{false}; - }; - std::vector pieces; - for (auto const &entry : items()) - { - piece model; - gather_parts(entry.model->Root, entry.transform, entry.skins, model.parts); - model.origin = glm::dvec3(entry.transform[3]); - if (false == model.parts.empty()) - pieces.push_back(std::move(model)); - } - if (pieces.empty()) + layout laid; + if (false == lay_out(*this, laid)) return shapes; - // the side of the model facing the curve is the one its lateral axis points away from: -x, or -z for a model laid along x - auto const needsflip{definition.along_x ? definition.lateral < 0.0 : definition.lateral > 0.0}; - auto const flip{definition.flip != (definition.face && needsflip)}; - // the model's own axes: along the curve, sideways (+ to the right of it), and up - auto const axes = [&](glm::dvec3 const &Vector) { - glm::dvec3 result{definition.along_x ? Vector.x : Vector.z, definition.along_x ? Vector.z : -Vector.x, Vector.y}; - if (flip) - { - result.x = -result.x; - result.y = -result.y; - } - if (definition.mirror) - { - result.y = -result.y; - } - return result; - }; - glm::dvec3 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; - for (auto &model : pieces) - { - for (auto const &part : model.parts) - for (auto const &vertex : part.vertices) - { - auto const local{axes(vertex.position)}; - model.low = glm::min(model.low, local); - model.high = glm::max(model.high, local); - } - low = glm::min(low, model.low); - high = glm::max(high, model.high); - } - // the long models of a template follow the curve, the others stand at their places along it - for (auto &model : pieces) - model.bent = definition.bend && (pieces.size() == 1 || model.high.x - model.low.x >= 0.5 * (high.x - low.x)); - // what's bent sets the length of a copy, its sides and its top; the rest only goes along with it - if (std::any_of(pieces.begin(), pieces.end(), [](piece const &Model) { return Model.bent; })) - { - low = glm::dvec3{std::numeric_limits::max()}; - high = glm::dvec3{-std::numeric_limits::max()}; - for (auto const &model : pieces) - if (model.bent) - { - low = glm::min(low, model.low); - high = glm::max(high, model.high); - } - } + auto &pieces{laid.models}; + auto const low{laid.low}; + auto const high{laid.high}; + auto const anchor_shift{laid.shift}; auto const own{high.x - low.x}; - // the point of the model the offset puts at its place, sideways and up - auto const nearside{definition.lateral >= 0.0 ? low.y : high.y}; - auto const farside{definition.lateral >= 0.0 ? high.y : low.y}; - double edgetop{-std::numeric_limits::max()}; - for (auto const &model : pieces) - if (model.bent || false == definition.bend) - for (auto const &part : model.parts) - for (auto const &vertex : part.vertices) - { - auto const local{axes(vertex.position)}; - if (std::abs(local.y - nearside) <= edge_band) - edgetop = std::max(edgetop, local.z); - } - if (edgetop == -std::numeric_limits::max()) - edgetop = high.z; - glm::dvec3 const anchor_shift{0.0, - definition.side_anchor == 1 ? -nearside : definition.side_anchor == 2 ? -farside : definition.side_anchor == 3 ? -0.5 * (low.y + high.y) : 0.0, - definition.height_anchor == 1 ? -low.z : definition.height_anchor == 2 ? -high.z : definition.height_anchor == 3 ? -edgetop : 0.0}; - auto const from{start()}; - auto const to{end()}; - auto const stretch{to - from}; + auto const first{distance_at(start())}; + auto const stretch{distance_at(end()) - first}; if (own < 1e-3 || stretch < 1e-3) return shapes; if (definition.bend) @@ -709,19 +1159,9 @@ std::vector sweep_node::create_shapes() const batches.push_back({Part.material, Part.lighting, Part.translucent, {}}); return batches.back().vertices; }; - // set back by the widening of the gauge: from the radius, and on the inner side of a canted curve by the lean of the cars - // at the height of the edge - auto const side{definition.lateral >= 0.0 ? 1 : 0}; - auto const edgeheight{std::max(0.0, definition.height - gauge::rail_height)}; - auto const setback = [&](frame const &At) { - if (false == definition.widen) - return 0.0; - auto const widening{At.widening[side] + std::max(0.0, At.cant[side]) * edgeheight / 1.5}; - return definition.lateral >= 0.0 ? widening : -widening; - }; // heights are given from the head of the rail on the side of the copies. the cant turns the track about its inner rail when the // simulation raises it for the cant, about the axis otherwise; a copy which leans with the cant goes up with the axis - auto const cone = [&](frame const &At) { return rail_head(At.roll, definition.tilt ? 0.0 : definition.lateral); }; + auto const cone = [&](frame const &At) { return rise(At); }; auto const upright = [](frame At) { glm::dvec3 forward{At.forward.x, 0.0, At.forward.z}; forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0}; @@ -733,12 +1173,12 @@ std::vector sweep_node::create_shapes() const glm::dvec3 const offset{0.0, definition.height, 0.0}; for (std::size_t copy = 0; copy < count; ++copy) { - auto const base{from + spacing * static_cast(copy)}; + auto const base{first + spacing * static_cast(copy)}; for (auto const &model : pieces) { // a model which doesn't bend stands upright at its point: the place of its origin along the curve auto const anchor{axes(model.origin) + anchor_shift}; - auto const stand{upright(frame_at(definition.bend ? base + (anchor.x - low.x) * scale : base))}; + auto const stand{upright(frame_at(station_at(definition.bend ? base + (anchor.x - low.x) * scale : base)))}; for (auto const &part : model.parts) { auto &vertices{target(part)}; @@ -750,7 +1190,7 @@ std::vector sweep_node::create_shapes() const world_vertex placed{vertex}; if (model.bent) { - auto const at{frame_at(base + (local.x - low.x) * scale)}; + auto const at{frame_at(station_at(base + (local.x - low.x) * scale))}; placed.position = at.position + at.left * (local.y + definition.lateral + setback(at)) + at.up * local.z + offset + glm::dvec3{0.0, cone(at), 0.0}; placed.normal = glm::vec3(glm::normalize(at.forward * direction.x + at.left * direction.y + at.up * direction.z)); } @@ -822,7 +1262,7 @@ void sweep_node::export_as_text_(std::ostream &Output) const { Output << "step " << sweep.step << ' '; } - if (sweep.from > 0.0 || sweep.to >= 0.0) + if (sweep.from != 0.0 || sweep.to >= 0.0) { Output << "range " << sweep.from << ' ' << sweep.to << ' '; } @@ -854,6 +1294,10 @@ void sweep_node::export_as_text_(std::ostream &Output) const { Output << "widen "; } + if (sweep.platform) + { + Output << "platform "; + } if (false == sweep.parameters.empty()) { Output << "parameters " << sweep.parameters.size() << ' '; @@ -862,9 +1306,17 @@ void sweep_node::export_as_text_(std::ostream &Output) const } if (sweep.side_anchor != 0 || sweep.height_anchor != 0) { - char const *sides[] = {"origin", "near", "far", "centre"}; - char const *heights[] = {"origin", "bottom", "top", "edge"}; - Output << "anchor " << sides[std::clamp(sweep.side_anchor, 0, 3)] << ' ' << heights[std::clamp(sweep.height_anchor, 0, 3)] << ' '; + char const *sides[] = {"origin", "near", "far", "centre", "point"}; + char const *heights[] = {"origin", "bottom", "top", "edge", "point"}; + Output << "anchor " << sides[std::clamp(sweep.side_anchor, 0, 4)] << ' ' << heights[std::clamp(sweep.height_anchor, 0, 4)] << ' '; + } + if (sweep.side_anchor == 4 || sweep.height_anchor == 4) + { + Output << "point " << sweep.point.x << ' ' << sweep.point.y << ' ' << sweep.point.z << ' '; + } + if (sweep.scale != glm::dvec3{1.0}) + { + Output << "scale " << sweep.scale.x << ' ' << sweep.scale.y << ' ' << sweep.scale.z << ' '; } auto const precision{Output.precision(std::numeric_limits::digits10)}; for (auto const &piece : sweep.pieces) @@ -892,6 +1344,15 @@ void sweep_table::create_geometry(scene::scratch_data &Scratchpad) { continue; } + if (sweep->bridge()) + { + if (Global.editor_session) + sweep->mark_dirty(); + } + else if (sweep->definition().widen || sweep->definition().platform) + { + sweep->refresh(); + } if (Global.editor_session && false == Global.NvRenderer) { // scenery opened for editing: the copies are kept apart, so they can be changed later diff --git a/world/Sweep.h b/world/Sweep.h index 7da708d7..2a8a11f6 100644 --- a/world/Sweep.h +++ b/world/Sweep.h @@ -38,10 +38,13 @@ struct scratch_data; // facing the curve, the other side or its middle; up its origin, its bottom or its top), // axis (axis of the model laid along the curve), flip (the model goes the other way), mirror (its sides swapped), // widen (the offset grows by the widening of the structure gauge in the curves, the way the platforms are set back), +// platform (as widen, and the copies stand on the plane of the rail heads carried on to the edge: lowered on the inner +// side of a canted curve, raised on the outer one), // noface (the model isn't turned to have its -x side, or -z for one laid along x, facing the curve), // parameters ... (of a template: the model can be an *.inc file, its long models are bent, the others stand // upright at their places along the curve), -// anchor sides: origin, near, far, centre; heights: origin, bottom, top, edge (the top of the side facing the curve), +// scale (of the model, along its own axes), point (a point of the model, in its own space, taken by the anchor point), +// anchor sides: origin, near, far, centre, point; heights: origin, bottom, top, edge (the top of the side facing the curve), point, // tilt (bent copies lean with the cant of the curve; otherwise they stay upright, as the platforms and the walls by a track do, // at the height the plane of the cant has at their sideways offset) class sweep_node : public scene::basic_node @@ -64,9 +67,12 @@ class sweep_node : public scene::basic_node bool tilt{false}; bool face{true}; // the model is turned so its -x side (-z for a model laid along x) faces the curve bool widen{false}; // moved away from the curve by the widening of the structure gauge in the curves + bool platform{false}; // set back as widen does, and its height taken from the plane of the rail heads at its edge std::vector parameters; // of a template (*.inc) laid along the curve - int side_anchor{0}; // 0: origin, 1: the side facing the curve, 2: the far side, 3: the middle - int height_anchor{0}; // 0: origin, 1: bottom, 2: top, 3: top of the side facing the curve + int side_anchor{0}; // 0: origin, 1: the side facing the curve, 2: the far side, 3: the middle, 4: the point + int height_anchor{0}; // 0: origin, 1: bottom, 2: top, 3: top of the side facing the curve, 4: the point + glm::dvec3 point{0.0}; + glm::dvec3 scale{1.0}; std::vector pieces; bool operator==(state const &Other) const; @@ -101,6 +107,15 @@ class sweep_node : public scene::basic_node double end() const; // length of a copy along the curve, as the model has it double model_length() const; + bool bounds(glm::dvec3 &Low, glm::dvec3 &High, glm::dvec3 &Shift) const; + bool edge_far() const; + glm::dvec3 axes(glm::dvec3 const &Vector) const; + double distance_at(double const Station) const; + double station_at(double const Distance) const; + double setback(frame const &At) const; + // height the copies stand at over the points of the curve, less the height of the rail: the head of the rail on their side, + // for a platform the plane of the rail heads carried on to its edge, lower on the inner side of a canted curve + double rise(frame const &At) const; std::vector create_shapes() const; // a model of what's laid along the curve, placed in its space struct item @@ -115,6 +130,8 @@ class sweep_node : public scene::basic_node // puts the copies in the scene as shapes of their own, which can be taken back void show(); void hide(); + void refresh(); + bool bridge(); void merged(bool const Merged) { m_merged = Merged; @@ -142,8 +159,16 @@ class sweep_node : public scene::basic_node std::array widening; std::array cant; }; + static void sample_piece(segment_data const &Piece, std::vector &Samples, double &Station); + static void gauge_along(std::vector &Samples); + void beside_switches(std::vector> const &Spans); state m_state; std::vector m_samples; + std::vector m_distances; + mutable bool m_boundsvalid{false}; + mutable bool m_boundsfound{false}; + mutable bool m_edgefar{false}; + mutable glm::dvec3 m_low{0.0}, m_high{0.0}, m_shift{0.0}; bool m_merged{false}; owned_shapes m_shapes; };