From 8f2a34b3cfdd813d052b5058edb166e9f8864664 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 2 Oct 2026 00:09:09 +0200 Subject: [PATCH] feat(editor): route design with curves and transitions --- CMakeLists.txt | 1 + application/editormode.cpp | 22 +- application/editormode.h | 39 +- application/editormodetrack.cpp | 557 +++++++++++++++++- editor/editorAlignment.cpp | 696 +++++++++++++++++++++++ editor/editorAlignment.hpp | 184 ++++++ editor/editorTrack.cpp | 245 ++++++++ editor/editorTrack.hpp | 26 + simulation/simulationstateserializer.cpp | 3 + world/Track.h | 1 + 10 files changed, 1758 insertions(+), 16 deletions(-) create mode 100644 editor/editorAlignment.cpp create mode 100644 editor/editorAlignment.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dabcc167..fe5b1f2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -171,6 +171,7 @@ set(SOURCES "editor/editorIncludeInfo.cpp" "editor/editorTerrain.cpp" "editor/editorTrack.cpp" +"editor/editorAlignment.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 0e044433..f6a0a472 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -683,7 +683,7 @@ void editor_mode::undo_last() if (snap.action == EditorSnapshot::Action::TrackEdit) { - restore_track_snapshot(snap, g_redo); + restore_track_snapshot(snap, g_redo, true); return; } @@ -778,7 +778,7 @@ void editor_mode::redo_last() if (snap.action == EditorSnapshot::Action::TrackEdit) { - restore_track_snapshot(snap, m_history); + restore_track_snapshot(snap, m_history, false); return; } @@ -1009,9 +1009,11 @@ bool editor_mode::update() // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); - // --- path editing: course and point handles of the selected path --- + // --- path editing: course and point handles of the selected path, designed route --- if (selected_track()) draw_track_overlay(); + if (route_active()) + draw_route_overlay(); // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) @@ -2243,6 +2245,13 @@ void editor_mode::render_gizmo() return; } + // route design: the gizmo moves the selected vertex of the designed alignment + if (route_active()) + { + render_route_gizmo(); + return; + } + if (!m_node && m_instance == 0) { m_gizmo_using = false; @@ -2534,6 +2543,8 @@ void editor_mode::exit() m_track_drag_points.clear(); m_track_snap = {}; m_track_point = {}; + m_route = {}; + m_route_gizmo_using = false; g_redo.clear(); m_history.clear(); @@ -2681,7 +2692,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_ESCAPE: // track mode: release the selected point handle, so the gizmo moves the whole path again if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) + { m_track_point = {}; + m_route.vertex = -1; + } break; case GLFW_KEY_BACKSPACE: @@ -2775,7 +2789,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const // in track mode the left button selects a point handle of the selected path, or another path if (mode == nodebank_panel::TRACK) { - if (false == ImGuizmo::IsOver() && false == pick_track_handle()) + if (false == ImGuizmo::IsOver() && false == pick_route_vertex() && false == pick_track_handle()) { GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { if (viewport_click()) diff --git a/application/editormode.h b/application/editormode.h index 6dc9320d..597fecbf 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTerrainStreamer.hpp" #include "editor/editorOrthophoto.hpp" #include "editor/editorTrack.hpp" +#include "editor/editorAlignment.hpp" #include #include @@ -104,6 +105,7 @@ class editor_mode : public application_mode scene::instance_handle instance{0}; // include of a scenery template which was placed or removed, instead of a node // TrackEdit: state of all paths modified by the change (the edited path and neighbours dragged along) std::vector> tracks; + std::vector created; // TrackEdit: paths added by the change }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -279,6 +281,8 @@ class editor_mode : public application_mode TTrack *selected_track() const; // settings and parameters of the selected path, drawn in the node bank tab of the track mode void render_track_ui(); + // parameters and points of the selected path + void render_path_ui(); // curve and point handles of the selected path void draw_track_overlay() const; // selects the point handle of the selected path located under the cursor. returns: true if a handle was hit @@ -289,10 +293,10 @@ class editor_mode : public application_mode void render_track_gizmo(); // rebuilds paths modified by the ongoing drag; unless forced, at most a few times per second void commit_track_drag(bool const Force); - // records provided state of paths in the undo history - void push_track_snapshot(std::vector> States); + // records provided state of paths in the undo history, along with paths added by the change + void push_track_snapshot(std::vector> States, std::vector Created = {}); // restores state of paths stored in the history, moving their current state to the opposite stack - void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite); + void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); editor_track::point_ref m_track_point; // selected point handle of the selected path std::vector m_track_drag; // paths modified by the ongoing gizmo drag std::vector> m_track_drag_points; // end points of neighbours dragged along @@ -304,6 +308,35 @@ class editor_mode : public application_mode std::chrono::steady_clock::time_point m_track_last_commit; editor_track::state m_track_field_before; // state of the path before the ongoing field edit std::array, 3> m_track_materials{}; // material names being edited + // route design: re-lays a chain of paths along an alignment designed with intersection points + struct route_design + { + TTrack *from{nullptr}; // first and last path of the re-laid fragment + TTrack *to{nullptr}; + editor_track::chain chain; + std::string error; + std::string status; // outcome of the last application + alignment::design design; + alignment::result result; + int vertex{-1}; // selected vertex + }; + void render_route_ui(); + // finds the chain between the selected fragment ends and proposes an initial design for it + void route_reset(); + void route_update() { m_route.result = alignment::compute(m_route.design); } + void route_apply(); + // sets recommended cant, transitions and vertical curve for the vertex + void route_recommend(alignment::vertex &Vertex) const; + void draw_route_overlay() const; + bool pick_route_vertex(); + void render_route_gizmo(); + // 3d position of the vertex handle + glm::dvec3 route_vertex_position(int const Vertex) const; + bool route_active() const; + route_design m_route; + bool m_route_tab{true}; // route design tab of the track mode is shown + bool m_route_gizmo_using{false}; + glm::mat4 m_route_gizmo{1.0f}; float m_track_snap_radius{1.0f}; bool m_track_align_tangent{true}; bool m_track_drag_connected{true}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index ab283129..44fe6649 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "rendering/renderer.h" #include "world/Track.h" +#include "utilities/Logs.h" #include "imgui/imgui.h" #include "imgui/ImGuizmo.h" @@ -146,7 +147,7 @@ void editor_mode::select_track(scene::basic_node *Node) bool editor_mode::pick_track_handle() { auto *track = selected_track(); - if (track == nullptr || false == editor_track::is_supported(*track)) + if (track == nullptr || false == editor_track::is_supported(*track) || m_route_tab) return false; screen_projection const projection; @@ -201,7 +202,7 @@ void editor_mode::draw_track_overlay() const previous = next; } } - if (false == trackmode || false == editor_track::is_supported(*track)) + if (false == trackmode || m_route_tab || false == editor_track::is_supported(*track)) return; // control vectors @@ -372,7 +373,7 @@ void editor_mode::commit_track_drag(bool const Force) m_track_last_commit = now; } -void editor_mode::push_track_snapshot(std::vector> States) +void editor_mode::push_track_snapshot(std::vector> States, std::vector Created) { if (States.empty()) return; @@ -388,27 +389,49 @@ void editor_mode::push_track_snapshot(std::vectorlocation(); snap.uuid = track->uuid; snap.tracks = std::move(States); + snap.created = std::move(Created); m_history.push_back(std::move(snap)); g_redo.clear(); } -void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite) +void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo) { if (Snapshot.tracks.empty()) return; - EditorSnapshot current{Snapshot}; + // paths restored by this step; paths added by the change exist only on its redo side std::vector tracks; - for (auto &entry : current.tracks) - { + for (auto const &entry : Snapshot.tracks) tracks.emplace_back(entry.first); - entry.second = editor_track::capture(*entry.first); - } + // current state goes to the opposite stack, including paths added by the change when it's being undone + EditorSnapshot current{Snapshot}; + current.tracks.clear(); + auto involved{tracks}; + if (Undo) + involved.insert(involved.end(), Snapshot.created.begin(), Snapshot.created.end()); + for (auto *track : involved) + current.tracks.emplace_back(track, editor_track::capture(*track)); Opposite.push_back(std::move(current)); + if (Undo) + { + for (auto *track : Snapshot.created) + editor_track::retire(*track); + } + else + { + for (auto *track : Snapshot.created) + track->m_editorremoved = false; + } for (auto const &entry : Snapshot.tracks) editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); + // the designed route refers to the chain, which might have changed + if (false == m_route.chain.tracks.empty()) + { + std::string error; + editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error); + } m_track_snap = {}; m_track_dirty = false; @@ -419,6 +442,26 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve } void editor_mode::render_track_ui() +{ + if (ImGui::BeginTabBar("##trackediting")) + { + if (ImGui::BeginTabItem("Route design")) + { + m_route_tab = true; + render_route_ui(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Single path")) + { + m_route_tab = false; + render_path_ui(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } +} + +void editor_mode::render_path_ui() { ImGui::TextDisabled("LMB: select path or point handle Esc: release point\nGizmo moves the selected point, otherwise the whole path (W: turn)"); auto *track = selected_track(); @@ -611,3 +654,499 @@ void editor_mode::render_track_ui() } } } + +// route design + +bool editor_mode::route_active() const +{ + return ui()->mode() == nodebank_panel::TRACK && m_route_tab && false == m_route.chain.tracks.empty(); +} + +void editor_mode::route_recommend(alignment::vertex &Vertex) const +{ + auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, m_route.design.norms)}; + Vertex.cant = recommended.cant; + Vertex.transition_in = recommended.transition; + Vertex.transition_out = recommended.transition; + Vertex.vertical_radius = 0.0; +} + +void editor_mode::route_reset() +{ + auto &route{m_route}; + route.chain = {}; + route.error.clear(); + route.status.clear(); + route.vertex = -1; + route.result = {}; + if (route.from == nullptr || route.to == nullptr) + return; + if (false == editor_track::find_chain(route.from, route.to, route.chain, route.error)) + return; + + auto const &chain{route.chain}; + alignment::design design; + // limits and the curve type are the user's choice, they carry over to the next fragment + design.norms = route.design.norms; + design.shape = route.design.shape; + design.start = chain.start; + design.end = chain.end; + auto const plan = [](glm::dvec3 const &Direction) { return glm::normalize(glm::dvec2{Direction.x, Direction.z}); }; + auto const slope = [](glm::dvec3 const &Direction) { return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); }; + design.start_direction = plan(chain.start_direction); + design.end_direction = plan(chain.end_direction); + design.start_grade = slope(chain.start_direction); + design.end_grade = slope(chain.end_direction); + design.start_radius = chain.start_radius; + design.end_radius = chain.end_radius; + design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; + route.design = design; + + // initial layout: a straight, a single curve at the intersection of the end tangents, or a pair of curves + glm::dvec2 intersection; + if (false == alignment::collinear(design)) + { + alignment::vertex vertex; + vertex.radius = chain.radius > 0.0 ? std::max(10.0, std::round(chain.radius / 10.0) * 10.0) : 1000.0; + route_recommend(vertex); + if (alignment::tangent_intersection(design, intersection)) + { + route.design.vertices.push_back(vertex); + } + else + { + auto const distance{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; + vertex.offset = distance / 3.0; + route.design.vertices.push_back(vertex); + route.design.vertices.push_back(vertex); + } + } + route_update(); +} + +void editor_mode::route_apply() +{ + auto &route{m_route}; + route.status.clear(); + if (false == route.result.valid || route.chain.tracks.empty()) + return; + // the paths might have changed since the fragment was selected + editor_track::chain chain; + if (false == editor_track::find_chain(route.from, route.to, chain, route.error)) + return; + if (glm::distance(chain.start, route.chain.start) > 1e-3 || glm::distance(chain.end, route.chain.end) > 1e-3) + { + route.error = "The fragment changed since it was selected, select it again"; + return; + } + for (auto *track : chain.tracks) + { + std::string reason; + if (false == editor_track::can_edit_geometry(*track, reason)) + { + route.error = track->name() + ": " + reason; + return; + } + } + + auto const pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + std::vector> states; + for (auto *track : chain.tracks) + states.emplace_back(track, editor_track::capture(*track)); + auto created{editor_track::relay(chain, pieces)}; + auto const added{created.size()}; + push_track_snapshot(std::move(states), std::move(created)); + + editor_track::find_chain(route.from, route.to, route.chain, route.error); + route.status = "Re-laid " + std::to_string(chain.tracks.size()) + " paths" + (added > 0 ? ", added " + std::to_string(added) : std::string{}); + WriteLog("Editor: route design - " + route.status, logtype::generic); +} + +glm::dvec3 editor_mode::route_vertex_position(int const Vertex) const +{ + auto const &design{m_route.design}; + auto const &result{m_route.result}; + auto const count{static_cast(design.vertices.size())}; + glm::dvec2 const start{design.start.x, design.start.z}; + glm::dvec2 const end{design.end.x, design.end.z}; + glm::dvec2 position; + if (Vertex < static_cast(result.vertices.size()) && false == result.vertices.empty()) + position = result.vertices[Vertex]; + else if (count == 1) + { + if (false == alignment::tangent_intersection(design, position)) + position = (start + end) * 0.5; + } + else if (Vertex == 0) + position = start + glm::normalize(design.start_direction) * design.vertices.front().offset; + else if (Vertex == count - 1) + position = end - glm::normalize(design.end_direction) * design.vertices.back().offset; + else + position = design.vertices[Vertex].position; + + double height; + if (Vertex < static_cast(result.vertex_elevations.size())) + height = result.vertex_elevations[Vertex]; + else if (false == design.vertices[Vertex].auto_elevation) + height = design.vertices[Vertex].elevation; + else + height = design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0); + return {position.x, height, position.y}; +} + +bool editor_mode::pick_route_vertex() +{ + if (false == route_active()) + return false; + screen_projection const projection; + ImVec2 const mouse = ImGui::GetIO().MousePos; + float best = kHandleRadius * kHandleRadius; + int hit{-1}; + for (int i = 0; i < static_cast(m_route.design.vertices.size()); ++i) + { + ImVec2 screen; + if (false == projection.project(route_vertex_position(i), screen)) + continue; + float const dx = screen.x - mouse.x; + float const dy = screen.y - mouse.y; + if (dx * dx + dy * dy < best) + { + best = dx * dx + dy * dy; + hit = i; + } + } + if (hit < 0) + return false; + m_route.vertex = hit; + return true; +} + +void editor_mode::draw_route_overlay() const +{ + auto const &route{m_route}; + auto const &design{route.design}; + auto const &result{route.result}; + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + + // polygon of tangents + std::vector polygon{design.start}; + for (int i = 0; i < static_cast(design.vertices.size()); ++i) + polygon.push_back(route_vertex_position(i)); + polygon.push_back(design.end); + for (std::size_t i = 0; i + 1 < polygon.size(); ++i) + projection.line(drawlist, polygon[i], polygon[i + 1], IM_COL32(255, 210, 60, 150), 1.5f); + + // designed alignment, coloured by element: straight white, transition orange, circular curve green + if (result.length > 0.0 && result.profile.size() >= 2) + { + for (auto const &e : result.elements) + { + ImU32 const color = e.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : e.kind == alignment::element_kind::arc ? IM_COL32(60, 230, 90, 255) : IM_COL32(255, 140, 30, 255); + int const steps = std::clamp(static_cast(e.length / 2.0), 1, 400); + auto previous{alignment::evaluate(result, e.chainage).position}; + for (int j = 1; j <= steps; ++j) + { + auto const next{alignment::evaluate(result, e.chainage + e.length * j / steps).position}; + projection.line(drawlist, previous, next, color, 3.0f); + previous = next; + } + ImVec2 screen; + if (projection.project(alignment::evaluate(result, e.chainage).position, screen)) + drawlist->AddCircleFilled(screen, 3.5f, IM_COL32(255, 255, 255, 255)); + } + } + + // fixed ends with their directions, and the vertices + for (auto const atend : {false, true}) + { + auto const &point{atend ? design.end : design.start}; + auto const direction{atend ? design.end_direction : design.start_direction}; + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddRectFilled(ImVec2(screen.x - 5.0f, screen.y - 5.0f), ImVec2(screen.x + 5.0f, screen.y + 5.0f), IM_COL32(70, 140, 255, 255)); + auto const arrow{glm::dvec3{direction.x, 0.0, direction.y} * (atend ? 15.0 : -15.0)}; + projection.line(drawlist, point, point + arrow, IM_COL32(70, 140, 255, 255), 2.0f); + } + for (int i = 0; i < static_cast(design.vertices.size()); ++i) + { + ImVec2 screen; + if (false == projection.project(route_vertex_position(i), screen)) + continue; + drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(255, 210, 60, 255)); + if (i == route.vertex) + drawlist->AddCircle(screen, 11.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f); + char label[8]; + std::snprintf(label, sizeof(label), "W%d", i + 1); + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), IM_COL32(255, 210, 60, 255), label); + } +} + +void editor_mode::render_route_gizmo() +{ + auto &route{m_route}; + auto const count{static_cast(route.design.vertices.size())}; + if (route.vertex < 0 || route.vertex >= count || false == m_gizmo_enabled) + { + m_route_gizmo_using = false; + return; + } + + ImGuizmo::BeginFrame(); + ImGuizmo::SetOrthographic(false); + ImGuiIO const &io = ImGui::GetIO(); + ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); + glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); + glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); + float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor); + float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; + glm::mat4 const projection = glm::perspective(fovy, aspect, 0.1f, 10000.0f); + + auto const position{route_vertex_position(route.vertex)}; + if (false == m_route_gizmo_using) + m_route_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(position - camerapos)); + glm::vec3 snapvalue(m_gizmo_snap); + float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; + ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, glm::value_ptr(m_route_gizmo), nullptr, snap); + + if (false == ImGuizmo::IsUsing()) + { + m_route_gizmo_using = false; + return; + } + m_route_gizmo_using = true; + glm::dvec3 const moved{camerapos + glm::dvec3(m_route_gizmo[3])}; + auto &vertex{route.design.vertices[route.vertex]}; + glm::dvec2 const point{moved.x, moved.z}; + // the first and the last vertex slide along the tangents of the fixed ends; a single vertex stays at their intersection + if (count >= 2) + { + if (route.vertex == 0) + vertex.offset = std::max(0.1, glm::dot(point - glm::dvec2{route.design.start.x, route.design.start.z}, glm::normalize(route.design.start_direction))); + else if (route.vertex == count - 1) + vertex.offset = std::max(0.1, glm::dot(glm::dvec2{route.design.end.x, route.design.end.z} - point, glm::normalize(route.design.end_direction))); + else + vertex.position = point; + } + if (std::abs(moved.y - position.y) > 1e-3) + { + vertex.auto_elevation = false; + vertex.elevation = moved.y; + } + route_update(); +} + +void editor_mode::render_route_ui() +{ + auto &route{m_route}; + auto &design{route.design}; + ImGui::TextDisabled("Re-lays a chain of regular paths along an alignment designed with intersection points.\n" + "The outer ends of the chain and their directions stay fixed.\n" + "LMB: select a path or a vertex (W1, W2...) Esc: release the vertex"); + + auto *track = selected_track(); + auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : (Track->name().empty() ? std::string{"(noname)"} : Track->name()); }; + ImGui::Text("First path: %s", name(route.from).c_str()); + ImGui::SameLine(ImGui::GetWindowContentRegionWidth() - 110.0f); + if (ImGui::Button("Use selected##routefrom", ImVec2(110.0f, 0.0f)) && track != nullptr) + { + route.from = track; + if (route.to == nullptr) + route.to = track; + route_reset(); + } + ImGui::Text("Last path: %s", name(route.to).c_str()); + ImGui::SameLine(ImGui::GetWindowContentRegionWidth() - 110.0f); + if (ImGui::Button("Use selected##routeto", ImVec2(110.0f, 0.0f)) && track != nullptr) + { + route.to = track; + if (route.from == nullptr) + route.from = track; + route_reset(); + } + if (false == route.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); + if (route.chain.tracks.empty()) + return; + + ImGui::Text("Fragment: %zu paths, %.2f m", route.chain.tracks.size(), route.chain.length); + ImGui::SameLine(); + if (ImGui::SmallButton("Reset design")) + { + route_reset(); + return; + } + + bool changed{false}; + ImGui::PushItemWidth(120.0f); + changed |= ImGui::InputDouble("Design speed (km/h)", &design.speed, 0.0, 0.0, "%.0f"); + design.speed = std::max(1.0, design.speed); + int shape = static_cast(design.shape); + char const *shapes[] = {"Cubic parabola", "Clothoid"}; + if (ImGui::Combo("Transition curve", &shape, shapes, IM_ARRAYSIZE(shapes))) + { + design.shape = static_cast(shape); + changed = true; + } + ImGui::PopItemWidth(); + + if (ImGui::TreeNode("Limits")) + { + auto &norms{design.norms}; + ImGui::PushItemWidth(100.0f); + changed |= ImGui::InputDouble("Rail axes distance (mm)", &norms.gauge, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Unbalanced acceleration (m/s2)", &norms.unbalanced, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Maximum cant (mm)", &norms.cant_max, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Cant change rate (mm/s)", &norms.cant_rate, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Cant ramp 1:(k*V), k", &norms.ramp_factor, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble("Unbalanced acc. change rate (m/s3)", &norms.jerk, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Vertical curve R >= k*V^2, k", &norms.vertical_factor, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Vertical curve R min (m)", &norms.vertical_min, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Straight between curves (s of travel)", &norms.tangent_min_time, 0.0, 0.0, "%.1f"); + ImGui::PopItemWidth(); + norms.gauge = std::max(100.0, norms.gauge); + norms.cant_rate = std::max(1.0, norms.cant_rate); + norms.jerk = std::max(0.01, norms.jerk); + ImGui::TreePop(); + } + + // vertices + auto &vertices{design.vertices}; + if (ImGui::Button("Add vertex")) + { + alignment::vertex vertex; + vertex.radius = vertices.empty() ? 1000.0 : vertices.back().radius; + route_recommend(vertex); + auto const distance{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; + if (vertices.size() == 1) + { + // a single vertex sits at the intersection of the tangents; two vertices slide along them + vertices.front().offset = distance / 3.0; + vertex.offset = distance / 3.0; + vertices.push_back(vertex); + } + else if (vertices.empty()) + { + vertices.push_back(vertex); + } + else + { + // new vertex between the selected one (or the last but one) and the next + auto const index{std::clamp(route.vertex, 0, static_cast(vertices.size()) - 2)}; + auto const a{route_vertex_position(index)}; + auto const b{route_vertex_position(index + 1)}; + vertex.position = glm::dvec2{(a.x + b.x) * 0.5, (a.z + b.z) * 0.5}; + // the moved vertices keep their place, so the former first/last vertex positions become free positions + for (int i = 0; i < static_cast(vertices.size()); ++i) + { + auto const p{route_vertex_position(i)}; + vertices[i].position = {p.x, p.z}; + } + vertices.insert(vertices.begin() + index + 1, vertex); + } + changed = true; + } + ImGui::SameLine(); + if (ImGui::Button("Remove vertex") && false == vertices.empty()) + { + auto const index{route.vertex >= 0 && route.vertex < static_cast(vertices.size()) ? route.vertex : static_cast(vertices.size()) - 1}; + vertices.erase(vertices.begin() + index); + route.vertex = -1; + changed = true; + } + + auto const count{static_cast(vertices.size())}; + for (int i = 0; i < count; ++i) + { + auto &vertex{vertices[i]}; + ImGui::PushID(i); + char header[32]; + std::snprintf(header, sizeof(header), "Vertex W%d", i + 1); + if (ImGui::Selectable(header, route.vertex == i)) + route.vertex = (route.vertex == i ? -1 : i); + ImGui::Indent(); + ImGui::PushItemWidth(110.0f); + if (count == 1) + ImGui::TextDisabled("At the intersection of the end tangents"); + else if (i == 0) + changed |= ImGui::InputDouble("Distance from the start (m)", &vertex.offset, 0.0, 0.0, "%.2f"); + else if (i == count - 1) + changed |= ImGui::InputDouble("Distance from the end (m)", &vertex.offset, 0.0, 0.0, "%.2f"); + else + { + ImGui::PushItemWidth(220.0f); + changed |= ImGui::InputScalarN("Position (x, z)", ImGuiDataType_Double, glm::value_ptr(vertex.position), 2, nullptr, nullptr, "%.2f"); + ImGui::PopItemWidth(); + } + changed |= ImGui::InputDouble("Radius R (m)", &vertex.radius, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble("Transition in (m)", &vertex.transition_in, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble("Transition out (m)", &vertex.transition_out, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble("Cant (mm)", &vertex.cant, 0.0, 0.0, "%.0f"); + changed |= ImGui::Checkbox("Elevation from the ends", &vertex.auto_elevation); + if (false == vertex.auto_elevation) + changed |= ImGui::InputDouble("Elevation (m)", &vertex.elevation, 0.0, 0.0, "%.3f"); + changed |= ImGui::InputDouble("Vertical curve R (0: min)", &vertex.vertical_radius, 0.0, 0.0, "%.0f"); + ImGui::PopItemWidth(); + vertex.radius = std::max(1.0, vertex.radius); + vertex.transition_in = std::max(0.0, vertex.transition_in); + vertex.transition_out = std::max(0.0, vertex.transition_out); + vertex.cant = std::max(0.0, vertex.cant); + vertex.vertical_radius = std::max(0.0, vertex.vertical_radius); + if (ImGui::SmallButton("Apply recommended for V")) + { + route_recommend(vertex); + changed = true; + } + + auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; + ImGui::TextDisabled("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m", design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); + for (auto const &curve : route.result.curves) + { + if (curve.vertex != i) + continue; + ImGui::Text("Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m", glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length); + ImGui::Text("Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3", curve.unbalanced, curve.cant_rate_in, curve.cant_rate_out, curve.jerk_in, curve.jerk_out); + } + ImGui::Unindent(); + ImGui::PopID(); + } + if (changed) + route_update(); + + // outcome + ImGui::Separator(); + auto const &result{route.result}; + if (result.length > 0.0) + { + auto const pieces{alignment::minimum_pieces(result, design)}; + ImGui::Text("Length %.2f m (now %.2f m), %zu elements", result.length, route.chain.length, result.elements.size()); + ImGui::TextDisabled("Paths: %zu in the fragment, %zu needed%s", route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? " (copies of the neighbours will be added)" : ""); + } + for (auto const &error : result.errors) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", error.c_str()); + for (auto const &warning : result.warnings) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", warning.c_str()); + + if (ImGui::TreeNode("Elements")) + { + char const *kinds[] = {"straight", "transition", "curve", "transition"}; + for (auto const &e : result.elements) + ImGui::Text("%8.2f m %-10s L %8.2f m%s", e.chainage, kinds[static_cast(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast(std::round(e.radius))) + " m").c_str()); + ImGui::TreePop(); + } + + if (result.valid) + { + if (ImGui::Button("Apply to the scenery")) + route_apply(); + } + else + { + ImGui::TextDisabled("Correct the errors to apply the design"); + } + if (false == route.status.empty()) + { + ImGui::SameLine(); + ImGui::TextUnformatted(route.status.c_str()); + } +} diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp new file mode 100644 index 00000000..82ad834a --- /dev/null +++ b/editor/editorAlignment.cpp @@ -0,0 +1,696 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorAlignment.hpp" + +#include +#include +#include +#include +#include + +namespace alignment +{ + +namespace +{ + +double const kGravity{9.81}; +double const kPi{3.14159265358979323846}; + +double cross(glm::dvec2 const &A, glm::dvec2 const &B) +{ + return A.x * B.y - A.y * B.x; +} + +// vector rotated by 90 degrees counter-clockwise +glm::dvec2 perpendicular(glm::dvec2 const &Vector) +{ + return {-Vector.y, Vector.x}; +} + +glm::dvec2 rotate(glm::dvec2 const &Vector, double const Angle) +{ + auto const c{std::cos(Angle)}; + auto const s{std::sin(Angle)}; + return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; +} + +glm::dvec2 plan(glm::dvec3 const &Point) +{ + return {Point.x, Point.z}; +} + +double ceil_to(double const Value, double const Step) +{ + return std::ceil(Value / Step - 1e-9) * Step; +} + +std::string format(char const *Format, ...) +{ + char buffer[512]; + va_list arguments; + va_start(arguments, Format); + std::vsnprintf(buffer, sizeof(buffer), Format, arguments); + va_end(arguments); + return buffer; +} + +// point of a transition curve in its local frame: x along the tangent at its start, y towards the curve centre +struct local_point +{ + double x{0.0}; + double y{0.0}; + double angle{0.0}; // tangent angle relative to the initial tangent +}; + +// Parameter is the fraction of the transition length, from the straight (0) to the circular curve (1) +local_point transition_point(transition_shape const Shape, double const Length, double const Radius, double const Parameter) +{ + if (Length <= 0.0 || Radius <= 0.0) + return {}; + if (Shape == transition_shape::cubic_parabola) + { + auto const x{Length * Parameter}; + return {x, x * x * x / (6.0 * Radius * Length), std::atan(x * x / (2.0 * Radius * Length))}; + } + // clothoid, tangent angle grows with the square of the travelled distance + auto const distance{Length * Parameter}; + auto const angle = [&](double const S) { return S * S / (2.0 * Radius * Length); }; + int const steps{std::max(8, static_cast(std::ceil(distance / 0.5)) * 2)}; + double const h{distance / steps}; + double x{0.0}, y{0.0}; + for (int i = 0; i <= steps; ++i) + { + double const weight = (i == 0 || i == steps) ? 1.0 : (i % 2 ? 4.0 : 2.0); + auto const a{angle(i * h)}; + x += weight * std::cos(a); + y += weight * std::sin(a); + } + return {x * h / 3.0, y * h / 3.0, angle(distance)}; +} + +// plan position and direction of travel at given distance from the start of the element +void element_point(element const &Element, transition_shape const Shape, double const Distance, glm::dvec2 &Position, glm::dvec2 &Direction) +{ + switch (Element.kind) + { + case element_kind::straight: + Position = Element.origin + Element.direction * Distance; + Direction = Element.direction; + break; + case element_kind::arc: + { + auto const angle{Element.turn * Distance / Element.radius}; + Position = Element.origin + rotate(Element.normal, angle); + Direction = rotate(Element.direction, angle); + break; + } + case element_kind::transition_in: + { + auto const local{transition_point(Shape, Element.transition, Element.radius, Distance / Element.transition)}; + Position = Element.origin + Element.direction * local.x + Element.normal * local.y; + Direction = Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle); + break; + } + case element_kind::transition_out: + { + auto const local{transition_point(Shape, Element.transition, Element.radius, 1.0 - Distance / Element.transition)}; + Position = Element.origin + Element.direction * local.x + Element.normal * local.y; + Direction = -(Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle)); + break; + } + } +} + +double element_cant(element const &Element, double const Distance) +{ + switch (Element.kind) + { + case element_kind::transition_in: return Element.cant * Distance / Element.transition; + case element_kind::transition_out: return Element.cant * (1.0 - Distance / Element.transition); + case element_kind::arc: return Element.cant; + default: return 0.0; + } +} + +element const &element_at(result const &Result, double const Chainage) +{ + auto const lookup = std::upper_bound(Result.elements.begin(), Result.elements.end(), Chainage, [](double const Value, element const &Element) { return Value < Element.chainage; }); + return lookup == Result.elements.begin() ? Result.elements.front() : *std::prev(lookup); +} + +// polar form of a cubic Bezier curve; control points of the part between parameters a and b are f(a,a,a), f(a,a,b), f(a,b,b), f(b,b,b) +glm::dvec2 blossom(std::array const &Points, double const A, double const B, double const C) +{ + glm::dvec2 r[3], s[2]; + for (int i = 0; i < 3; ++i) + r[i] = glm::mix(Points[i], Points[i + 1], A); + for (int i = 0; i < 2; ++i) + s[i] = glm::mix(r[i], r[i + 1], B); + return glm::mix(s[0], s[1], C); +} + +// number of cubic pieces needed to represent the element accurately +std::size_t element_pieces(element const &Element, transition_shape const Shape) +{ + if (Element.length < 1e-3) + return 0; + switch (Element.kind) + { + case element_kind::arc: + // cubic approximation of a 10 degree arc deviates from the circle by less than 1e-7 of its radius + return static_cast(std::max(1.0, std::ceil(Element.length / Element.radius / (10.0 * kPi / 180.0) - 1e-9))); + case element_kind::transition_in: + case element_kind::transition_out: + return Shape == transition_shape::cubic_parabola ? 1 : static_cast(std::max(1.0, std::ceil(Element.length / 20.0 - 1e-9))); + default: + return 1; + } +} + +} // namespace + +double unbalanced_acceleration(double const Speed, double const Radius, double const Cant, limits const &Limits) +{ + if (Radius <= 0.0) + return 0.0; + return Speed * Speed / (12.96 * Radius) - kGravity * Cant / Limits.gauge; +} + +double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits) +{ + double length{0.0}; + if (Cant > 0.0) + { + // rate of cant change in time, and the inclination of the cant ramp + length = std::max(length, Speed * Cant / (3.6 * Limits.cant_rate)); + length = std::max(length, Cant / 1000.0 * Limits.ramp_factor * Speed); + } + // rate of change of the unbalanced acceleration + auto const unbalanced{std::max(0.0, unbalanced_acceleration(Speed, Radius, Cant, Limits))}; + length = std::max(length, unbalanced * Speed / (3.6 * Limits.jerk)); + return length > 0.0 ? ceil_to(length, 5.0) : 0.0; +} + +recommendation recommend(double const Speed, double const Radius, limits const &Limits) +{ + recommendation result; + auto const factor{Limits.gauge / (kGravity * 12.96)}; // ~11.8 for standard gauge + auto const allowance{Limits.gauge * Limits.unbalanced / kGravity}; + result.radius_min = factor * Speed * Speed / (Limits.cant_max + allowance); + result.vertical_radius = std::max(Limits.vertical_min, Limits.vertical_factor * Speed * Speed); + if (Radius <= 0.0) + return result; + result.cant_equilibrium = factor * Speed * Speed / Radius; + result.cant_min = std::max(0.0, result.cant_equilibrium - allowance); + // keep a part of the permissible unbalanced acceleration in reserve, but never go below the required minimum + result.cant = std::min(Limits.cant_max, ceil_to(std::max(result.cant_min, result.cant_equilibrium * 2.0 / 3.0), 5.0)); + result.transition = transition_length(Speed, Radius, result.cant, Limits); + return result; +} + +result compute(design const &Design) +{ + result r; + r.shape = Design.shape; + r.gauge = Design.norms.gauge; + + auto const start{plan(Design.start)}; + auto const end{plan(Design.end)}; + auto const startdirection{glm::normalize(Design.start_direction)}; + auto const enddirection{glm::normalize(Design.end_direction)}; + auto const count{Design.vertices.size()}; + + // resolve vertex positions; the first and the last vertex lie on the tangents of the fixed ends + r.vertices.resize(count); + if (count == 0) + { + auto const offset{end - start}; + if (std::abs(cross(startdirection, offset)) > 0.01 || glm::dot(startdirection, enddirection) < std::cos(1e-4) || glm::dot(startdirection, offset) <= 0.0) + r.errors.emplace_back("The ends don't lie on a common straight line, add a vertex"); + } + else if (count == 1) + { + auto const offset{end - start}; + auto const determinant{cross(startdirection, enddirection)}; + if (std::abs(determinant) < 1e-9) + { + r.errors.emplace_back("Tangents of the ends are parallel, use two vertices"); + } + else + { + auto const t{cross(offset, enddirection) / determinant}; + auto const u{cross(startdirection, offset) / determinant}; + if (t <= 0.0 || u <= 0.0) + r.errors.emplace_back("Tangents of the ends intersect outside of the fragment, use two vertices"); + r.vertices[0] = start + startdirection * t; + } + } + else + { + for (std::size_t i = 1; i + 1 < count; ++i) + r.vertices[i] = Design.vertices[i].position; + r.vertices.front() = start + startdirection * Design.vertices.front().offset; + r.vertices.back() = end - enddirection * Design.vertices.back().offset; + } + if (false == r.errors.empty()) + return r; + + std::vector polygon{start}; + polygon.insert(polygon.end(), r.vertices.begin(), r.vertices.end()); + polygon.push_back(end); + for (std::size_t i = 0; i + 1 < polygon.size(); ++i) + { + if (glm::distance(polygon[i], polygon[i + 1]) < 1e-3) + { + r.errors.emplace_back(format("Tangent %zu has zero length", i + 1)); + return r; + } + } + + double chainage{0.0}; + auto const push = [&](element Element) { + Element.chainage = chainage; + chainage += Element.length; + r.elements.push_back(Element); + }; + auto const push_straight = [&](glm::dvec2 const &From, glm::dvec2 const &Direction, double const Length) { + if (Length <= 1e-6) + return; + element straight; + straight.kind = element_kind::straight; + straight.origin = From; + straight.direction = Direction; + straight.length = Length; + push(straight); + }; + + std::vector vertexchainage(count, 0.0); + glm::dvec2 cursor{start}; + for (std::size_t k = 0; k < count; ++k) + { + auto const &vertex{Design.vertices[k]}; + auto const &position{polygon[k + 1]}; + auto const directionin{glm::normalize(polygon[k + 1] - polygon[k])}; + auto const directionout{glm::normalize(polygon[k + 2] - polygon[k + 1])}; + auto const turn{cross(directionin, directionout)}; + auto const deflection{std::atan2(std::abs(turn), glm::dot(directionin, directionout))}; + + auto const lead{glm::dot(position - cursor, directionin)}; + if (deflection < 1e-7) + { + // tangents are collinear, there's no curve to fit + if (lead < -1e-4) + r.errors.emplace_back(format("Vertex %zu lies inside the curve of the previous vertex", k + 1)); + push_straight(cursor, directionin, lead); + vertexchainage[k] = chainage; + cursor = position; + continue; + } + if (deflection > kPi - 1e-3) + { + r.errors.emplace_back(format("Vertex %zu: the route turns back on itself", k + 1)); + return r; + } + if (vertex.radius < 1.0) + { + r.errors.emplace_back(format("Vertex %zu: radius is too small", k + 1)); + return r; + } + + int const side{turn > 0.0 ? 1 : -1}; + auto const radius{vertex.radius}; + auto const lengthin{std::max(0.0, vertex.transition_in)}; + auto const lengthout{std::max(0.0, vertex.transition_out)}; + auto const endin{transition_point(Design.shape, lengthin, radius, 1.0)}; + auto const endout{transition_point(Design.shape, lengthout, radius, 1.0)}; + // shift of the circular curve towards its centre, and position of the shifted curve start along the tangent + auto const shiftin{endin.y - radius * (1.0 - std::cos(endin.angle))}; + auto const shiftout{endout.y - radius * (1.0 - std::cos(endout.angle))}; + auto const middlein{endin.x - radius * std::sin(endin.angle)}; + auto const middleout{endout.x - radius * std::sin(endout.angle)}; + + // the centre lies at distances R + shift from both tangents + auto const normalin{perpendicular(directionin) * static_cast(side)}; + auto const normalout{perpendicular(directionout) * static_cast(side)}; + auto const a{radius + shiftin}; + auto const b{radius + shiftout}; + auto const determinant{cross(normalin, normalout)}; + glm::dvec2 const centre{position + glm::dvec2{(a * normalout.y - normalin.y * b) / determinant, (normalin.x * b - a * normalout.x) / determinant}}; + auto const curvestart{centre - normalin * a - directionin * middlein}; + auto const curveend{centre - normalout * b + directionout * middleout}; + auto const arcangle{deflection - endin.angle - endout.angle}; + if (arcangle < -1e-9) + { + r.errors.emplace_back(format("Vertex %zu: transition curves are too long for the deflection angle", k + 1)); + return r; + } + + auto const straight{glm::dot(curvestart - cursor, directionin)}; + if (straight < -1e-4) + { + if (k == 0) + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight)); + else + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); + } + push_straight(cursor, directionin, straight); + + element curve; + curve.vertex = static_cast(k); + curve.radius = radius; + curve.turn = side; + curve.cant = std::max(0.0, vertex.cant); + if (lengthin > 0.0) + { + curve.kind = element_kind::transition_in; + curve.transition = curve.length = lengthin; + curve.origin = curvestart; + curve.direction = directionin; + curve.normal = normalin; + push(curve); + } + auto const arcstart{curvestart + directionin * endin.x + normalin * endin.y}; + curve.kind = element_kind::arc; + curve.transition = 0.0; + curve.length = radius * std::max(0.0, arcangle); + curve.origin = centre; + curve.normal = arcstart - centre; + curve.direction = rotate(directionin, side * endin.angle); + vertexchainage[k] = chainage + curve.length * 0.5; + if (curve.length > 1e-6) + push(curve); + if (lengthout > 0.0) + { + curve.kind = element_kind::transition_out; + curve.transition = curve.length = lengthout; + curve.origin = curveend; + curve.direction = -directionout; + curve.normal = normalout; + push(curve); + } + cursor = curveend; + + curve_report report; + report.vertex = static_cast(k); + report.deflection = deflection; + report.arc_length = radius * std::max(0.0, arcangle); + report.tangent_in = glm::distance(position, curvestart); + report.tangent_out = glm::distance(position, curveend); + report.unbalanced = unbalanced_acceleration(Design.speed, radius, curve.cant, Design.norms); + auto const speed{Design.speed / 3.6}; + if (lengthin > 0.0) + { + report.cant_rate_in = curve.cant * speed / lengthin; + report.jerk_in = std::max(0.0, report.unbalanced) * speed / lengthin; + } + if (lengthout > 0.0) + { + report.cant_rate_out = curve.cant * speed / lengthout; + report.jerk_out = std::max(0.0, report.unbalanced) * speed / lengthout; + } + report.recommended = recommend(Design.speed, radius, Design.norms); + r.curves.push_back(report); + + // verification against the limits + auto const &norms{Design.norms}; + if (radius < report.recommended.radius_min - 1e-6) + r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, radius, report.recommended.radius_min, Design.speed)); + if (curve.cant > norms.cant_max + 1e-6) + r.warnings.emplace_back(format("Vertex %zu: cant %.0f mm exceeds %.0f mm", k + 1, curve.cant, norms.cant_max)); + if (report.unbalanced > norms.unbalanced + 1e-6) + r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2 (cant at least %.0f mm)", k + 1, report.unbalanced, norms.unbalanced, report.recommended.cant_min)); + auto const required{transition_length(Design.speed, radius, curve.cant, norms)}; + if (lengthin + 1e-6 < required || lengthout + 1e-6 < required) + r.warnings.emplace_back(format("Vertex %zu: transition curves shorter than the required %.0f m", k + 1, required)); + if (curve.cant > 0.0 && (lengthin <= 0.0 || lengthout <= 0.0)) + r.warnings.emplace_back(format("Vertex %zu: cant without a transition curve to ramp it", k + 1)); + } + auto const lastdirection{glm::normalize(polygon.back() - polygon[polygon.size() - 2])}; + auto const tail{glm::dot(end - cursor, lastdirection)}; + if (tail < -1e-4) + r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail)); + push_straight(cursor, lastdirection, tail); + r.length = chainage; + + // straights between curves, measured as travel time + if (Design.norms.tangent_min_time > 0.0) + { + auto const minimum{Design.norms.tangent_min_time * Design.speed / 3.6}; + for (std::size_t i = 1; i + 1 < r.elements.size(); ++i) + { + auto const &e{r.elements[i]}; + if (e.kind == element_kind::straight && e.length < minimum && r.elements[i - 1].kind != element_kind::straight && r.elements[i + 1].kind != element_kind::straight) + r.warnings.emplace_back(format("Straight between curves is %.1f m long, less than %.0f m", e.length, minimum)); + } + } + + // vertical profile: grades between the fixed ends and the vertex elevations, joined with vertical curves + r.vertex_chainages = vertexchainage; + r.profile.push_back({0.0, Design.start.y, 0.0, 0.0}); + for (std::size_t k = 0; k < count; ++k) + { + auto const &vertex{Design.vertices[k]}; + auto const elevation{vertex.auto_elevation ? Design.start.y + (Design.end.y - Design.start.y) * vertexchainage[k] / std::max(r.length, 1e-6) : vertex.elevation}; + r.vertex_elevations.push_back(elevation); + auto const radius{vertex.vertical_radius > 0.0 ? vertex.vertical_radius : std::max(Design.norms.vertical_min, Design.norms.vertical_factor * Design.speed * Design.speed)}; + if (vertexchainage[k] <= r.profile.back().chainage + 1e-3) + continue; // vertex without its own place in the profile + r.profile.push_back({vertexchainage[k], elevation, radius, 0.0}); + } + r.profile.push_back({r.length, Design.end.y, 0.0, 0.0}); + for (std::size_t i = 1; i + 1 < r.profile.size(); ++i) + { + auto &point{r.profile[i]}; + auto const gradebefore{(point.elevation - r.profile[i - 1].elevation) / (point.chainage - r.profile[i - 1].chainage)}; + auto const gradeafter{(r.profile[i + 1].elevation - point.elevation) / (r.profile[i + 1].chainage - point.chainage)}; + point.tangent = point.radius * std::abs(gradeafter - gradebefore) / 2.0; + } + for (std::size_t i = 0; i + 1 < r.profile.size(); ++i) + { + if (r.profile[i].tangent + r.profile[i + 1].tangent > r.profile[i + 1].chainage - r.profile[i].chainage + 1e-6) + r.warnings.emplace_back(format("Vertical curves overlap between chainage %.0f m and %.0f m", r.profile[i].chainage, r.profile[i + 1].chainage)); + } + if (r.profile.size() >= 2) + { + auto const first{(r.profile[1].elevation - r.profile[0].elevation) / (r.profile[1].chainage - r.profile[0].chainage)}; + auto const last{(r.profile.back().elevation - r.profile[r.profile.size() - 2].elevation) / (r.profile.back().chainage - r.profile[r.profile.size() - 2].chainage)}; + if (std::abs(first - Design.start_grade) > 0.0005) + r.warnings.emplace_back(format("Grade break at the start: %.1f per mille against %.1f of the adjoining track", first * 1000.0, Design.start_grade * 1000.0)); + if (std::abs(last - Design.end_grade) > 0.0005) + r.warnings.emplace_back(format("Grade break at the end: %.1f per mille against %.1f of the adjoining track", last * 1000.0, Design.end_grade * 1000.0)); + } + // curvature continuity with the adjoining tracks; the designed fragment starts and ends on a tangent + if (Design.start_radius > 0.0 && Design.start_radius < 10000.0) + r.warnings.emplace_back(format("The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly", Design.start_radius)); + if (Design.end_radius > 0.0 && Design.end_radius < 10000.0) + r.warnings.emplace_back(format("The adjoining track at the end is curved (R %.0f m), the curvature changes abruptly", Design.end_radius)); + + r.valid = r.errors.empty() && r.length > 1e-3; + return r; +} + +bool tangent_intersection(design const &Design, glm::dvec2 &Point) +{ + auto const start{plan(Design.start)}; + auto const offset{plan(Design.end) - start}; + auto const startdirection{glm::normalize(Design.start_direction)}; + auto const enddirection{glm::normalize(Design.end_direction)}; + auto const determinant{cross(startdirection, enddirection)}; + if (std::abs(determinant) < 1e-9) + return false; + auto const t{cross(offset, enddirection) / determinant}; + auto const u{cross(startdirection, offset) / determinant}; + if (t <= 0.0 || u <= 0.0) + return false; + Point = start + startdirection * t; + return true; +} + +bool collinear(design const &Design) +{ + auto const offset{plan(Design.end) - plan(Design.start)}; + auto const startdirection{glm::normalize(Design.start_direction)}; + auto const enddirection{glm::normalize(Design.end_direction)}; + return std::abs(cross(startdirection, offset)) <= 0.01 && glm::dot(startdirection, enddirection) >= std::cos(1e-4) && glm::dot(startdirection, offset) > 0.0; +} + +double elevation(result const &Result, double const Chainage) +{ + auto const &profile{Result.profile}; + if (profile.size() < 2) + return 0.0; + auto const s{std::clamp(Chainage, 0.0, Result.length)}; + std::size_t segment{0}; + while (segment + 2 < profile.size() && s > profile[segment + 1].chainage) + ++segment; + auto const &from{profile[segment]}; + auto const &to{profile[segment + 1]}; + double height{from.elevation + (to.elevation - from.elevation) * (s - from.chainage) / (to.chainage - from.chainage)}; + for (std::size_t i = 1; i + 1 < profile.size(); ++i) + { + auto const &point{profile[i]}; + auto const distance{std::abs(s - point.chainage)}; + if (point.tangent <= 0.0 || distance >= point.tangent) + continue; + auto const gradebefore{(point.elevation - profile[i - 1].elevation) / (point.chainage - profile[i - 1].chainage)}; + auto const gradeafter{(profile[i + 1].elevation - point.elevation) / (profile[i + 1].chainage - point.chainage)}; + auto const offset{(point.tangent - distance) * (point.tangent - distance) / (2.0 * point.radius)}; + height += gradeafter > gradebefore ? offset : -offset; + } + return height; +} + +double grade(result const &Result, double const Chainage) +{ + auto const &profile{Result.profile}; + if (profile.size() < 2) + return 0.0; + auto const s{std::clamp(Chainage, 0.0, Result.length)}; + std::size_t segment{0}; + while (segment + 2 < profile.size() && s > profile[segment + 1].chainage) + ++segment; + auto const &from{profile[segment]}; + auto const &to{profile[segment + 1]}; + double slope{(to.elevation - from.elevation) / (to.chainage - from.chainage)}; + for (std::size_t i = 1; i + 1 < profile.size(); ++i) + { + auto const &point{profile[i]}; + auto const distance{s - point.chainage}; + if (point.tangent <= 0.0 || std::abs(distance) >= point.tangent) + continue; + auto const gradebefore{(point.elevation - profile[i - 1].elevation) / (point.chainage - profile[i - 1].chainage)}; + auto const gradeafter{(profile[i + 1].elevation - point.elevation) / (profile[i + 1].chainage - point.chainage)}; + auto const change{(point.tangent - std::abs(distance)) / point.radius * (distance < 0.0 ? 1.0 : -1.0)}; + slope += gradeafter > gradebefore ? change : -change; + } + return slope; +} + +station evaluate(result const &Result, double const Chainage) +{ + station result; + if (Result.elements.empty()) + return result; + auto const s{std::clamp(Chainage, 0.0, Result.length)}; + auto const &e{element_at(Result, s)}; + auto const distance{std::clamp(s - e.chainage, 0.0, e.length)}; + glm::dvec2 position, direction; + element_point(e, Result.shape, distance, position, direction); + result.position = {position.x, elevation(Result, s), position.y}; + result.direction = glm::normalize(glm::dvec3{direction.x, grade(Result, s), direction.y}); + result.cant = element_cant(e, distance); + result.turn = e.turn; + return result; +} + +std::size_t minimum_pieces(result const &Result, design const &Design) +{ + std::size_t count{0}; + for (auto const &e : Result.elements) + count += element_pieces(e, Design.shape); + return count; +} + +std::vector pieces(result const &Result, design const &Design, std::size_t const Count) +{ + struct span + { + std::size_t element; + double from, to; + }; + std::vector spans; + for (std::size_t i = 0; i < Result.elements.size(); ++i) + { + auto const &e{Result.elements[i]}; + auto const count{element_pieces(e, Design.shape)}; + for (std::size_t j = 0; j < count; ++j) + spans.push_back({i, e.length * j / count, e.length * (j + 1) / count}); + } + // use up the requested number of pieces by dividing the longest ones + while (false == spans.empty() && spans.size() < Count) + { + auto longest = std::max_element(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.to - A.from < B.to - B.from; }); + auto const middle{(longest->from + longest->to) * 0.5}; + span const second{longest->element, middle, longest->to}; + longest->to = middle; + spans.insert(std::next(longest), second); + } + + std::vector result; + for (auto const &piece : spans) + { + auto const &e{Result.elements[piece.element]}; + glm::dvec2 p0, p3, d0, d3; + element_point(e, Design.shape, piece.from, p0, d0); + element_point(e, Design.shape, piece.to, p3, d3); + glm::dvec2 p1, p2; + switch (e.kind) + { + case element_kind::straight: + p1 = p0 + (p3 - p0) / 3.0; + p2 = p0 + (p3 - p0) * (2.0 / 3.0); + break; + case element_kind::arc: + { + auto const angle{(piece.to - piece.from) / e.radius}; + auto const handle{4.0 / 3.0 * std::tan(angle / 4.0) * e.radius}; + p1 = p0 + d0 * handle; + p2 = p3 - d3 * handle; + break; + } + default: + { + if (Design.shape == transition_shape::cubic_parabola) + { + // exact: part of the cubic which describes the whole transition + std::array const local{glm::dvec2{0.0, 0.0}, glm::dvec2{e.transition / 3.0, 0.0}, glm::dvec2{e.transition * 2.0 / 3.0, 0.0}, glm::dvec2{e.transition, e.transition * e.transition / (6.0 * e.radius)}}; + auto const a{e.kind == element_kind::transition_in ? piece.from / e.transition : 1.0 - piece.from / e.transition}; + auto const b{e.kind == element_kind::transition_in ? piece.to / e.transition : 1.0 - piece.to / e.transition}; + auto const world = [&](glm::dvec2 const &Point) { return e.origin + e.direction * Point.x + e.normal * Point.y; }; + p0 = world(blossom(local, a, a, a)); + p1 = world(blossom(local, a, a, b)); + p2 = world(blossom(local, a, b, b)); + p3 = world(blossom(local, b, b, b)); + } + else + { + auto const chord{glm::distance(p0, p3) / 3.0}; + p1 = p0 + d0 * chord; + p2 = p3 - d3 * chord; + } + break; + } + } + // heights follow the profile, with end slopes matching its grade + auto const s0{e.chainage + piece.from}; + auto const s3{e.chainage + piece.to}; + auto const y0{elevation(Result, s0)}; + auto const y3{elevation(Result, s3)}; + auto const y1{y0 + grade(Result, s0) * glm::distance(p0, p1)}; + auto const y2{y3 - grade(Result, s3) * glm::distance(p2, p3)}; + + auto const roll = [&](double const Cant) { return static_cast(e.turn * glm::degrees(std::asin(std::clamp(Cant / Design.norms.gauge, -1.0, 1.0)))); }; + + segment_data path; + path.points[segment_data::point::start] = {p0.x, y0, p0.y}; + path.points[segment_data::point::end] = {p3.x, y3, p3.y}; + path.points[segment_data::point::control1] = glm::dvec3{p1.x, y1, p1.y} - path.points[segment_data::point::start]; + path.points[segment_data::point::control2] = glm::dvec3{p2.x, y2, p2.y} - path.points[segment_data::point::end]; + path.rolls[0] = roll(element_cant(e, piece.from)); + path.rolls[1] = roll(element_cant(e, piece.to)); + path.radius = e.kind == element_kind::straight ? 0.f : static_cast(e.radius); + result.push_back(path); + } + return result; +} + +} // namespace alignment diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp new file mode 100644 index 00000000..4241eb2b --- /dev/null +++ b/editor/editorAlignment.hpp @@ -0,0 +1,184 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +#include "world/Segment.h" + +// Railway alignment design: horizontal alignment defined by a polygon of tangents, with a circular curve +// and transition curves fitted at each intersection point (vertex), and a vertical profile made of grades +// joined with parabolic vertical curves. +// +// Plan coordinates are (x, z) of the scene; heights are scene y. Design values follow the usual railway +// conventions: speed in km/h, cant in mm, radii and lengths in metres. All the limits are configurable. +namespace alignment +{ + +enum class transition_shape +{ + cubic_parabola, // y = x^3 / (6RL), represented exactly by a single cubic Bezier + clothoid // curvature linear along the length, approximated by cubic pieces +}; + +// permissible values used to recommend and verify the design +struct limits +{ + double gauge{1500.0}; // distance between rail axes, mm + double unbalanced{0.6}; // permissible unbalanced lateral acceleration, m/s2 + double cant_max{150.0}; // maximum cant, mm + double cant_rate{35.0}; // permissible rate of cant change along transition, mm/s + double ramp_factor{10.0}; // cant ramp inclination limited to 1 / (factor * V) + double jerk{0.3}; // permissible rate of change of unbalanced acceleration, m/s3 + double vertical_factor{0.4}; // minimum vertical curve radius = factor * V^2 + double vertical_min{2000.0}; // absolute minimum vertical curve radius, m + double tangent_min_time{0.0}; // minimum length of a straight between curves expressed as travel time, s (0: not checked) +}; + +// intersection point of two tangents of the horizontal alignment, with the curve fitted there +struct vertex +{ + glm::dvec2 position{0.0}; // plan position of an unconstrained vertex + double offset{100.0}; // first/last vertex: distance from the fixed end along its tangent + double radius{1000.0}; + double transition_in{0.0}; + double transition_out{0.0}; + double cant{0.0}; // mm + bool auto_elevation{true}; // elevation interpolated between the fixed ends + double elevation{0.0}; // height of the profile at the vertex + double vertical_radius{0.0}; // 0: minimum radius from the limits +}; + +struct design +{ + // fixed ends of the designed fragment, with plan directions pointing along the route + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + glm::dvec2 start_direction{0.0, 1.0}; + glm::dvec2 end_direction{0.0, 1.0}; + double start_grade{0.0}; // grade of the adjoining track, for continuity check + double end_grade{0.0}; + double start_radius{0.0}; // plan radius of the adjoining track at the joint, 0 for straight + double end_radius{0.0}; + std::vector vertices; + transition_shape shape{transition_shape::cubic_parabola}; + double speed{100.0}; // design speed, km/h + limits norms; +}; + +// values derived from the design speed for a curve of given radius +struct recommendation +{ + double cant_equilibrium{0.0}; // cant compensating the lateral acceleration fully, mm + double cant_min{0.0}; // smallest cant keeping the unbalanced acceleration within the limit, mm + double cant{0.0}; // proposed cant, mm + double radius_min{0.0}; // smallest radius for the design speed with maximum cant, m + double transition{0.0}; // minimum transition length for the proposed cant, m + double vertical_radius{0.0}; // minimum vertical curve radius, m +}; +recommendation recommend(double const Speed, double const Radius, limits const &Limits); +// minimum transition length for given speed, cant and radius +double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits); +// unbalanced lateral acceleration, m/s2 +double unbalanced_acceleration(double const Speed, double const Radius, double const Cant, limits const &Limits); + +enum class element_kind +{ + straight, + transition_in, + arc, + transition_out +}; + +// single geometric element of the computed alignment +struct element +{ + element_kind kind{element_kind::straight}; + int vertex{-1}; // vertex the curve element belongs to + double chainage{0.0}; // distance from the start of the fragment to the start of the element + double length{0.0}; + double radius{0.0}; // curve radius (also the final radius of a transition) + double transition{0.0}; // transition length + int turn{0}; // +1/-1 direction of the curve, 0 for straights + double cant{0.0}; // full cant of the curve, mm + // geometric frame: straight - start point and direction; arc - centre, radius vector to its start point + // and direction at the start; transition_in - its start point (TS) with the tangent direction and inner normal; + // transition_out - its end point (ST) with the direction pointing backwards and inner normal + glm::dvec2 origin{0.0}; + glm::dvec2 direction{0.0, 1.0}; + glm::dvec2 normal{0.0}; +}; + +// verification of a single curve against the limits +struct curve_report +{ + int vertex{-1}; + double deflection{0.0}; // angle between the tangents, radians + double arc_length{0.0}; + double tangent_in{0.0}; // distance from the vertex to the start of the curve + double tangent_out{0.0}; // distance from the vertex to the end of the curve + double unbalanced{0.0}; // m/s2 + double cant_rate_in{0.0}, cant_rate_out{0.0}; // mm/s + double jerk_in{0.0}, jerk_out{0.0}; // m/s3 + recommendation recommended; +}; + +struct profile_point +{ + double chainage{0.0}; + double elevation{0.0}; + double radius{0.0}; // vertical curve radius at the grade break + double tangent{0.0}; // vertical curve tangent length +}; + +struct result +{ + bool valid{false}; + transition_shape shape{transition_shape::cubic_parabola}; + double gauge{1500.0}; + std::vector errors; + std::vector warnings; + std::vector vertices; // resolved plan positions of the vertices + std::vector vertex_chainages; // chainage of the middle of each vertex curve + std::vector vertex_elevations; // profile elevation at each vertex + std::vector elements; + std::vector curves; + std::vector profile; + double length{0.0}; +}; + +result compute(design const &Design); +// intersection of the tangents of the fixed ends, if it lies ahead of both ends +bool tangent_intersection(design const &Design, glm::dvec2 &Point); +// checks whether the fixed ends lie on a common straight line +bool collinear(design const &Design); + +// position, direction of travel (unit, 3d), cant and curvature direction at specified chainage +struct station +{ + glm::dvec3 position{0.0}; + glm::dvec3 direction{0.0}; + double cant{0.0}; + int turn{0}; +}; +station evaluate(result const &Result, double const Chainage); +// profile height and grade at specified chainage +double elevation(result const &Result, double const Chainage); +double grade(result const &Result, double const Chainage); + +// smallest number of cubic pieces representing the alignment with the required accuracy +std::size_t minimum_pieces(result const &Result, design const &Design); +// splits the alignment into at least Count cubic pieces (the longest pieces are divided further to reach the count), +// in the order of the route, as path source data with absolute end points and relative control vectors +std::vector pieces(result const &Result, design const &Design, std::size_t const Count); + +} // namespace alignment diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 5a127833..c4ae7c2a 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -16,10 +16,12 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "utilities/Globals.h" #include "utilities/utilities.h" +#include "utilities/parser.h" #include #include #include +#include namespace { @@ -54,6 +56,74 @@ void add_unique(std::vector &Tracks, TTrack *Track) Tracks.emplace_back(Track); } +// first derivative of the path curve at its start or end, in the direction from the start to the end +glm::dvec3 path_tangent(segment_data const &Path, bool const Atend) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (false == Atend) + return control1 != glm::dvec3{} ? control1 : (end + control2) - start; + return control2 != glm::dvec3{} ? -control2 : end - (start + control1); +} + +// plan radius of the path curve at specified parameter, 0 if it's straight there +double path_radius(segment_data const &Path, double const T) +{ + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + return 0.0; + auto const p1{p0 + control1}; + auto const p2{p3 + control2}; + auto const u{1.0 - T}; + auto const first{3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2)}; + auto const second{6.0 * u * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1)}; + auto const speed{std::hypot(first.x, first.z)}; + auto const curvature{std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed)}; + return curvature > 1e-7 ? 1.0 / curvature : 0.0; +} + +// the same path run in the opposite direction +segment_data reversed(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + // roll follows the direction of the curve, which flips along with the direction of travel + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +// direction of travel and plan radius at the joint of a chain with an adjoining path. +// Leaving: the chain continues into the adjoining path (its end), otherwise it comes from it (its start) +bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool const Leaving, glm::dvec3 &Direction, double &Radius) +{ + if (Neighbour == nullptr) + return false; + for (auto const &path : Neighbour->m_paths) + { + for (auto const atend : {false, true}) + { + auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]}; + if (glm::distance(point, Joint) > kSamePoint) + continue; + // the tangent runs from the start of the neighbour to its end; it's the direction of travel when + // entering the neighbour at its start, or arriving from it through its end + auto const tangent{glm::normalize(path_tangent(path, atend))}; + Direction = (Leaving != atend) ? tangent : -tangent; + Radius = path_radius(path, atend ? 1.0 : 0.0); + return true; + } + } + return false; +} + } // namespace editor_track::state editor_track::capture(TTrack const &Track) @@ -637,6 +707,181 @@ void editor_track::store_switch_path(TTrack &Switch, int const Path) } } +bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error) +{ + Chain = {}; + if (From == nullptr || To == nullptr) + { + Error = "Select the first and the last path of the fragment"; + return false; + } + auto const regular = [](TTrack const *Track) { return Track->eType == tt_Normal && is_supported(*Track); }; + if (false == regular(From) || false == regular(To)) + { + Error = "The fragment has to consist of regular paths, without switches"; + return false; + } + + Chain.tracks = {From}; + Chain.forward = {true}; + if (From != To) + { + bool found{false}; + // try to reach the last path leaving the first one through its end, then through its start + for (auto const throughend : {true, false}) + { + Chain.tracks = {From}; + Chain.forward = {throughend}; + TTrack *previous{From}; + TTrack *current{throughend ? From->trNext : From->trPrev}; + while (current != nullptr && current != From && Chain.tracks.size() < 10000 && regular(current)) + { + auto const entersatstart{current->trPrev == previous}; + Chain.tracks.push_back(current); + Chain.forward.push_back(entersatstart); + if (current == To) + { + found = true; + break; + } + previous = current; + current = entersatstart ? current->trNext : current->trPrev; + } + if (found) + break; + } + if (false == found) + { + Chain = {}; + Error = "The selected paths aren't connected by a chain of regular paths (switches end the chain)"; + return false; + } + } + + auto const category{From->iCategoryFlag & 15}; + for (auto const *track : Chain.tracks) + { + if ((track->iCategoryFlag & 15) != category) + { + Error = "The fragment mixes different kinds of paths"; + Chain = {}; + return false; + } + if (false == track->Dynamics.empty()) + { + Error = "There are vehicles placed on path " + track->name(); + Chain = {}; + return false; + } + Chain.length += track->Length(); + Chain.velocity = std::max(Chain.velocity, velocity(*track)); + auto const radius{path_radius(track->m_paths.front(), 0.5)}; + if (radius > 0.0 && (Chain.radius == 0.0 || radius < Chain.radius)) + Chain.radius = radius; + } + + // fixed ends, with directions taken from the adjoining paths when there are any + auto const &first{From->m_paths.front()}; + auto const firstforward{Chain.forward.front()}; + Chain.start = first.points[firstforward ? segment_data::point::start : segment_data::point::end]; + Chain.start_direction = firstforward ? path_tangent(first, false) : -path_tangent(first, true); + adjoining_direction(firstforward ? From->trPrev : From->trNext, Chain.start, false, Chain.start_direction, Chain.start_radius); + Chain.start_direction = glm::normalize(Chain.start_direction); + + auto const &last{To->m_paths.front()}; + auto const lastforward{Chain.forward.back()}; + Chain.end = last.points[lastforward ? segment_data::point::end : segment_data::point::start]; + Chain.end_direction = lastforward ? path_tangent(last, true) : -path_tangent(last, false); + adjoining_direction(lastforward ? To->trNext : To->trPrev, Chain.end, true, Chain.end_direction, Chain.end_radius); + Chain.end_direction = glm::normalize(Chain.end_direction); + + Error.clear(); + return true; +} + +std::vector editor_track::relay(chain const &Chain, std::vector const &Pieces) +{ + std::vector created; + auto const count{Chain.tracks.size()}; + if (count == 0 || Pieces.size() < count) + return created; + + // existing paths are spread evenly over the pieces, added paths fill the gaps between them + std::vector tracks; + std::size_t previous{count}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + auto const index{i * count / Pieces.size()}; + TTrack *track; + bool forward; + if (index != previous) + { + track = Chain.tracks[index]; + forward = Chain.forward[index]; + } + else + { + track = clone(*Chain.tracks[index]); + created.push_back(track); + forward = true; + } + previous = index; + track->m_paths.resize(1); + track->m_paths.front() = forward ? Pieces[i] : reversed(Pieces[i]); + tracks.push_back(track); + } + commit(tracks); + return created; +} + +TTrack *editor_track::clone(TTrack const &Template) +{ + std::ostringstream text; + Template.export_as_text_(text); + cParser parser(text.str(), cParser::buffer_TEXT); + parser.getTokens(); + std::string token; + parser >> token; // node type, consumed before the path is loaded + + scene::node_data data; + data.type = "track"; + if (false == Template.name().empty() && Template.name() != "none") + { + for (int i = 1; data.name.empty() || simulation::Paths.find(data.name) != nullptr; ++i) + data.name = Template.name() + "_" + std::to_string(i); + } + auto *track = new TTrack(data); + track->m_rangesquaredmin = Template.m_rangesquaredmin; + track->m_rangesquaredmax = Template.m_rangesquaredmax; + track->Load(&parser, glm::dvec3{}); + // events stay with the original path only + for (auto *events : {&track->m_events0, &track->m_events1, &track->m_events2, &track->m_events0all, &track->m_events1all, &track->m_events2all}) + events->clear(); + track->m_events = false; + track->m_friction = Template.m_friction; + simulation::Paths.insert(track); + return track; +} + +void editor_track::retire(TTrack &Track) +{ + auto const adjoining{neighbours(Track)}; + disconnect(Track); + simulation::Region->erase_and_unregister(&Track); + Track.m_editorremoved = true; + for (auto *neighbour : adjoining) + { + update_transition(*neighbour); + rebuild_geometry(*neighbour); + } +} + +void editor_track::revive(TTrack &Track) +{ + Track.m_editorremoved = false; + commit({&Track}); +} + void editor_track::rebuild_geometry(TTrack &Track) { auto §ion{simulation::Region->section(Track.location())}; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index bd39766c..17ed0247 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -108,6 +108,30 @@ class editor_track // places the point at the target and optionally aligns the control vector with the target path static void snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent); + // sequence of regular paths connected end to end + struct chain + { + std::vector tracks; + std::vector forward; // the path runs in the direction of the chain (its start is closer to the chain start) + glm::dvec3 start{0.0}; // outer end of the first path + glm::dvec3 end{0.0}; // outer end of the last path + glm::dvec3 start_direction{0.0}; // direction of travel along the chain at its ends, from the adjoining paths if there are any + glm::dvec3 end_direction{0.0}; + double start_radius{0.0}; // plan radius of the adjoining paths at the joints, 0 for straight + double end_radius{0.0}; + double length{0.0}; + double velocity{-1.0}; // highest speed limit of the paths in the chain + double radius{0.0}; // smallest radius of the paths in the chain, 0 if all are straight + }; + // collects regular paths connected between provided paths, both included. returns: true on success + static bool find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error); + // lays the paths of the chain along provided pieces, in the order of the chain. if there are more pieces than paths, + // additional paths are made as copies of their neighbours. returns: the added paths + static std::vector relay(chain const &Chain, std::vector const &Pieces); + // takes a path added by the editor out of the scene, or puts it back + static void retire(TTrack &Track); + static void revive(TTrack &Track); + // names of the materials used by the path; "none" for missing ones static std::string material_name(material_handle const Material); static material_handle fetch_material(std::string const &Name); @@ -133,4 +157,6 @@ class editor_track static void store_switch_path(TTrack &Switch, int const Path); // rebuilds render geometry of the path in the bank of the section it's placed in static void rebuild_geometry(TTrack &Track); + // makes a new path with the parameters of provided path (without its events) and registers it in the path table + static TTrack *clone(TTrack const &Template); }; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 9e90941f..f6f0c3c5 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -1362,6 +1362,9 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons // tracks scmfile << "// paths\n"; for( auto const *path : Paths.sequence() ) { + if( path == nullptr || path->m_editorremoved ) { + continue; + } if( path->dirty() == Dirty && path->group() == null_handle ) { path->export_as_text( scmfile ); } diff --git a/world/Track.h b/world/Track.h index 33379dfd..3cadd981 100644 --- a/world/Track.h +++ b/world/Track.h @@ -185,6 +185,7 @@ public: geometryhandle_sequence Geometry2; // geometry chunks textured with texture 2 std::vector m_paths; // source data for owned paths + bool m_editorremoved { false }; // taken out of the scene by the editor, kept alive for its undo history int iterate_stamp = 0; // sleepermodel optional parameter -------------------------------------------------