From 16348c655a4888a8dc0f92091652aaf5ccb88fba Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 18 Oct 2026 02:24:04 +0200 Subject: [PATCH] refactor(editor): the track mode split by tool instead of one file of 6900 lines - editormoderoute.cpp: the route design, its kept records and digitizing - editormodestraights.cpp: the plan tool: straights, the detour, the curve fit - editormodelay.cpp: laying track, free ends and extending, the build tools - editormodeswitchplace.cpp: placing switches, crossovers and links, double slips, curved switches - editormodetrack.cpp keeps the selection, the snapshots, the tabs, the inspector and the path panel, the hints - the sizes of the handles and reaches and the small helpers they all use in editormodetrackshared.h The code is moved as it was. --- CMakeLists.txt | 4 + application/editormodelay.cpp | 1152 ++++++ application/editormoderoute.cpp | 1225 ++++++ application/editormodestraights.cpp | 1270 +++++++ application/editormodeswitchplace.cpp | 1317 +++++++ application/editormodetrack.cpp | 4990 +------------------------ application/editormodetrackshared.h | 233 ++ 7 files changed, 5203 insertions(+), 4988 deletions(-) create mode 100644 application/editormodelay.cpp create mode 100644 application/editormoderoute.cpp create mode 100644 application/editormodestraights.cpp create mode 100644 application/editormodeswitchplace.cpp create mode 100644 application/editormodetrackshared.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3546ff50..7a382990 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -205,6 +205,10 @@ set(SOURCES "editor/editorClips.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" +"application/editormoderoute.cpp" +"application/editormodestraights.cpp" +"application/editormodelay.cpp" +"application/editormodeswitchplace.cpp" "application/editormodetrackgizmo.cpp" "application/editormoderecords.cpp" "application/editormodeswitchdrive.cpp" diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp new file mode 100644 index 00000000..cf14ffb3 --- /dev/null +++ b/application/editormodelay.cpp @@ -0,0 +1,1152 @@ +/* +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: +*/ + + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/TrackFeatures.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application/editormodetrackshared.h" + +using namespace track_mode; + +void editor_mode::update_build_tools() +{ + glm::dvec3 const ground{cursor_ground()}; + auto const &io{ImGui::GetIO()}; + glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; + m_straights.detour_mouse = ground; + auto const joining = [&](editor_track::snap_target const &Target) { + if (Target.track == m_join_track && Target.point == m_join_point) + return; + m_join_track = Target.track; + m_join_point = Target.point; + m_join_radius = 0.0; + m_join_variant = 0; + }; + bool const pointing{m_cursor_override.has_value() || ImGui::IsMousePosValid()}; + if (m_extend.active) + { + if (pointing) + m_extend.mouse = cursor_terrain(m_extend.point.y); + m_extend.snap = m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + if (m_extend.snap.track == nullptr) + m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); + joining(m_extend.snap); + } + if (m_switch.placing) + m_switch.mouse = cursor_level(m_switch.point.y); + auto &lay{m_lay}; + lay.preview.clear(); + lay.preview_error.clear(); + lay.preview_length = 0.0; + if (false == lay.active) + return; + // geometric ray, same as extend: Mouse_Position() is a delayed depth pick and lags the cursor + double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : ground.y}; + if (pointing) + lay.mouse = typed_lay(cursor_terrain(height)); + if (pointing && ImGui::GetIO().KeyAlt && m_typed.empty() && false == lay.points.empty()) + lay.mouse = lay_angle_snap(lay.mouse); + lay.mouse_snap = m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); + if (lay.mouse_snap.track == nullptr) + lay.mouse = lay_bed(lay.mouse); + if (false == m_extend.active) + joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); + if (lay.points.empty()) + return; + auto points{lay.points}; + auto end{lay.mouse_snap}; + if (end.track != nullptr && glm::distance(end.position, points.front()) > 1.0) + points.push_back(end.position); + else + { + end = {}; + if (plan_distance(lay.mouse, points.back()) > 1.0) + points.push_back(lay.mouse); + } + lay.preview = lay_pieces(points, end, lay.preview_length, lay.preview_error); +} + +bool editor_mode::free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool) +{ + editor_track::point_ref const point{Path, Atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + if (Path < 0 || Path >= static_cast(Track.m_paths.size()) || editor_track::is_connected(Track, point)) + return false; + auto const &path{Track.m_paths[Path]}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto const &control1{path.points[segment_data::point::control1]}; + auto const &control2{path.points[segment_data::point::control2]}; + glm::dvec3 tangent; + if (Atend) + tangent = control2 != glm::dvec3{} ? -control2 : end - (start + control1); + else + tangent = -(control1 != glm::dvec3{} ? control1 : (end + control2) - start); + glm::dvec2 const planar{tangent.x, tangent.z}; + if (glm::length(planar) < 1e-6) + return false; + Tool.path = Path; + Tool.atend = Atend; + Tool.track = &Track; + Tool.point = editor_track::point_position(Track, point); + Tool.direction = glm::normalize(planar); + Tool.grade = tangent.y / glm::length(planar); + Tool.mouse = Tool.point; + return true; +} + +bool editor_mode::chosen_free_end(extend_tool &Tool) +{ + auto *track{selected_track()}; + if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) + return false; + if (m_track_point.valid() && editor_track::is_end(m_track_point.kind) && free_end(*track, m_track_point.path, m_track_point.kind == editor_track::point_kind::end, Tool)) + return true; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const atend : {true, false}) + if (free_end(*track, i, atend, Tool)) + return true; + return false; +} + +bool editor_mode::start_extend(editor_track::snap_target const &End) +{ + auto *track{End.track}; + if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) + return false; + extend_tool tool; + if (false == free_end(*track, End.point.path, End.point.kind == editor_track::point_kind::end, tool)) + return false; + auto const &io{ImGui::GetIO()}; + m_extend = tool; + m_extend.origin = End; + m_extend.pressed = {io.MousePos.x, io.MousePos.y}; + m_extend.active = true; + if (track->eType == tt_Switch && End.point.path == 1 && End.point.kind == editor_track::point_kind::end) + m_extend.line = editor_track::find_straight(*track, m_straights.tolerance).tracks; + return true; +} + +bool editor_mode::extend_snap(editor_track::snap_target &Target) const +{ + auto const &tool{m_extend}; + if (false == tool.active || tool.track == nullptr) + return false; + Target = tool.snap; + return Target.track != nullptr && glm::distance(Target.position, tool.point) > 0.5; +} + +glm::dvec3 editor_mode::cursor_ground() const +{ + glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + if (GfxRenderer->Mouse_Hit()) + return ground; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(ImGui::GetIO().MousePos, 0.0, level)) + return level; + return {ground.x, 0.0, ground.z}; +} + +glm::dvec3 editor_mode::cursor_level(double const Height) const +{ + if (m_cursor_override.has_value()) + return glm::dvec3{m_cursor_override->x, Height, m_cursor_override->z}; + glm::dvec3 const ground{cursor_ground()}; + screen_projection const projection; + glm::dvec3 level; + if (false == projection.on_level(ImGui::GetIO().MousePos, Height, level) || (false == Global.editor_tracks && glm::distance(level, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) + return ground; + return level; +} + +editor_track::snap_target editor_mode::snap_free_end(glm::dvec3 const &Near, glm::vec2 const &Screen, TTrack const *Self, int const Category, std::vector const &Exclude) const +{ + screen_projection const projection; + auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; + editor_track::snap_target result; + auto best{kSnapPixels * kSnapPixels}; + for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) + { + ImVec2 screen; + if (false == projection.project(candidate.position, screen)) + continue; + auto const distance{(screen.x - Screen.x) * (screen.x - Screen.x) + (screen.y - Screen.y) * (screen.y - Screen.y)}; + if (distance < best) + { + best = distance; + result = candidate; + } + } + if (result.track == nullptr && m_track_snap_radius > 0.0f) + result = editor_track::find_free_end(Self, Category, Near, m_track_snap_radius, Exclude); + return result; +} + +void editor_mode::draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const +{ + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const &display{ImGui::GetIO().DisplaySize}; + auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; + for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) + { + ImVec2 screen; + if (projection.project(candidate.position, screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) + drawlist->AddCircle(screen, 7.0f, IM_COL32(255, 60, 255, 170), 12, 1.5f); + } +} + +bool editor_mode::switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point) +{ + auto *selected{selected_track()}; + if (m_track_tab != track_tab::turnout || m_switch.armed < 0 || selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) + return false; + auto const &line{current_straight()}; + if (false == line.tracks.empty()) + { + auto const along{std::clamp(line.along(Ground), 0.0, line.length)}; + Point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along; + } + else + { + Point = editor_track::point_at(*selected, editor_track::nearest_parameter(*selected, Ground)); + } + return plan_distance(Point, Ground) <= kSwitchReach; +} + +void editor_mode::handle_drag_start(glm::dvec3 const &Anchor) +{ + auto const &io{ImGui::GetIO()}; + m_handle_drag = {}; + m_handle_drag.active = true; + m_handle_drag.from = {io.MousePos.x, io.MousePos.y}; + m_handle_drag.height = Anchor.y; + m_handle_drag.position = Anchor; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(io.MousePos, Anchor.y, level)) + m_handle_drag.offset = {Anchor.x - level.x, 0.0, Anchor.z - level.z}; +} + +bool editor_mode::handle_drag_step(glm::dvec3 &Position) +{ + auto &drag{m_handle_drag}; + auto const &io{ImGui::GetIO()}; + if (false == drag.active) + return false; + if (false == io.MouseDown[0]) + { + drag = {}; + return false; + } + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (false == drag.moved && glm::length(mouse - drag.from) < kDragPixels) + return false; + drag.moved = true; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(io.MousePos, drag.height, level) && (Global.editor_tracks || glm::distance(level, glm::dvec3{Global.pCamera.Pos}) <= static_cast(kMaxPlacementDistance))) + drag.position = level + drag.offset; + Position = drag.position; + return true; +} + +void editor_mode::point_drag_start(editor_track::point_ref const &Point) +{ + auto *track{selected_track()}; + m_track_point = Point; + m_point_drag = {}; + if (track == nullptr || false == Point.valid() || Point.path >= static_cast(track->m_paths.size())) + return; + auto const &io{ImGui::GetIO()}; + m_point_drag.active = true; + m_point_drag.from = {io.MousePos.x, io.MousePos.y}; + m_point_drag.height = editor_track::point_position(*track, Point).y; +} + +void editor_mode::point_drag_update() +{ + auto &drag{m_point_drag}; + if (false == drag.active) + return; + auto const &io{ImGui::GetIO()}; + if (false == io.MouseDown[0]) + { + point_drag_finish(); + return; + } + auto *track{selected_track()}; + if (track == nullptr || false == m_track_point.valid() || m_track_point.path >= static_cast(track->m_paths.size())) + { + drag = {}; + return; + } + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (false == drag.moved) + { + if (glm::length(mouse - drag.from) < kDragPixels) + return; + std::string reason; + if (false == editor_track::can_edit_geometry(*track, reason)) + { + ui()->set_status(reason, true); + drag = {}; + return; + } + drag.moved = true; + m_track_drag = {track}; + m_track_drag_points.clear(); + m_track_joints.clear(); + m_track_snap = {}; + if (editor_track::is_end(m_track_point.kind) && m_track_drag_connected) + { + for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) + { + std::string neighbourreason; + if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason)) + continue; + m_track_drag_points.emplace_back(connection); + if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) + m_track_drag.emplace_back(connection.first); + } + } + std::vector> states; + for (auto *dragged : m_track_drag) + states.emplace_back(dragged, editor_track::capture(*dragged)); + push_track_snapshot(std::move(states)); + records_watch_begin(); + } + screen_projection const projection; + glm::dvec3 position; + if (false == projection.on_level(io.MousePos, drag.height, position) || (false == Global.editor_tracks && glm::distance(position, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) + return; + m_track_snap = {}; + if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) + { + std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); + m_track_snap = snap_free_end(position, mouse, track, track->iCategoryFlag & 15, exclude); + if (m_track_snap.track != nullptr) + position = m_track_snap.position; + } + editor_track::move_point(*track, m_track_point, position); + for (auto const &connection : m_track_drag_points) + editor_track::move_point(*connection.first, connection.second, position); + m_track_dirty = true; + commit_track_drag(false); +} + +void editor_mode::point_drag_finish() +{ + auto const drag{m_point_drag}; + m_point_drag = {}; + if (false == drag.active || false == drag.moved) + return; + if (auto *track{selected_track()}; track != nullptr && m_track_snap.track != nullptr && m_track_point.valid()) + { + editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent); + m_track_dirty = true; + } + m_track_snap = {}; + commit_track_drag(true); + records_watch_end(); + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_joints.clear(); +} + +std::vector editor_mode::extend_pieces() const +{ + std::vector result; + auto const &tool{m_extend}; + editor_track::snap_target target; + if (extend_snap(target)) + { + editor_track::branch_link link; + std::string reason; + if (plan_branch_link(link, reason)) + { + m_join_last = {}; + return link.pieces; + } + double length; + std::string error; + return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error); + } + m_join_last = {}; + if (m_track_layout != track_layout::straight && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) + { + auto const &diverging{tool.track->m_paths[1]}; + bezier const curve{diverging}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; + if (auto const radius{editor_track::branch_radius(diverging)}; radius > 0.0) + { + int const side{turn > 0.0 ? 1 : -1}; + auto const &direction{tool.direction}; + auto const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * radius)}; + auto const angle{geometry::signed_angle(plan_of(tool.point) - centre, plan_of(tool.mouse) - centre) * side}; + if (angle * radius < 0.5) + return result; + auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side)}; + for (auto &piece : pieces) + piece.rolls = {diverging.rolls[1], diverging.rolls[1]}; + follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); + return pieces; + } + } + auto radius{m_lay.radius}; + if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) + radius = std::abs(values[0]); + auto pieces{editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; + follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); + return pieces; +} + +double editor_mode::design_speed(TTrack const *From, TTrack const *To) const +{ + double speed{0.0}; + for (auto const *track : {From, To}) + { + if (track == nullptr) + continue; + auto const velocity{editor_track::velocity(*track)}; + if (velocity > 0.0) + speed = speed > 0.0 ? std::min(speed, velocity) : velocity; + } + if (speed <= 0.0) + speed = m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; + return speed; +} + +std::vector editor_mode::join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, + std::string &Error) const +{ + alignment::join_request request; + request.start.position = Start; + request.start.direction = Direction; + request.start.roll = From != nullptr ? -editor_track::joint_roll(*From, Frompoint) : 0.0; + request.end.position = End.position; + request.end.direction = plan_of(End.direction); + request.end.roll = End.track != nullptr ? editor_track::joint_roll(*End.track, End.point) : 0.0; + request.radius = m_join_radius; + request.variant = m_join_variant; + request.transitions = m_lay.transitions; + request.speed = design_speed(From, End.track); + request.kink = m_joints.angle * 0.5; + request.shape = m_route.design.shape; + request.norms = laying_norms(); + auto result{alignment::join(request)}; + if (m_lay.transitions && result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) + { + auto const grade{(End.position.y - Start.y) / std::max(1.0, plan_distance(Start, End.position))}; + auto const in{From != nullptr && From->eType == tt_Normal ? editor_track::ease_out_of(*From, Start, grade, request.speed, request.norms) : editor_track::easing{}}; + auto const out{End.track != nullptr && End.track->eType == tt_Normal ? editor_track::ease_out_of(*End.track, End.position, -grade, request.speed, request.norms) : editor_track::easing{}}; + if (auto eased{eased_join(request, in, out)}) + result = std::move(*eased); + } + Length = result.length; + std::vector notes; + for (auto const &warning : result.warnings) + if (m_join_radius > 0.0 || warning.find("R reduced") == std::string::npos) + notes.push_back(warning); + if (result.kind == alignment::join_kind::bezier) + notes.insert(notes.begin(), STR_C("No curve fits between the ends, they are joined with a free curve")); + Error = lines_of(notes); + m_join_last = result; + return result.pieces; +} + +void editor_mode::finish_extend() +{ + auto const &io{ImGui::GetIO()}; + if (glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - m_extend.pressed) < kDragPixels) + { + m_extend.active = false; + if (m_lay.active && m_lay.points.empty() && m_extend.origin.track != nullptr) + { + m_lay.status.clear(); + m_lay.start = m_extend.origin; + m_lay.points.push_back(m_extend.origin.position); + } + else if (false == m_lay.active && m_extend.origin.track != nullptr && m_extend.origin.track == selected_track()) + m_track_point = m_extend.origin.point; + return; + } + std::string reason; + if (editor_track::branch_link link; plan_branch_link(link, reason)) + { + finish_branch_link(link); + return; + } + if (false == reason.empty()) + ui()->set_status(reason, true); + crossover_plan plan; + editor_track::snap_target target; + if (false == extend_snap(target) && plan_crossover(plan)) + { + auto *origin{m_extend.track}; + m_extend.active = false; + std::vector> states; + std::vector created; + std::vector removed; + if (false == place_switch_on_straight(plan.target, plan.shape, origin, plan.along, plan.direction, plan.side, false, states, created, removed, switch_name_for_new())) + return; + auto *placed{*std::find_if(created.begin(), created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; + if (glm::distance(plan.insert.points[segment_data::point::start], plan.insert.points[segment_data::point::end]) > 0.01) + { + auto *insert{editor_track::create_path(*origin, plan.insert)}; + editor_track::commit({insert}); + created.push_back(insert); + } + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + if (place_switch_drive(*placed)) + join_last_steps(); + advance_switch_name(); + return; + } + auto const pieces{extend_pieces()}; + auto *style{m_extend.track}; + m_extend.active = false; + if (pieces.empty() || style == nullptr) + return; + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*style, piece)); + editor_track::commit(created); + digitize_drawn(created); + push_track_snapshot({}, created); + first_step_done(first_step::laid); + if (target.track != nullptr) + first_step_done(first_step::join); + m_node = created.back(); + ui()->set_node(m_node); + straight_refresh(); +} + +void editor_mode::extend_add(int const Side) +{ + extend_tool tool; + if (false == chosen_free_end(tool)) + return; + auto const length{std::max(0.5, m_extend_length)}; + std::vector pieces; + if (Side == 0) + { + segment_data path; + path.points[segment_data::point::start] = tool.point; + path.points[segment_data::point::end] = {tool.point.x + tool.direction.x * length, tool.point.y + tool.grade * length, tool.point.z + tool.direction.y * length}; + pieces.push_back(path); + } + else + { + auto const radius{std::max(10.0, m_extend_radius)}; + pieces = arc_pieces(tool.point, tool.direction, tool.grade, radius, std::min(length / radius, glm::radians(180.0)), Side); + } + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*tool.track, piece)); + editor_track::commit(created); + digitize_drawn(created); + push_track_snapshot({}, created); + m_node = created.back(); + ui()->set_node(m_node); + m_track_point = {}; + straight_refresh(); +} + +void editor_mode::render_extend_ui() +{ + extend_tool tool; + if (false == chosen_free_end(tool)) + return; + if (false == ImGui::CollapsingHeader(STR_C("Continue from the free end"), ImGuiTreeNodeFlags_DefaultOpen)) + return; + ImGui::PushItemWidth(100.0f); + ImGui::DragScalar(STR_C("Length (m)##extend"), ImGuiDataType_Double, &m_extend_length, 0.5f, nullptr, nullptr, "%.1f"); + item_tooltip("Length of the next piece, along the curve for the curves"); + ImGui::SameLine(); + ImGui::DragScalar("R (m)##extend", ImGuiDataType_Double, &m_extend_radius, 5.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Radius of the curves added"); + ImGui::PopItemWidth(); + m_extend_length = std::clamp(m_extend_length, 0.5, 5000.0); + m_extend_radius = std::clamp(m_extend_radius, 10.0, 100000.0); + if (ImGui::Button(STR_C("Straight##extend"), ImVec2(100.0f, 0.0f))) + extend_add(0); + item_tooltip("Adds a straight at the free end; the new piece gets selected, so the next click goes on from it"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Curve left##extend"), ImVec2(100.0f, 0.0f))) + extend_add(1); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) + extend_add(-1); +} + +void editor_mode::render_layout_combo() +{ + char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; + auto current{static_cast(m_track_layout)}; + ImGui::PushItemWidth(200.0f); + if (ImGui::BeginCombo(STR_C("Shape from a free end"), STR_C(names[current]))) + { + for (int i = 0; i < 4; ++i) + if (ImGui::Selectable(STR_C(names[i]), i == current)) + m_track_layout = static_cast(i); + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + item_tooltip("What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius"); +} + +bool editor_mode::track_wheel(double const Offset) +{ + auto const &io{ImGui::GetIO()}; + if (m_switch.placing && m_switch.connection >= 0 && Offset != 0.0 && (io.KeyCtrl || io.KeyShift || io.KeyAlt)) + { + auto &tool{m_switch}; + int const step{Offset > 0.0 ? 1 : -1}; + if (io.KeyShift) + tool.connection = (tool.connection + step + connection::kind_count) % connection::kind_count; + else if (io.KeyCtrl) + { + static double const radii[]{0.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0}; + int index{0}; + for (int i = 0; i < static_cast(std::size(radii)); ++i) + if (radii[i] <= tool.connection_radius + 1e-6) + index = i; + tool.connection_radius = radii[std::clamp(index + step, 0, static_cast(std::size(radii)) - 1)]; + } + else + tool.connection_together = false == tool.connection_together; + return true; + } + if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0) + return false; + bool const laying{m_lay.active && false == m_lay.points.empty()}; + if (false == m_extend.active && false == laying) + return false; + int const step{Offset > 0.0 ? 1 : -1}; + editor_track::snap_target target; + bool const joined{m_extend.active ? extend_snap(target) : m_lay.mouse_snap.track != nullptr && m_lay.start.track != nullptr}; + static double const radii[]{100.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0, 4000.0, 6000.0}; + auto const next_radius = [&](double const Current) { + if (step > 0) + { + for (auto const radius : radii) + if (radius > Current * 1.001) + return radius; + return std::end(radii)[-1]; + } + for (auto radius{std::rbegin(radii)}; radius != std::rend(radii); ++radius) + if (*radius < Current * 0.999) + return *radius; + return radii[0]; + }; + first_step_done(io.KeyCtrl ? first_step::wheel : first_step::shape); + if (io.KeyCtrl) + { + if (false == joined) + m_lay.radius = next_radius(m_lay.radius); + else if (m_join_radius <= 0.0) + m_join_radius = next_radius(m_join_last.radius > 0.0 ? m_join_last.radius : 500.0); + else if (step > 0 && m_join_radius >= std::end(radii)[-1]) + m_join_radius = 0.0; + else + m_join_radius = next_radius(m_join_radius); + return true; + } + if (joined) + m_join_variant += step; + else + m_track_layout = static_cast((static_cast(m_track_layout) + 4 - step) % 4); + return true; +} + +std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const +{ + auto const &tool{m_lay}; + Length = 0.0; + if (Points.size() < 2) + return {}; + if (Points.size() == 2) + { + auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; + if (glm::length(leaving) < 1e-6) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + if (End.track != nullptr && tool.start.track != nullptr) + return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); + m_join_last = {}; + std::vector pieces; + if (tool.start.track != nullptr) + { + auto const grade{-grade_of(tool.start.direction)}; + pieces = editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); + Error = narrowed(pieces, m_track_layout, tool.radius); + follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN(), tool.start.track); + } + else if (End.track == nullptr) + { + auto const length{glm::length(leaving)}; + pieces = straight_piece(Points[0], glm::normalize(leaving), (Points[1].y - Points[0].y) / length, length); + follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), Points[1].y, std::numeric_limits::quiet_NaN(), tool.copy_selected ? selected_track() : nullptr); + } + else if (glm::length(plan_of(End.direction)) > 1e-6) + { + auto const back{editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; + for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) + pieces.push_back(reversed(*piece)); + Error = narrowed(pieces, m_track_layout, tool.radius); + follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); + } + for (auto const &piece : pieces) + Length += bezier{piece}.plan_length(); + if (pieces.empty()) + Error = STR_C("The track can't be laid through these points"); + return pieces; + } + auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; + auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; + if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + alignment::design design; + auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)}; + if (false == shape.valid) + { + Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front(); + return {}; + } + Error = lines_of(shape.warnings); + auto const count = [&](alignment::result const &Shape, alignment::design const &Design) { + return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); + }; + // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them + 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 editor_track::easing{}; + auto result{editor_track::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{tool.transitions ? eased(tool.start, 0.0, 1.0) : editor_track::easing{}}; + auto const out{tool.transitions ? eased(End, shape.length, -1.0) : editor_track::easing{}}; + if ((false == in.pieces.empty() || false == out.pieces.empty()) && editor_track::eases_room(in, out, Points.front(), design.start_direction, Points.back(), design.end_direction)) + { + auto points{Points}; + auto starting{design.start_direction}; + auto ending{design.end_direction}; + 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) + { + Length = editor_track::eased_length(in, easedshape.length, out); + Error = lines_of(easedshape.warnings); + return editor_track::eased_pieces(in, alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign)), out); + } + 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 = laying_norms(); + design.shape = m_route.design.shape; + design.speed = design_speed(tool.start.track, nullptr); + design.start = Points.front(); + design.end = Points.back(); + design.start_direction = Startdirection; + design.end_direction = Enddirection; + + alignment::vertex corner; + corner.radius = std::max(1.0, tool.radius); + if (Points.size() == 2) + { + if (false == alignment::collinear(design)) + { + glm::dvec2 intersection; + if (alignment::tangent_intersection(design, intersection)) + { + design.vertices.push_back(corner); + } + else + { + corner.offset = plan_distance(design.start, design.end) / 3.0; + design.vertices = {corner, corner}; + } + } + } + else + { + for (std::size_t i = 1; i + 1 < Points.size(); ++i) + { + corner.position = plan_of(Points[i]); + design.vertices.push_back(corner); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(1.0, glm::dot(plan_of(Points[1] - Points.front()), design.start_direction)); + design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); + } + } + return alignment::compute_recommended(design, tool.transitions); +} + +void editor_mode::lay_click() +{ + auto &tool{m_lay}; + tool.status.clear(); + auto const snap{tool.mouse_snap}; + if (snap.track != nullptr && false == tool.points.empty() && glm::distance(snap.position, tool.points.front()) > 1.0) + { + // reaching a free end finishes the track there, in line with the path it joins + tool.points.push_back(snap.position); + lay_finish(snap); + return; + } + if (tool.points.empty()) + { + tool.chain.clear(); + tool.start = snap; + tool.points.push_back(snap.track != nullptr ? snap.position : tool.mouse); + return; + } + auto const &point{tool.mouse}; + if (plan_distance(point, tool.points.back()) < 1.0) + return; + m_typed.clear(); + if (tool.points.size() == 1) + { + lay_tool::step const previous{tool.start, tool.points.front()}; + tool.points.push_back(tool.mouse); + lay_finish({}); + if (false == tool.points.empty()) + { + tool.points.pop_back(); + return; + } + auto *last{dynamic_cast(m_node)}; + if (last == nullptr || last->m_paths.empty()) + return; + auto const &path{last->m_paths.front()}; + editor_track::snap_target next; + next.track = last; + next.point = {0, editor_track::point_kind::end}; + next.position = path.points[segment_data::point::end]; + auto const &control{path.points[segment_data::point::control2]}; + next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position); + tool.chain.push_back(previous); + tool.start = next; + tool.points = {next.position}; + return; + } + tool.points.push_back(point); +} + +void editor_mode::lay_finish(editor_track::snap_target const &End) +{ + auto &tool{m_lay}; + double length; + std::string error; + auto const pieces{lay_pieces(tool.points, End, length, error)}; + tool.status_time = ImGui::GetTime(); + if (pieces.empty()) + { + tool.status = error.empty() ? std::string{STR_C("Click at least two points")} : error; + if (End.track != nullptr && false == tool.points.empty()) + tool.points.pop_back(); + return; + } + auto const *style{tool.start.track != nullptr ? tool.start.track : End.track}; + if (style == nullptr && tool.copy_selected) + style = selected_track(); + std::vector created; + for (auto const &piece : pieces) + created.push_back(style != nullptr ? editor_track::create_path(*style, piece) : editor_track::create_path(tool.style, piece)); + editor_track::commit(created); + digitize_drawn(created); + push_track_snapshot({}, created); + first_step_done(first_step::laid); + if (End.track != nullptr) + first_step_done(first_step::join); + 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); + tool.points.clear(); + tool.start = {}; + m_node = created.back(); + ui()->set_node(m_node); + straight_refresh(); +} + +void editor_mode::lay_enter() +{ + auto &tool{m_lay}; + if (false == m_typed.empty()) + { + lay_click(); + return; + } + if (tool.points.empty()) + return; + auto const end{tool.mouse_snap}; + if (end.track != nullptr && glm::distance(end.position, tool.points.front()) > 1.0) + { + tool.points.push_back(end.position); + lay_finish(end); + return; + } + auto const point{placement_on_ground(tool.mouse)}; + bool const appended{plan_distance(point, tool.points.back()) > 1.0}; + if (appended) + tool.points.push_back(point); + auto const count{tool.points.size()}; + lay_finish({}); + if (appended && tool.points.size() == count) + tool.points.pop_back(); +} + +void editor_mode::lay_cancel() +{ + auto &tool{m_lay}; + tool.points.clear(); + tool.start = {}; + tool.chain.clear(); + tool.preview.clear(); + tool.status.clear(); +} + +void editor_mode::lay_after_history(bool const Undo) +{ + auto &tool{m_lay}; + if (tool.points.empty()) + return; + if (false == Undo) + { + lay_cancel(); + return; + } + while (tool.start.track != nullptr && tool.start.track->m_editorremoved) + { + if (tool.chain.empty()) + { + lay_cancel(); + return; + } + tool.start = tool.chain.back().start; + tool.points = {tool.chain.back().point}; + tool.chain.pop_back(); + } +} + +void editor_mode::lay_back() +{ + auto &tool{m_lay}; + if (false == tool.chain.empty()) + { + undo_last(); + tool.status = STR_C("The last piece was taken back"); + tool.status_time = ImGui::GetTime(); + return; + } + if (false == tool.points.empty()) + tool.points.pop_back(); + if (tool.points.empty()) + tool.start = {}; +} + +glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse) const +{ + auto const &last{m_lay.points.back()}; + glm::dvec2 const offset{Mouse.x - last.x, Mouse.z - last.z}; + auto const length{glm::length(offset)}; + if (length < 1.0) + return Mouse; + auto const leaving{m_lay.start.track != nullptr ? -plan_of(m_lay.start.direction) : glm::dvec2{0.0}}; + glm::dvec2 const reference{glm::length(leaving) > 1e-6 ? glm::normalize(leaving) : glm::dvec2{0.0, 1.0}}; + auto const step{glm::radians(15.0)}; + auto const heading{turned(reference, std::round(signed_angle(reference, offset / length) / step) * step) * length}; + return {last.x + heading.x, Mouse.y, last.z + heading.y}; +} + +editor_mode::track_hover editor_mode::split_spot() const +{ + auto const *selected{selected_track()}; + if (selected == nullptr || ui()->mode() != nodebank_panel::TRACK || ImGui::GetIO().WantCaptureMouse) + return {}; + return track_under_cursor(selected, 20.0f); +} + +alignment::limits editor_mode::laying_norms() const +{ + auto norms{m_route.design.norms}; + norms.checked = false == m_norms_free; + return norms; +} + +bool editor_mode::render_norms_choice() +{ + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(STR_C("Norms:")); + auto changed{false}; + char const *names[]{STR_C("free"), STR_C("PKP PLK")}; + for (int i = 0; i < 2; ++i) + { + ImGui::SameLine(); + auto const chosen{m_norms_free == (i == 0)}; + if (chosen) + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + if (ImGui::Button(names[i], ImVec2(90.0f, 0.0f)) && false == chosen) + { + m_norms_free = i == 0; + m_lay.transitions = false == m_norms_free; + changed = true; + } + if (chosen) + ImGui::PopStyleColor(); + item_tooltip(i == 0 ? "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked" + : "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); + } + if (changed && false == m_route.chain.tracks.empty()) + route_update(); + return changed; +} + +void editor_mode::render_lay_ui() +{ + auto &tool{m_lay}; + render_norms_choice(); + ImGui::Separator(); + ImGui::PushItemWidth(120.0f); + ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Drag to change, Ctrl+click to type the value. While laying: Ctrl+wheel"); + tool.radius = std::max(10.0, tool.radius); + ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f"); + tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); + item_tooltip(m_norms_free ? "Speed limit of the new paths; the speed of the track joined comes first" + : "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows"); + ImGui::PopItemWidth(); + render_layout_combo(); + if (auto *track{selected_track()}; track != nullptr) + { + std::string reason; + if (editor_track::can_edit_geometry(*track, reason)) + render_extend_ui(); + } + if (false == ImGui::TreeNode(STR_C("More settings"))) + { + render_lay_status(); + return; + } + ImGui::PushItemWidth(120.0f); + ImGui::DragScalar(STR_C("Longest path (m)"), ImGuiDataType_Double, &tool.piece_length, 1.0f, nullptr, nullptr, "%.0f"); + tool.piece_length = std::clamp(tool.piece_length, 5.0, 1000.0); + ImGui::PopItemWidth(); + ImGui::Checkbox(STR_C("Parameters of the selected track"), &tool.copy_selected); + item_tooltip("Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins"); + if (false == tool.copy_selected) + { + auto const text_field = [](char const *Label, std::string &Value, char const *Tooltip) { + char buffer[256]; + std::snprintf(buffer, sizeof(buffer), "%s", Value.c_str()); + if (ImGui::InputText(Label, buffer, sizeof(buffer))) + Value = buffer; + item_tooltip(Tooltip); + }; + ImGui::PushItemWidth(-1.0f); + text_field("##layrail", tool.style.rail, STR_C("Texture of the rails")); + text_field("##layballast", tool.style.ballast, STR_C("Texture of the ballast")); + ImGui::PopItemWidth(); + } + ImGui::TreePop(); + render_lay_status(); +} + +void editor_mode::render_lay_status() +{ + auto const &tool{m_lay}; + if (scene::Layers.active() == null_handle) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The scenery isn't opened for editing: new track won't be saved")); + if (tool.active && false == tool.preview_error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", tool.preview_error.c_str()); + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp new file mode 100644 index 00000000..33bbc00e --- /dev/null +++ b/application/editormoderoute.cpp @@ -0,0 +1,1225 @@ +/* +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: +*/ + + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/TrackFeatures.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application/editormodetrackshared.h" + +using namespace track_mode; + +bool editor_mode::route_active() const +{ + return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && 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; +} + +void editor_mode::route_bind_ends() +{ + auto const &chain{m_route.chain}; + auto &design{m_route.design}; + design.start = chain.start; + design.end = chain.end; + design.start_direction = glm::normalize(plan_of(chain.start_direction)); + design.end_direction = glm::normalize(plan_of(chain.end_direction)); + design.start_grade = grade_of(chain.start_direction); + design.end_grade = grade_of(chain.end_direction); + design.start_radius = chain.start_radius; + design.end_radius = chain.end_radius; + design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance)); + design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance)); +} + +void editor_mode::route_reset() +{ + auto &route{m_route}; + route.chain = {}; + route.error.clear(); + route.status.clear(); + route.vertex = -1; + route.grip = -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, true)) + return; + if (false == route.chain.switches.empty()) + route.status = format(STR_C("The fragment runs through %zu switches along their main tracks. They move as a whole with the line, " + "so they have to fall on its straights"), + route.chain.switches.size()); + + auto const &chain{route.chain}; + alignment::design design; + design.norms = route.design.norms; + design.shape = route.design.shape; + design.transition_pieces = route.design.transition_pieces; + design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; + route.design = design; + route_bind_ends(); + design = route.design; + + 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{plan_distance(design.start, design.end)}; + vertex.offset = distance / 3.0; + route.design.vertices.push_back(vertex); + route.design.vertices.push_back(vertex); + } + } + route_update(); +} + +bool editor_mode::route_from_curve(TTrack &Track) +{ + editor_track::curve curve; + if (false == editor_track::find_curve(Track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + m_route.chain = {}; + m_route.result = {}; + m_route.vertex = -1; + m_route.grip = -1; + m_route.error = STR_C("No curve here: LMB on a curve"); + return false; + } + m_route.from = curve.from; + m_route.to = curve.to; + route_reset(); + if (m_route.chain.tracks.empty()) + return false; + auto &design{m_route.design}; + auto vertex{route_vertex_of(curve)}; + m_route.designed = curve.designed.has_value() ? 1 : 0; + if (curve.reversals == 0) + { + design.vertices = {vertex}; + } + else + { + auto const distance{plan_distance(design.start, design.end)}; + vertex.offset = distance / 3.0; + vertex.compound = false; + vertex.reverse_turn = false; + design.vertices = {vertex, vertex}; + } + route_update(); + m_route.status = STR_C("Curve of ") + std::to_string(m_route.chain.tracks.size()) + STR_C(" paths, turning ") + std::to_string(static_cast(std::round(glm::degrees(curve.turn)))) + STR_C(" deg"); + return true; +} + +alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const +{ + if (Curve.designed.has_value()) + return *Curve.designed; + alignment::vertex vertex; + vertex.radius = Curve.radius > 0.0 ? std::max(1.0, Curve.radius) : 1000.0; + vertex.transition_in = std::max(0.0, Curve.transition_in); + vertex.transition_out = std::max(0.0, Curve.transition_out); + vertex.cant = std::max(0.0, Curve.cant); + vertex.reverse_turn = std::abs(Curve.turn) > glm::pi(); + compound_from(Curve, vertex); + return vertex; +} + + +bool editor_mode::route_load(TTrack &Track) +{ + m_route.designed = 0; + if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track))) + return false; + route_read_stored(Track); + return true; +} + +// the design kept in the scenery for the fragment takes the place of the one read from the paths; its chainage is carried on +void editor_mode::route_read_stored(TTrack &Track) +{ + auto &route{m_route}; + route.departed = -1; + route.departure = 0.0; + route.origin = 0.0; + if (route.chain.tracks.empty()) + return; + auto const chain{route.chain}; + bool reversed; + auto index{alignment_store::find(chain.start, chain.end, reversed)}; + route.departed = alignment_store::departed(chain.tracks, route.departure); + auto const loose{std::any_of(chain.tracks.begin(), chain.tracks.end(), [](TTrack const *Track) { return false == alignment_store::on_track(*Track).has_value(); })}; + bool digitized{false}; + int parts{0}; + int skipped{0}; + std::string failure; + if (index < 0 && route.departed < 0 && loose && false == route.single) + { + parts = digitize_parts(chain, &failure, skipped); + if (parts == 1 && skipped == 0) + { + alignment_store::settle(); + index = alignment_store::find(chain.start, chain.end, reversed); + digitized = true; + } + else if (parts > 0) + { + alignment_store::settle(); + route_from_line(Track); + route.departed = alignment_store::departed(route.chain.tracks, route.departure); + } + } + if (index >= 0 && false == reversed && false == route.single) + { + route_take_stored(index); + } + else if (auto const km{alignment_store::chainage_at(chain.start)}) + { + auto const ahead{chain.start + glm::dvec3{route.design.start_direction.x, 0.0, route.design.start_direction.y}}; + auto const further{alignment_store::chainage_at(ahead)}; + if (further.has_value() && *further > *km) + route.origin = *km; + } + if (digitized && route.designed == 0) + route.status += STR_C("\nThe line is digitized now, a straight"); + else if (digitized) + route.status += format(STR_C("\nThe line is digitized now, curves kept in the scenery: %d"), route.designed); + else if (parts > 0) + route.status += format(STR_C("\nThe line is digitized in parts: %d, paths left out: %d"), parts, skipped) + (failure.empty() ? std::string{} : " - " + failure); + else if (skipped > 0 && loose && skipped < static_cast(chain.tracks.size())) + route.status += format(STR_C("\nPaths left out of the digitized line: %d"), skipped) + (failure.empty() ? std::string{} : " - " + failure); + else if (false == failure.empty()) + route.status += STR_C("\nThe line can't be digitized: ") + failure; + else if (route.designed > 0) + route.status += format(STR_C("\nCurves read from the design kept in the scenery: %d"), route.designed); + else if (false == route.design.vertices.empty()) + route.status += STR_C("\nCurves recognized from the paths, the scenery keeps no design for them"); +} + +void editor_mode::route_take_stored(int const Index) +{ + auto &route{m_route}; + auto const &records{alignment_store::records()}; + if (Index < 0 || Index >= static_cast(records.size())) + return; + auto const &stored{records[Index]}; + auto &design{route.design}; + design.vertices = stored.design.vertices; + design.shape = stored.design.shape; + design.speed = stored.design.speed; + design.norms = stored.design.norms; + design.transition_pieces = stored.design.transition_pieces; + design.arc_piece_angle = stored.design.arc_piece_angle; + route.designed = static_cast(design.vertices.size()); + route.origin = stored.origin; + route.vertex = -1; + route.grip = -1; + route_update(); +} + +void editor_mode::render_route_stored() +{ + auto &route{m_route}; + ImGui::PushItemWidth(120.0f); + auto km{route.origin / 1000.0}; + if (ImGui::InputDouble(STR_C("Km of the start"), &km, 0.0, 0.0, "%.3f")) + route.origin = km * 1000.0; + ImGui::PopItemWidth(); + item_tooltip("Chainage of the start of the fragment, kept with the design in the scenery; the vertical profile of these paths counts from it"); + if (ImGui::SmallButton(STR_C("Digitize all the tracks"))) + { + digitize_all(); + return; + } + item_tooltip("Every line of the scenery not digitized yet is kept in it with its straights, arcs and transition curves, the way the drawn tracks are kept; the paths stay as they are"); + auto const &records{alignment_store::records()}; + if (route.departed < 0 || route.departed >= static_cast(records.size())) + return; + auto const &layer{scene::Layers.layer(records[route.departed].layer).name}; + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The design kept in %s for this fragment departs from its paths by up to %.2f m"), layer.c_str(), route.departure); + if (ImGui::SmallButton(STR_C("Take the kept design"))) + { + route_take_stored(route.departed); + route.status = STR_C("The kept design is loaded, applying it lays the paths by it again"); + } + item_tooltip("Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Forget the kept design"))) + { + alignment_store::forget(route.departed); + alignment_store::settle(); + route.departed = -1; + route.status = STR_C("The kept design will be removed from ") + layer + STR_C(" on save"); + } + item_tooltip("The paths stay as they are, the scenery no longer keeps a design for them"); +} + +void editor_mode::route_store() +{ + auto &route{m_route}; + if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) + return; + alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front())); + route.departed = -1; + route.departure = 0.0; +} + +bool editor_mode::digitize_drawn(std::vector const &Created, bool const Alone) +{ + std::vector drawn; + for (auto *track : Created) + if (track != nullptr && track->eType == tt_Normal && false == track->m_editorremoved && false == track->m_paths.empty()) + drawn.push_back(track); + if (drawn.empty() || scene::Layers.empty()) + return false; + editor_track::chain chain; + std::string error; + if (editor_track::find_run(*drawn.front(), kDrawnReach, chain, error) && digitize_chain(chain)) + return true; + if (Alone && editor_track::find_chain(drawn.front(), drawn.back(), chain, error) && chain.tracks.size() == drawn.size() && digitize_chain(chain)) + return true; + WriteLog("Editor: the drawn paths don't make a line of straights, arcs and transition curves, no design kept for them", logtype::generic); + return false; +} + +void editor_mode::digitize_all() +{ + std::set visited; + int kept{0}; + int failed{0}; + std::string first; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || false == editor_track::is_supported(*track) || visited.count(track) > 0) + continue; + if (alignment_store::on_track(*track).has_value()) + continue; + editor_track::chain chain; + std::string error; + if (false == editor_track::find_run(*track, kDrawnReach, chain, error) || chain.tracks.empty()) + { + visited.insert(track); + continue; + } + visited.insert(chain.tracks.begin(), chain.tracks.end()); + std::string reason; + kept += digitize_parts(chain, &reason, failed); + if (first.empty()) + first = reason; + } + alignment_store::settle(); + auto text{format(STR_C("Lines digitized: %d, paths left out: %d"), kept, failed)}; + if (false == first.empty()) + text += STR_C(", the first: ") + first; + WriteLog("Editor: " + text, logtype::generic); + ui()->set_status(text, failed > 0); + if (false == m_route.chain.tracks.empty()) + route_load(*m_route.chain.tracks[m_route.chain.tracks.size() / 2]); +} + +int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *Reason, int &Skipped) +{ + auto const count{Chain.tracks.size()}; + if (count == 0 || std::all_of(Chain.tracks.begin(), Chain.tracks.end(), [](TTrack const *Track) { return alignment_store::on_track(*Track).has_value(); })) + return 0; + TTrack *culprit{nullptr}; + std::string reason; + if (digitize_chain(Chain, &reason, &culprit)) + return 1; + if (count == 1) + { + ++Skipped; + reason = (Chain.tracks.front()->name().empty() ? std::string{"(noname)"} : Chain.tracks.front()->name()) + ": " + reason; + WriteLog("Editor: digitizing - " + reason, logtype::generic); + if (Reason != nullptr && Reason->empty()) + *Reason = reason; + return 0; + } + auto const found{static_cast(std::find(Chain.tracks.begin(), Chain.tracks.end(), culprit) - Chain.tracks.begin())}; + auto const split{found < count ? std::max(1, found) : count / 2}; + int parts{0}; + for (auto const &[first, last] : {std::pair{0, split - 1}, std::pair{split, count - 1}}) + { + editor_track::chain part; + std::string error; + if (editor_track::find_chain(Chain.tracks[first], Chain.tracks[last], part, error) && part.tracks.size() == last - first + 1) + parts += digitize_parts(part, Reason, Skipped); + else + Skipped += static_cast(last - first + 1); + } + return parts; +} + +bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string *Reason, TTrack **Culprit) +{ + if (Chain.tracks.empty()) + return false; + auto const name = [](TTrack const *Track) { return Track == nullptr || Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const nearest = [&](glm::dvec2 const &Point) { + TTrack *found{nullptr}; + double distance{std::numeric_limits::max()}; + for (auto *track : Chain.tracks) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + if (auto const d{glm::distance(plan_of(track->m_paths.front().points[end]), Point)}; d < distance) + { + distance = d; + found = track; + } + return found; + }; + alignment::design design; + design.norms = m_route.design.norms; + design.transition_pieces = m_route.design.transition_pieces; + design.arc_piece_angle = m_route.design.arc_piece_angle; + design.speed = Chain.velocity > 0.0 ? Chain.velocity : 100.0; + design.start = Chain.start; + design.end = Chain.end; + if (glm::length(plan_of(Chain.start_direction)) < 1e-9 || glm::length(plan_of(Chain.end_direction)) < 1e-9) + return false; + design.start_direction = glm::normalize(plan_of(Chain.start_direction)); + design.end_direction = glm::normalize(plan_of(Chain.end_direction)); + design.start_grade = grade_of(Chain.start_direction); + design.end_grade = grade_of(Chain.end_direction); + design.start_radius = Chain.start_radius; + design.end_radius = Chain.end_radius; + auto const preferred{m_route.design.shape}; + auto const other{preferred == alignment::transition_shape::clothoid ? alignment::transition_shape::cubic_parabola : alignment::transition_shape::clothoid}; + alignment::result result; + alignment::design best; + alignment::result bestresult; + double closest{std::numeric_limits::max()}; + TTrack *farthest{nullptr}; + bool fits{false}; + for (auto const attempt : {0, 1, 2, 3}) + { + auto const shape{attempt % 2 == 0 ? preferred : other}; + design.shape = shape; + if (attempt == 2) + { + design.norms.arc_min = 0.0; + design.norms.arc_min_factor = 0.0; + } + design.vertices.clear(); + for (auto const &entry : editor_track::recognize_line(Chain, m_straights.tolerance, design.norms.gauge, shape)) + { + if (entry.kink) + { + if (Reason != nullptr) + *Reason = format(STR_C("the straights meet with no curve at path %s"), name(nearest(entry.position)).c_str()); + if (Culprit != nullptr) + *Culprit = nearest(entry.position); + return false; + } + auto vertex{route_vertex_of(entry.shape)}; + vertex.position = entry.position; + design.vertices.push_back(vertex); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); + design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); + } + result = alignment::compute(design); + if (alignment_store::follows(result, Chain.tracks)) + { + fits = true; + break; + } + TTrack *worst{nullptr}; + if (auto const away{alignment_store::deviation(result, Chain.tracks, worst)}; away < closest) + { + closest = away; + farthest = worst; + best = design; + bestresult = result; + } + } + double fit{0.0}; + if (false == fits && closest <= kDigitizeFit) + { + design = best; + result = bestresult; + fit = closest; + fits = true; + } + if (false == fits) + { + if (Culprit != nullptr) + *Culprit = farthest; + if (Reason != nullptr) + *Reason = closest < std::numeric_limits::max() ? format(STR_C("the curves read from the paths leave path %s by %.2f m"), name(farthest).c_str(), closest) : std::string{STR_C("the paths don't make a line of straights, arcs and transition curves")}; + return false; + } + auto const &records{alignment_store::records()}; + std::set covered; + for (auto *track : Chain.tracks) + if (auto const found{alignment_store::on_track(*track)}) + covered.insert(found->record); + double origin{0.0}; + for (auto const index : covered) + { + auto const &old{records[index]}; + auto const start{alignment_store::chainage_on(result, old.design.start)}; + auto const end{alignment_store::chainage_on(result, old.design.end)}; + if (false == start.has_value() || false == end.has_value()) + return false; + if (index == *covered.begin() && *end > *start) + origin = old.origin - *start; + } + auto const layer{covered.empty() ? alignment_store::layer_of(*Chain.tracks.front()) : records[*covered.begin()].layer}; + for (auto index{covered.rbegin()}; index != covered.rend(); ++index) + alignment_store::forget(*index); + alignment_store::store(design, result, origin, layer, "drawn", fit); + return true; +} + +bool editor_mode::route_from_line(TTrack &Track) +{ + auto &route{m_route}; + editor_track::chain run; + if (false == editor_track::find_run(Track, kLineReach, run, route.error)) + return false; + route.from = run.tracks.front(); + route.to = run.tracks.back(); + route_reset(); + if (route.chain.tracks.empty()) + return false; + auto &design{route.design}; + auto const found{editor_track::recognize_line(route.chain, m_straights.tolerance, design.norms.gauge, design.shape)}; + route.designed = 0; + if (false == found.empty()) + { + design.vertices.clear(); + for (auto const &entry : found) + { + alignment::vertex vertex; + if (entry.kink) + { + vertex.radius = std::max(10.0, std::round(alignment::recommend(design.speed, 1000.0, design.norms).radius_min / 10.0) * 10.0); + vertex.transition_in = 0.0; + vertex.transition_out = 0.0; + vertex.cant = 0.0; + } + else + { + vertex = route_vertex_of(entry.shape); + if (entry.shape.designed.has_value()) + ++route.designed; + } + vertex.position = entry.position; + design.vertices.push_back(vertex); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); + design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); + } + route_update(); + } + route.status = format(STR_C("Line of %zu paths, %.0f m, %zu vertices"), route.chain.tracks.size(), route.chain.length, design.vertices.size()); + return true; +} + +void editor_mode::route_apply() +{ + auto &route{m_route}; + route.status.clear(); + if (false == route.result.valid || route.chain.tracks.empty()) + return; + editor_track::chain chain; + if (false == editor_track::find_chain(route.from, route.to, chain, route.error, true)) + return; + if (glm::distance(chain.start, route.chain.start) > 1e-3 || glm::distance(chain.end, route.chain.end) > 1e-3) + { + route.error = STR_C("The fragment changed since it was selected, select it again"); + return; + } + auto involved{chain.tracks}; + for (auto const &passage : chain.switches) + involved.push_back(passage.track); + if (false == tracks_editable(involved, route.error)) + return; + + std::vector> states; + std::vector created; + // takes back what was done before the paths were re-laid + auto const undo = [&]() { + for (auto *track : created) + editor_track::retire(*track); + for (auto entry = states.rbegin(); entry != states.rend(); ++entry) + { + editor_track::apply(*entry->first, entry->second); + editor_track::commit({entry->first}); + } + }; + auto *from{route.from}; + auto *to{route.to}; + auto const startextension{route.result.start_extension}; + auto const endextension{route.result.end_extension}; + if (startextension > 0.0 || endextension > 0.0) + { + auto const before{chain}; + if (startextension > 0.0) + from = editor_track::take_straight(editor_track::straight_beyond(before, false, m_straights.tolerance), from, before.start, route.result.start, states, created); + if (endextension > 0.0) + to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); + std::string error; + auto const found{editor_track::find_chain(from, to, chain, error, true)}; + auto const startoff{found ? plan_distance(chain.start, route.result.start) : 0.0}; + auto const endoff{found ? plan_distance(chain.end, route.result.end) : 0.0}; + if (false == found || startoff > 5.0 || endoff > 5.0) + { + auto const startrun{editor_track::straight_beyond(before, false, m_straights.tolerance)}; + auto const endrun{editor_track::straight_beyond(before, true, m_straights.tolerance)}; + WriteLog(format("editor: cutting the straights failed: found %d, start %.2f m off (extension %.2f, run %zu paths %.2f m), end %.2f m off (extension %.2f, run %zu paths %.2f m), chain %zu paths, from %s, to %s, error '%s'", found ? 1 : 0, startoff, + startextension, startrun.tracks.size(), startrun.length, endoff, endextension, endrun.tracks.size(), endrun.length, chain.tracks.size(), from != nullptr ? from->name().c_str() : "-", to != nullptr ? to->name().c_str() : "-", error.c_str())); + undo(); + route.error = error.empty() ? format(STR_C("The adjoining straights can't be cut at the ends of the curve: the start misses by %.1f m, the end by %.1f m"), startoff, endoff) : error; + return; + } + if (false == tracks_editable(chain.tracks, route.error)) + { + undo(); + return; + } + } + for (auto *track : chain.tracks) + if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) + states.emplace_back(track, editor_track::capture(*track)); + std::vector relaid; + if (chain.switches.empty()) + { + auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + editor_track::keep_heights(chain, pieces); + relaid = editor_track::relay(chain, pieces, &to); + } + else if (false == route_relay_through_switches(chain, states, relaid, to)) + { + undo(); + return; + } + // the straights were cut along their own direction, the curve ends where the design says: the ends are brought together + auto const added{relaid.size()}; + created.insert(created.end(), relaid.begin(), relaid.end()); + push_track_snapshot(std::move(states), std::move(created)); + + route.from = from; + route.to = to; + editor_track::find_chain(route.from, route.to, route.chain, route.error, true); + if (startextension > 0.0 || endextension > 0.0) + { + route_bind_ends(); + auto &vertices{route.design.vertices}; + if (vertices.size() >= 2) + { + vertices.front().offset += startextension; + vertices.back().offset += endextension; + } + route_update(); + } + route_store(); + records_to_history(); + route.status = STR_C("Re-laid ") + std::to_string(chain.tracks.size()) + STR_C(" paths") + (added > 0 ? STR_C(", added ") + std::to_string(added) : std::string{}); + if (startextension > 0.0 || endextension > 0.0) + route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); + WriteLog("Editor: route design - " + route.status, logtype::generic); +} + +// the switches of the chain are moved as a whole onto the designed line, the regular paths between them re-laid +bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created, TTrack *&Last) +{ + auto &route{m_route}; + auto const &result{route.result}; + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const plan = [](glm::dvec3 const &Vector) { return glm::normalize(plan_of(Vector)); }; + struct placement + { + TTrack *track; + double from, length; // chainage along the new line + glm::dvec3 entry; // of the main track, where the chain comes in + double angle; + }; + // chainages along the old line, the switches keep their distance from the nearer end of the chain + double before{0.0}; + std::vector starts; + std::size_t next{0}; + for (std::size_t i = 0; i <= Chain.tracks.size(); ++i) + { + for (; next < Chain.switches.size() && Chain.switches[next].before == i; ++next) + { + starts.push_back(before); + before += bezier{Chain.switches[next].track->m_paths.front()}.plan_length(); + } + if (i < Chain.tracks.size()) + before += bezier{Chain.tracks[i]->m_paths.front()}.plan_length(); + } + auto const oldlength{before}; + std::vector placements; + std::vector breaks; + for (std::size_t k = 0; k < Chain.switches.size(); ++k) + { + auto const &passage{Chain.switches[k]}; + auto const &main{passage.track->m_paths.front()}; + auto const length{bezier{main}.plan_length()}; + auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])}; + if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01)) + { + route.error = STR_C("The switch ") + name(passage.track) + STR_C(" doesn't fit on the designed line"); + return false; + } + auto const entry{main.points[passage.forward ? segment_data::point::start : segment_data::point::end]}; + auto const exit{main.points[passage.forward ? segment_data::point::end : segment_data::point::start]}; + bezier const curve{main}; + auto const direction{passage.forward ? plan(curve.first(0.0)) : -plan(curve.first(1.0))}; + auto const first{alignment::evaluate(result, from)}; + auto const last{alignment::evaluate(result, from + length)}; + auto const angle{signed_angle(direction, plan(first.direction))}; + // where the other end of the main track goes, set against the line + auto const moved{plan_of(first.position) + turned(plan_of(exit - entry), angle)}; + auto const departure{glm::distance(moved, plan_of(last.position))}; + if (departure > 0.02) + { + route.error = format(STR_C("The switch %s would leave the designed line by %.2f m at its other end: it lies in a curve of the design. " + "Move the vertices so that the switch falls on a straight"), + name(passage.track).c_str(), departure); + return false; + } + placements.push_back({passage.track, from, length, entry, angle}); + breaks.push_back(from); + breaks.push_back(from + length); + } + // the regular parts between the switches, one interval each, the switches taking the ones between + auto const parts{editor_track::chain_parts(Chain)}; + std::vector counts; + std::vector partof; + std::size_t part{0}; + for (std::size_t k = 0; k <= Chain.switches.size(); ++k) + { + auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before}; + auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before}; + counts.push_back(last - first); + partof.push_back(last > first ? static_cast(part++) : -1); + if (k < Chain.switches.size()) + counts.push_back(1); + } + if (part != parts.size()) + { + route.error = STR_C("The paths between the switches can't be read as a chain"); + return false; + } + std::vector intervals; + auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)}; + std::vector> groups(parts.size()); + for (std::size_t i = 0; i < pieces.size(); ++i) + if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0) + groups[partof[intervals[i] / 2]].push_back(pieces[i]); + for (std::size_t i = 0; i < parts.size(); ++i) + if (groups[i].size() < parts[i].tracks.size()) + { + route.error = STR_C("The designed line between the switches is too short for the paths there"); + return false; + } + + // all checked, the changes go in + auto const remember = [&](TTrack *Track) { + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; })) + States.emplace_back(Track, editor_track::capture(*Track)); + }; + for (auto const &place : placements) + { + auto joints{editor_track::joints(*place.track)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return std::find(Chain.tracks.begin(), Chain.tracks.end(), Joint.other) != Chain.tracks.end(); }), joints.end()); + remember(place.track); + std::vector changed{place.track}; + for (auto const &joint : joints) + { + remember(joint.other); + if (std::find(changed.begin(), changed.end(), joint.other) == changed.end()) + changed.push_back(joint.other); + } + // the heights stay, the paths between the switches keep theirs too + auto target{alignment::evaluate(result, place.from).position}; + target.y = place.entry.y; + editor_track::place(*place.track, place.entry, target, place.angle); + editor_track::follow(*place.track, joints); + editor_track::commit(changed); + } + for (std::size_t i = 0; i < parts.size(); ++i) + { + auto group{groups[i]}; + editor_track::keep_heights(parts[i], group); + TTrack *last{nullptr}; + auto const added{editor_track::relay(parts[i], group, &last)}; + if (last != nullptr && parts[i].tracks.back() == Chain.tracks.back()) + Last = last; + Created.insert(Created.end(), added.begin(), added.end()); + } + return true; +} + +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; + + auto const height{Vertex < static_cast(result.vertex_elevations.size()) ? result.vertex_elevations[Vertex] : design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0)}; + return {position.x, height, position.y}; +} + +bool editor_mode::route_hit(int &Vertex, int &Grip) const +{ + if (false == route_active()) + return false; + screen_projection const projection; + float best = kHandleRadius * kHandleRadius; + int hit{-1}; + for (int i = 0; i < static_cast(m_route.design.vertices.size()); ++i) + { + if (auto const distance{projection.mouse_distance2(route_vertex_position(i))}; distance < best) + { + best = distance; + hit = i; + } + } + int griphit{-1}; + for (int i = 0; i < static_cast(m_route.result.vertex_chainages.size()) && m_route.result.length > 0.0; ++i) + { + if (auto const distance{projection.mouse_distance2(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position)}; distance < best) + { + best = distance; + griphit = i; + hit = -1; + } + } + Vertex = hit; + Grip = griphit; + return hit >= 0 || griphit >= 0; +} + +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(ImGui::GetMainViewport()); + auto const &start{result.valid ? result.start : design.start}; + auto const &end{result.valid ? result.end : design.end}; + + std::vector polygon{start}; + for (int i = 0; i < static_cast(design.vertices.size()); ++i) + polygon.push_back(route_vertex_position(i)); + polygon.push_back(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); + + 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 ? overlay_color::grip : 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, overlay_color::highlight); + } + } + + for (auto const atend : {false, true}) + { + auto const &point{atend ? end : 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, overlay_color::marked); + if (i == route.vertex) + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); + if (i < static_cast(result.vertex_chainages.size()) && result.length > 0.0 && projection.project(alignment::evaluate(result, result.vertex_chainages[i]).position, screen)) + { + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::grip, 4); + if (i == route.grip) + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); + auto const *report{i < static_cast(result.curves.size()) ? &result.curves[i] : nullptr}; + auto radius{format("R %.0f", report != nullptr ? report->radius : design.vertices[i].radius)}; + if (report != nullptr && report->radii.size() >= 2) + { + radius = "R"; + for (std::size_t k = 0; k < report->radii.size(); ++k) + radius += format(k == 0 ? " %.0f" : " / %.0f", report->radii[k]); + } + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), overlay_color::grip, radius.c_str()); + } + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); + } +} + +void editor_mode::render_route_gizmo() +{ + auto &route{m_route}; + auto const count{static_cast(route.design.vertices.size())}; + bool const grip{route.grip >= 0 && route.grip < count && route.grip < static_cast(route.result.vertex_chainages.size())}; + if (false == grip && (route.vertex < 0 || route.vertex >= count)) + { + m_route_dragging = false; + return; + } + + auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)}; + glm::dvec3 moved{position}; + if (false == handle_drag_step(moved)) + { + if (m_route_dragging) + { + m_route_dragging = false; + route_apply(); + } + return; + } + m_route_dragging = true; + if (grip) + { + auto &vertex{route.design.vertices[route.grip]}; + auto const corner{route_vertex_position(route.grip)}; + auto const distance{plan_distance(moved, corner)}; + for (auto const &curve : route.result.curves) + { + if (curve.vertex != route.grip || vertex.reverse_turn) + continue; + auto const secant{1.0 / std::cos(curve.deflection * 0.5) - 1.0}; + if (secant > 1e-6) + vertex.radius = std::max(10.0, std::round(distance / secant)); + } + route_update(); + return; + } + auto &vertex{route.design.vertices[route.vertex]}; + glm::dvec2 const point{moved.x, moved.z}; + if (count >= 2) + { + if (route.vertex == 0) + vertex.offset = std::max(0.1, glm::dot(point - plan_of(route.design.start), glm::normalize(route.design.start_direction))); + else if (route.vertex == count - 1) + vertex.offset = std::max(0.1, glm::dot(plan_of(route.design.end) - point, glm::normalize(route.design.end_direction))); + else + vertex.position = point; + } + route_update(); +} + +void editor_mode::render_route_ui() +{ + auto &route{m_route}; + render_norms_choice(); + ImGui::Separator(); + if (false == route.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); + if (route.chain.tracks.empty()) + return; + + ImGui::TextDisabled(STR_C("%zu paths, %.2f m"), route.chain.tracks.size(), route.chain.length); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Reset design"))) + { + auto *track{route.chain.tracks[route.chain.tracks.size() / 2]}; + route_load(*track); + return; + } + item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); + render_route_stored(); + auto changed{render_route_vertices()}; + if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) + changed |= render_route_parameters(); + if (changed) + route_update(); + + ImGui::Separator(); + render_route_result(); +} + +bool editor_mode::render_route_parameters() +{ + auto &design{m_route.design}; + bool changed{false}; + ImGui::PushItemWidth(120.0f); + changed |= ImGui::InputDouble(STR_C("Design speed (km/h)"), &design.speed, 0.0, 0.0, "%.0f"); + item_tooltip("Speed the cant, the transitions and the recommended radius are designed for"); + design.speed = std::max(1.0, design.speed); + int shape = static_cast(design.shape); + char const *shapes[] = {STR_C("Cubic parabola"), STR_C("Clothoid")}; + if (ImGui::Combo(STR_C("Transition curve"), &shape, shapes, IM_ARRAYSIZE(shapes))) + { + design.shape = static_cast(shape); + changed = true; + } + item_tooltip("Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length"); + ImGui::PopItemWidth(); + + if (ImGui::TreeNode(STR_C("Limits"))) + { + auto &norms{design.norms}; + ImGui::PushItemWidth(100.0f); + changed |= ImGui::InputDouble(STR_C("Rail axes distance (mm)"), &norms.gauge, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &norms.unbalanced, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble(STR_C("Maximum cant (mm)"), &norms.cant_max, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &norms.cant_rate, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &norms.ramp_factor, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &norms.jerk, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble(STR_C("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(); + } + return changed; +} + +bool editor_mode::render_route_vertices() +{ + bool changed{false}; + auto const count{static_cast(m_route.design.vertices.size())}; + for (int i = 0; i < count; ++i) + { + ImGui::PushID(i); + changed |= render_route_vertex(i); + ImGui::PopID(); + } + return changed; +} + +bool editor_mode::render_route_vertex(int const Index) +{ + auto &route{m_route}; + auto &design{route.design}; + auto &vertex{design.vertices[Index]}; + auto const count{static_cast(design.vertices.size())}; + bool changed{false}; + if (count > 1) + ImGui::TextDisabled(STR_C("Curve %d of the S-curve"), Index + 1); + ImGui::PushItemWidth(110.0f); + changed |= ImGui::DragScalar(vertex.compound ? STR_C("Radius R1 (m)") : STR_C("Radius R (m)"), ImGuiDataType_Double, &vertex.radius, 5.0f, nullptr, nullptr, "%.0f"); + if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound)) + { + changed = true; + if (vertex.compound && vertex.arcs.empty()) + vertex.arcs.push_back({std::max(10.0, std::round(vertex.radius * 0.6 / 10.0) * 10.0), 0.0, 1.0}); + } + item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions"); + if (vertex.compound) + { + auto total{std::max(0.0, vertex.share)}; + for (auto const &arc : vertex.arcs) + total += std::max(0.0, arc.share); + auto const percent = [&](double const Share) { return total > 0.0 ? 100.0 * std::max(0.0, Share) / total : 0.0; }; + changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), 1).c_str(), ImGuiDataType_Double, &vertex.share, 0.01f, nullptr, nullptr, "%.2f"); + ImGui::SameLine(); + ImGui::TextDisabled("%.0f%%", percent(vertex.share)); + int removed{-1}; + for (int k = 0; k < static_cast(vertex.arcs.size()); ++k) + { + auto &arc{vertex.arcs[k]}; + ImGui::PushID(k); + ImGui::Separator(); + changed |= ImGui::DragScalar(format(STR_C("Radius R%d (m)"), k + 2).c_str(), ImGuiDataType_Double, &arc.radius, 5.0f, nullptr, nullptr, "%.0f"); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) + removed = k; + item_tooltip("Takes this radius out of the curve"); + changed |= ImGui::DragScalar(format(STR_C("Transition R%d-R%d (m)"), k + 1, k + 2).c_str(), ImGuiDataType_Double, &arc.transition, 1.0f, nullptr, nullptr, "%.0f"); + changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f"); + ImGui::SameLine(); + ImGui::TextDisabled("%.0f%%", percent(arc.share)); + arc.radius = std::max(1.0, arc.radius); + arc.transition = std::max(0.0, arc.transition); + arc.share = std::max(0.0, arc.share); + ImGui::PopID(); + } + vertex.share = std::max(0.0, vertex.share); + if (removed >= 0) + { + vertex.arcs.erase(vertex.arcs.begin() + removed); + if (vertex.arcs.empty()) + vertex.compound = false; + changed = true; + } + if (ImGui::SmallButton(STR_C("Add a radius"))) + { + auto const last{vertex.arcs.empty() ? vertex.radius : vertex.arcs.back().radius}; + vertex.arcs.push_back({std::max(10.0, std::round(last * 0.8 / 10.0) * 10.0), 0.0, 1.0}); + changed = true; + } + item_tooltip("Another arc after the last one, the angle of the curve is shared out by the shares"); + ImGui::Separator(); + } + changed |= ImGui::DragScalar(STR_C("Transition in (m)"), ImGuiDataType_Double, &vertex.transition_in, 1.0f, nullptr, nullptr, "%.0f"); + changed |= ImGui::DragScalar(STR_C("Transition out (m)"), ImGuiDataType_Double, &vertex.transition_out, 1.0f, nullptr, nullptr, "%.0f"); + changed |= ImGui::DragScalar(STR_C("Cant (mm)"), ImGuiDataType_Double, &vertex.cant, 1.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Height of the outer rail above the inner one"); + 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); + if (false == m_norms_free) + { + if (ImGui::SmallButton(STR_C("Apply recommended for V"))) + { + route_recommend(vertex); + changed = true; + } + item_tooltip("Cant and transitions recommended for the design speed and the radius"); + auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; + ImGui::TextDisabled(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); + } + if (ImGui::TreeNode(STR_C("Details"))) + { + for (auto const &curve : route.result.curves) + { + if (curve.vertex != Index) + continue; + ImGui::Text(STR_C("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(STR_C("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); + } + changed |= ImGui::Checkbox(STR_C("Turn the longer way (over 180 deg)"), &vertex.reverse_turn); + item_tooltip("For the loops: the curve turns by more than a half circle"); + ImGui::TreePop(); + } + return changed; +} + +void editor_mode::render_route_result() +{ + auto const &route{m_route}; + auto const &design{route.design}; + auto const &result{route.result}; + if (result.length > 0.0) + { + auto const pieces{alignment::minimum_pieces(result, design)}; + ImGui::Text(STR_C("Length %.2f m (now %.2f m), %zu elements"), result.length, route.chain.length, result.elements.size()); + ImGui::TextDisabled(STR_C("Paths: %zu in the fragment, %zu needed%s"), route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? STR_C(" (copies of the neighbours will be added)") : ""); + if (result.start_extension > 0.0 || result.end_extension > 0.0) + ImGui::Text(STR_C("Takes over %.2f m of the straight at the start, %.2f m at the end"), result.start_extension, result.end_extension); + } + 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(STR_C("Elements"))) + { + ImGui::TextDisabled(STR_C("Straight available beyond the ends: %.2f / %.2f m"), design.start_reserve, design.end_reserve); + char const *kinds[] = {"straight", "transition", "curve", "transition", "spiral"}; + for (auto const &e : result.elements) + ImGui::Text("km %s %-10s L %8.2f m%s", profile::format_chainage(m_route.origin + e.chainage).c_str(), 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) + { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); + if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) + route_apply(); + ImGui::PopStyleColor(2); + } + else + { + ImGui::TextDisabled(STR_C("Correct the errors to apply the design")); + } + if (false == route.status.empty()) + ImGui::TextUnformatted(route.status.c_str()); +} + +editor_track::straight const &editor_mode::current_straight() +{ + auto *track{selected_track()}; + if (track != m_straights.current_for) + { + m_straights.current_for = track; + m_straights.handle = -1; + m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{}; + find_neighbour_straights(); + } + return m_straights.current; +} diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp new file mode 100644 index 00000000..f68114fc --- /dev/null +++ b/application/editormodestraights.cpp @@ -0,0 +1,1270 @@ +/* +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: +*/ + + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/TrackFeatures.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application/editormodetrackshared.h" + +using namespace track_mode; + +void editor_mode::draw_straights_overlay() const +{ + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + glm::dvec3 const camera{Global.pCamera.Pos}; + auto const nearby = [&](editor_track::straight const &Line) { + auto const offset{camera - Line.start}; + auto const along{std::clamp(glm::dot(plan_of(offset), Line.direction), 0.0, Line.length)}; + auto const closest{Line.start + glm::dvec3{Line.direction.x, 0.0, Line.direction.y} * along}; + return plan_distance(closest, camera) < 3000.0; + }; + auto const draw = [&](editor_track::straight const &Line, ImU32 const Color, float const Thickness) { + if (Line.tracks.empty()) + return; + projection.line(drawlist, Line.start, Line.end, Color, Thickness); + for (auto const &point : {Line.start, Line.end}) + { + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircleFilled(screen, Thickness + 2.0f, Color); + } + }; + for (int i = 0; i < static_cast(m_straights.found.size()); ++i) + { + auto const &line{m_straights.found[i]}; + if (nearby(line)) + draw(line, i == m_straights.listed ? overlay_color::selected : IM_COL32(255, 255, 255, 110), i == m_straights.listed ? 4.0f : 2.0f); + } + for (auto const &member : m_straights.set) + draw(member, IM_COL32(255, 150, 30, 255), 4.0f); + draw(m_straights.current, overlay_color::selected, 4.0f); + if (m_straights.tool_placed) + { + auto const &state{m_straights}; + auto const &line{state.tool_line}; + ImVec2 screen; + if (projection.project(state.tool_point, screen)) + drawlist->AddCircleFilled(screen, 7.0f, overlay_color::invalid); + if (projection.project(state.tool_handle, screen)) + drawlist->AddCircleFilled(screen, 7.0f, overlay_color::marked); + if (state.tool == 1) + { + auto const direction{plan_of(state.tool_handle - state.tool_point)}; + if (glm::length(direction) > 1e-3) + { + auto const unit{glm::normalize(direction)}; + auto const rest{std::max(0.0, line.length - state.tool_at)}; + projection.line(drawlist, state.tool_point, state.tool_point + glm::dvec3{unit.x, line.grade, unit.y} * rest, overlay_color::marked, 3.0f); + } + } + else + { + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; + auto const shift{glm::dot(offset, normal)}; + auto const length{glm::dot(offset, line.direction)}; + auto const shifted{glm::dvec3{normal.x, 0.0, normal.y} * shift}; + auto const along{glm::dvec3{line.direction.x, 0.0, line.direction.y}}; + projection.line(drawlist, state.tool_point, state.tool_point + along * length + shifted, IM_COL32(255, 210, 60, 160), 2.0f); + projection.line(drawlist, state.tool_point + along * length + shifted, line.end + shifted, overlay_color::marked, 3.0f); + } + } + if (m_straights.dragging && m_straights.fitting) + { + auto const &result{m_route.result}; + if (result.length > 0.0 && result.profile.size() >= 2) + { + int const steps{std::clamp(static_cast(result.length / 2.0), 2, 2000)}; + auto previous{alignment::evaluate(result, 0.0).position}; + for (int i = 1; i <= steps; ++i) + { + auto const next{alignment::evaluate(result, result.length * i / steps).position}; + projection.line(drawlist, previous, next, result.valid ? overlay_color::marked : overlay_color::invalid, 3.0f); + previous = next; + } + } + } + else if (m_straights.dragging) + { + for (auto const &member : m_straights.drag_lines) + { + for (auto const *track : member.tracks) + { + auto const &path{track->m_paths.front()}; + projection.line(drawlist, path.points[segment_data::point::start], path.points[segment_data::point::end], overlay_color::marked, 3.0f); + } + } + } + else if (false == m_straights.current.tracks.empty()) + { + auto const &line{m_straights.current}; + glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; + for (int i = 0; i < 3; ++i) + { + ImVec2 screen; + if (false == projection.project(handles[i], screen)) + continue; + if (i < 2) + drawlist->AddRectFilled(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), overlay_color::selected); + else + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::selected, 4); + if (i == m_straights.handle) + drawlist->AddCircle(screen, 12.0f, overlay_color::highlight, 16, 2.5f); + } + } + auto line{m_straights.dragging ? m_straights.drag_line : m_straights.current}; + if (line.tracks.empty()) + return; + glm::dvec3 const offset{m_straights.dragging ? m_straights.preview_start - m_straights.drag_line.start : glm::dvec3{}}; + if (m_straights.dragging) + { + line.start = m_straights.preview_start; + line.end = m_straights.preview_end; + auto const planar{plan_of(line.end - line.start)}; + auto const length{glm::length(planar)}; + if (length > 1e-6) + { + line.direction = planar / length; + line.length = length; + } + } + auto const offer{straight_parallel_offer(m_straights.dragging ? m_straights.drag_line : m_straights.current, offset)}; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + for (auto const &other : m_straights.neighbours) + { + auto const across = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - line.start), normal); }; + auto const a0{line.along(other.start)}, a1{line.along(other.end)}; + auto const from{std::max(0.0, std::min(a0, a1))}; + auto const to{std::min(line.length, std::max(a0, a1))}; + if (to <= from) + continue; + auto const middle{(from + to) * 0.5}; + auto const fraction{std::abs(a1 - a0) > 1e-6 ? (middle - a0) / (a1 - a0) : 0.0}; + auto const lateral{across(other.start) + (across(other.end) - across(other.start)) * fraction}; + glm::dvec3 const foot{line.start.x + line.direction.x * middle, line.start.y + line.grade * middle, line.start.z + line.direction.y * middle}; + glm::dvec3 const target{foot.x + normal.x * lateral, foot.y, foot.z + normal.y * lateral}; + bool const highlighted{offer.nearby && offer.neighbour == &other}; + auto const tick{highlighted ? (offer.snaps ? IM_COL32(80, 230, 255, 255) : IM_COL32(255, 210, 60, 255)) : IM_COL32(255, 230, 120, 230)}; + if (highlighted) + projection.line(drawlist, other.start, other.end, tick, 3.0f); + projection.line(drawlist, foot, target, tick, highlighted ? 3.0f : 1.5f); + ImVec2 screen; + if (projection.project((foot + target) * 0.5, screen)) + { + auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))}; + auto const text{highlighted ? (offer.snaps ? format(STR_C("parallel %.2f m"), offer.spacing) : format("%.2f → %.2f m %.1f°", std::abs(lateral), offer.spacing, offer.angle)) : + (angle > 0.05 ? format("%.2f m %.1f°", std::abs(lateral), angle) : format("%.2f m", std::abs(lateral)))}; + auto const *label{text.c_str()}; + auto const size{ImGui::CalcTextSize(label)}; + drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 4.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 190), 3.0f); + drawlist->AddText(screen, highlighted ? tick : IM_COL32(255, 230, 120, 255), label); + } + } + if (offer.nearby && false == offer.snaps && std::abs(offer.correction) > 0.02) + { + auto const shift{glm::dvec3{normal.x, 0.0, normal.y} * offer.correction}; + projection.line(drawlist, line.start + shift, line.end + shift, IM_COL32(80, 230, 255, 210), 3.5f); + } +} + +void editor_mode::find_neighbour_straights() +{ + auto &state{m_straights}; + state.neighbours.clear(); + auto const &line{state.current}; + if (line.tracks.empty()) + return; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const middle{(line.start + line.end) * 0.5}; + std::vector visited(line.tracks.begin(), line.tracks.end()); + auto const sections{simulation::Region->sections(middle, static_cast(line.length * 0.5 + 60.0))}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *track : cell.m_paths) + { + if (std::find(visited.begin(), visited.end(), track) != visited.end() || false == editor_track::is_straight(*track, state.tolerance)) + continue; + auto other{editor_track::find_straight(*track, state.tolerance)}; + visited.insert(visited.end(), other.tracks.begin(), other.tracks.end()); + if (other.tracks.empty() || std::abs(line.direction.x * other.direction.y - line.direction.y * other.direction.x) > std::sin(glm::radians(kParallelFindAngle))) + continue; + auto const offset{glm::dvec2{(other.start + other.end).x * 0.5 - line.start.x, (other.start + other.end).z * 0.5 - line.start.z}}; + if (std::abs(glm::dot(offset, normal)) > 60.0) + continue; + auto const a0{line.along(other.start)}; + auto const a1{line.along(other.end)}; + if (std::max(a0, a1) <= 0.0 || std::min(a0, a1) >= line.length) + continue; + state.neighbours.push_back(std::move(other)); + } + } + } +} + +editor_mode::parallel_offer editor_mode::straight_parallel_offer(editor_track::straight const &Line, glm::dvec3 const &Offset) const +{ + parallel_offer offer; + glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; + auto const middle{(Line.start + Line.end) * 0.5 + Offset}; + double best{kParallelHintWindow}; + for (auto const &other : m_straights.neighbours) + { + auto const sine{std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x)}; + if (sine > std::sin(glm::radians(kParallelFindAngle))) + continue; + auto const distance{glm::dot(plan_of(other.start - middle), normal)}; + auto const angle{glm::degrees(std::abs(std::asin(std::clamp(sine, 0.0, 1.0))))}; + for (auto const spacing : m_straights.spacings) + { + auto const error{std::abs(std::abs(distance) - spacing)}; + if (error < best) + { + best = error; + offer.nearby = true; + offer.snaps = error < kParallelSnapWindow && angle <= kParallelSnapAngle; + offer.spacing = spacing; + offer.distance = std::abs(distance); + offer.angle = angle; + offer.correction = distance > 0.0 ? std::abs(distance) - spacing : spacing - std::abs(distance); + offer.neighbour = &other; + } + } + } + return offer; +} + +glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const +{ + auto const offer{straight_parallel_offer(Line, Offset)}; + if (false == offer.snaps) + return Offset; + glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; + return Offset + glm::dvec3{normal.x, 0.0, normal.y} * offer.correction; +} + +glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const +{ + glm::dvec2 const offset{Moved.x - Pivot.x, Moved.z - Pivot.z}; + auto const length{glm::length(offset)}; + if (length < 1e-3) + return Moved; + auto const direction{offset / length}; + for (auto const &other : m_straights.neighbours) + { + auto const sine{direction.x * other.direction.y - direction.y * other.direction.x}; + if (std::abs(sine) > std::sin(glm::radians(kParallelSnapAngle))) + continue; + auto const aligned{other.direction * (glm::dot(direction, other.direction) < 0.0 ? -1.0 : 1.0)}; + return {Pivot.x + aligned.x * length, Moved.y, Pivot.z + aligned.y * length}; + } + return Moved; +} + +void editor_mode::render_straights_ui() +{ + auto &state{m_straights}; + ImGui::TextDisabled(STR_C("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to")); + + ImGui::PushItemWidth(100.0f); + double angle{glm::degrees(state.tolerance.angle)}; + if (ImGui::InputDouble(STR_C("Angle tolerance (deg)"), &angle, 0.0, 0.0, "%.4f")) + state.tolerance.angle = glm::radians(std::max(0.0, angle)); + item_tooltip("Largest change of the direction between the paths of one straight"); + ImGui::InputDouble(STR_C("Offset tolerance (m)"), &state.tolerance.offset, 0.0, 0.0, "%.3f"); + item_tooltip("Largest distance of the paths from the common axis of the straight"); + ImGui::InputDouble(STR_C("Treat as straight above R (m)"), &state.tolerance.radius, 0.0, 0.0, "%.0f"); + item_tooltip("Curves of a larger radius count as straight"); + ImGui::InputDouble(STR_C("Minimum length (m)"), &state.minimum_length, 0.0, 0.0, "%.0f"); + item_tooltip("Shorter straights aren't listed"); + ImGui::PopItemWidth(); + if (ImGui::Button(STR_C("Recognize straights in the scenery"))) + { + state.found = editor_track::find_straights(state.minimum_length, state.tolerance); + state.listed = -1; + state.current_for = nullptr; + } + if (false == state.found.empty()) + { + ImGui::Text(STR_C("%zu straights"), state.found.size()); + ImGui::SameLine(); + ImGui::PushItemWidth(150.0f); + ImGui::InputTextWithHint("##straightfilter", STR_C("path name"), state.filter, sizeof(state.filter)); + ImGui::PopItemWidth(); + ImGui::BeginChild("##straights", ImVec2(0.0f, 160.0f), true); + for (int i = 0; i < static_cast(state.found.size()); ++i) + { + auto const &line{state.found[i]}; + if (state.filter[0] != '\0' && std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return Track->name().find(state.filter) != std::string::npos; })) + continue; + auto const label{std::to_string(i + 1) + ". " + describe(line) + "##straight" + std::to_string(i)}; + if (ImGui::Selectable(label.c_str(), state.listed == i)) + { + state.listed = i; + m_node = line.tracks[line.tracks.size() / 2]; + ui()->set_node(m_node); + start_focus(m_node); + } + } + ImGui::EndChild(); + } + +} + +void editor_mode::render_straight_ui() +{ + auto &state{m_straights}; + auto const &line{current_straight()}; + if (line.tracks.empty()) + { + if (false == state.status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); + return; + } + ImGui::TextUnformatted(describe(line).c_str()); + if (false == m_straights.neighbours.empty()) + ImGui::TextDisabled("%s", STR_C("Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m")); + + auto const toolbutton = [&](char const *Label, int const Tool, char const *Tooltip) { + bool const active{state.tool == Tool}; + if (active) + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + if (ImGui::Button(Label)) + { + state.tool = active ? 0 : Tool; + state.tool_placed = false; + state.handle = -1; + } + if (active) + ImGui::PopStyleColor(); + item_tooltip(Tooltip); + }; + toolbutton(STR_C("Break"), 1, STR_C("Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)")); + ImGui::SameLine(); + toolbutton(STR_C("S-curve"), 2, STR_C("Shifts a piece of the straight sideways around an obstacle and back: four clicks on the straight (Shift+click in the view does the same)")); + ImGui::SameLine(); + bool const member{in_straight_set(line)}; + if (ImGui::Button(member ? STR_C("Leave the group") : STR_C("Move with others"))) + { + if (member) + state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return std::find(Member.tracks.begin(), Member.tracks.end(), Track) != Member.tracks.end(); }); }), state.set.end()); + else + state.set.push_back(line); + } + item_tooltip("Straights of a group move together and stay parallel; Alt+click in the view adds another one"); + if (state.tool != 0) + { + ImGui::PushItemWidth(100.0f); + ImGui::InputDouble(STR_C("Curve R (m, 0: automatic)"), &state.curve_radius, 0.0, 0.0, "%.0f"); + item_tooltip("Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line"); + ImGui::Checkbox(STR_C("Transitions from the line speed"), &state.auto_transitions); + item_tooltip("Lengths of the transition curves from the speed of the paths and the limits of the curve design"); + if (false == state.auto_transitions) + { + ImGui::SameLine(); + ImGui::InputDouble(STR_C("Transition (m)"), &state.transition, 0.0, 0.0, "%.1f"); + } + ImGui::PopItemWidth(); + } + if (false == state.status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); + if (false == state.set.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.15f, 1.0f), STR_C("Group: %zu straights move together"), state.set.size()); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Clear"))) + state.set.clear(); + } + + if (false == ImGui::IsAnyItemActive()) + { + state.edit_start = line.start; + state.edit_end = line.end; + state.edit_length = line.length; + state.edit_azimuth = line.azimuth; + } + if (ImGui::CollapsingHeader(STR_C("Exact values"))) + { + ImGui::PushItemWidth(-110.0f); + ImGui::InputScalarN(STR_C("Start (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_start), 3, nullptr, nullptr, "%.3f"); + ImGui::InputScalarN(STR_C("End (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_end), 3, nullptr, nullptr, "%.3f"); + ImGui::PopItemWidth(); + if (ImGui::Button(STR_C("Apply ends"))) + straight_apply(line, state.edit_start, state.edit_end); + ImGui::PushItemWidth(120.0f); + ImGui::InputDouble(STR_C("Length (m)"), &state.edit_length, 0.0, 0.0, "%.3f"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Set from the start##length"))) + { + glm::dvec3 const direction{line.direction.x, line.grade, line.direction.y}; + straight_apply(line, line.start, line.start + direction * std::max(0.1, state.edit_length)); + } + ImGui::InputDouble(STR_C("Azimuth (deg)"), &state.edit_azimuth, 0.0, 0.0, "%.5f"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Turn around the start"))) + { + auto const azimuth{glm::radians(state.edit_azimuth)}; + glm::dvec3 const direction{std::sin(azimuth), line.grade, std::cos(azimuth)}; + straight_apply(line, line.start, line.start + direction * line.length); + } + ImGui::PopItemWidth(); + } +} + +bool editor_mode::straights_active() +{ + return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights && false == current_straight().tracks.empty(); +} + +int editor_mode::straight_handle_hit() +{ + if (false == straights_active()) + return -1; + auto const &line{current_straight()}; + glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; + screen_projection const projection; + float best = kHandleRadius * kHandleRadius; + int hit{-1}; + for (int i = 0; i < 3; ++i) + { + if (auto const distance{projection.mouse_distance2(handles[i])}; distance < best) + { + best = distance; + hit = i; + } + } + return hit; +} + +void editor_mode::straight_refresh() +{ + auto &state{m_straights}; + state.current_for = selected_track(); + state.current = state.current_for != nullptr ? editor_track::find_straight(*state.current_for, state.tolerance) : editor_track::straight{}; + find_neighbour_straights(); + for (auto &line : state.found) + if (false == line.tracks.empty()) + line = editor_track::find_straight(*line.tracks.front(), state.tolerance); + for (auto &line : state.set) + if (false == line.tracks.empty()) + line = editor_track::find_straight(*line.tracks.front(), state.tolerance); +} + +bool editor_mode::in_straight_set(editor_track::straight const &Line) const +{ + for (auto const &member : m_straights.set) + for (auto const *track : Line.tracks) + if (std::find(member.tracks.begin(), member.tracks.end(), track) != member.tracks.end()) + return true; + return false; +} + +void editor_mode::straight_apply(editor_track::straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End) +{ + straights_apply({Line}, {{Start, End}}); +} + +void editor_mode::straights_apply(std::vector const &Lines, std::vector> const &Ends) +{ + auto &state{m_straights}; + state.status.clear(); + auto const tracks{editor_track::straight_affected(Lines)}; + if (false == tracks_editable(tracks, state.status)) + return; + std::vector> states; + for (auto *track : tracks) + states.emplace_back(track, editor_track::capture(*track)); + push_track_snapshot(std::move(states)); + editor_track::move_straights(Lines, Ends); + editor_track::commit(tracks); + straight_refresh(); +} + +void editor_mode::render_straight_gizmo() +{ + auto &state{m_straights}; + if (state.tool_mouse) + { + glm::dvec3 const ground{cursor_ground()}; + state.tool_handle = {ground.x, state.tool_handle.y, ground.z}; + typed_turn(); + return; + } + auto const &line{state.dragging ? state.drag_line : current_straight()}; + if (state.handle < 0 || line.tracks.empty()) + { + state.dragging = false; + return; + } + + glm::dvec3 const anchors[] = {line.start, line.end, (line.start + line.end) * 0.5}; + glm::dvec3 handled{anchors[state.handle]}; + bool const held{handle_drag_step(handled)}; + if (false == held && false == state.dragging) + return; + + if (held) + { + if (false == state.dragging) + { + std::vector lines; + if (in_straight_set(line)) + { + for (auto const &member : state.set) + if (false == member.tracks.empty()) + lines.push_back(editor_track::find_straight(*member.tracks.front(), state.tolerance)); + } + else + { + lines.push_back(line); + } + state.fitting = lines.size() == 1 && start_curve_fit(line, state.handle); + auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; + if (false == tracks_editable(tracks, state.status)) + { + state.fitting = false; + return; + } + state.dragging = true; + state.drag_line = line; + state.drag_lines = lines; + state.drag_states.clear(); + for (auto *track : tracks) + state.drag_states.emplace_back(track, editor_track::capture(*track)); + if (false == state.fitting) + { + push_track_snapshot(state.drag_states); + m_track_drag = tracks; + } + } + auto const &grabbed{state.drag_line}; + glm::dvec3 moved{handled}; + std::function transform; + if (state.handle == 2) + { + auto offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)}; + if (auto const values{typed_values()}; false == values.empty()) + offset = glm::dvec3{-grabbed.direction.y, 0.0, grabbed.direction.x} * values[0]; + transform = [offset](glm::dvec3 const &Point) { return Point + offset; }; + } + else + { + auto const pivot{state.handle == 0 ? grabbed.end : grabbed.start}; + auto const original{state.handle == 0 ? grabbed.start : grabbed.end}; + moved = snap_straight_direction(pivot, moved); + if (auto const values{typed_values()}; false == values.empty() && values[0] > 0.1) + { + glm::dvec2 heading{moved.x - pivot.x, moved.z - pivot.z}; + if (glm::length(heading) < 1e-6) + heading = {original.x - pivot.x, original.z - pivot.z}; + heading = glm::normalize(heading) * values[0]; + moved.x = pivot.x + heading.x; + moved.z = pivot.z + heading.y; + } + glm::dvec2 const before{original.x - pivot.x, original.z - pivot.z}; + glm::dvec2 const after{moved.x - pivot.x, moved.z - pivot.z}; + auto const lengthbefore{glm::length(before)}; + auto const lengthafter{std::max(0.1, glm::length(after))}; + auto const axis{before / std::max(1e-9, lengthbefore)}; + auto const newaxis{after / lengthafter}; + auto const scale{lengthafter / std::max(1e-9, lengthbefore)}; + auto const rise{moved.y - original.y}; + transform = [=](glm::dvec3 const &Point) { + glm::dvec2 const offset{Point.x - pivot.x, Point.z - pivot.z}; + auto const along{glm::dot(offset, axis)}; + auto const across{axis.x * offset.y - axis.y * offset.x}; + auto const planar{plan_of(pivot) + newaxis * (along * scale) + glm::dvec2{-newaxis.y, newaxis.x} * across}; + return glm::dvec3{planar.x, Point.y + rise * along / std::max(1e-9, lengthbefore), planar.y}; + }; + } + std::vector> ends; + for (auto const &member : state.drag_lines) + ends.emplace_back(transform(member.start), transform(member.end)); + state.preview_start = transform(grabbed.start); + state.preview_end = transform(grabbed.end); + if (state.fitting) + { + update_curve_fit(state.preview_start, state.preview_end); + return; + } + for (auto const &entry : state.drag_states) + editor_track::apply(*entry.first, entry.second); + editor_track::move_straights(state.drag_lines, ends); + m_track_dirty = true; + commit_track_drag(false); + } + else if (state.dragging) + { + if (state.fitting) + { + state.fitting = false; + state.dragging = false; + state.drag_lines.clear(); + if (m_route.result.valid) + route_apply(); + straight_refresh(); + return; + } + commit_track_drag(true); + state.dragging = false; + m_track_drag.clear(); + state.drag_states.clear(); + state.drag_lines.clear(); + straight_refresh(); + } +} + +void editor_mode::cancel_track_tools() +{ + auto &state{m_straights}; + if (state.dragging) + { + // the geometry goes back to what it was before the drag + if (false == state.fitting) + { + for (auto const &entry : state.drag_states) + editor_track::apply(*entry.first, entry.second); + m_track_dirty = true; + commit_track_drag(true); + m_track_drag.clear(); + } + state.dragging = false; + state.fitting = false; + state.drag_states.clear(); + state.drag_lines.clear(); + straight_refresh(); + } + m_extend.active = false; + m_turntable.placing = false; + m_signal.placing = false; + m_signal.moving = false; + m_handle_drag = {}; + if (m_point_drag.active) + { + m_track_snap = {}; + point_drag_finish(); + } + if (m_switch.placing) + { + m_switch.placing = false; + m_switch.status = STR_C("Placing of the switch cancelled"); + } + else if (m_switch.armed >= 0) + { + m_switch.armed = -1; + m_switch.status.clear(); + } + m_track_point = {}; + m_route.vertex = -1; + m_route.grip = -1; + state.handle = -1; + state.tool_placed = false; + state.detour.clear(); +} + +void editor_mode::straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius) +{ + auto &state{m_straights}; + state.status.clear(); + struct span + { + TTrack *track; + int path; + double from; + double to; + bool forward; + }; + auto line{Line}; + auto const point = [&](double const Along) { + auto const planar{plan_of(line.start) + line.direction * Along}; + return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; + }; + std::vector spans; + auto const collect = [&]() { + spans.clear(); + for (std::size_t i = 0; i < line.tracks.size(); ++i) + { + auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; + auto const a{line.along(path.points[segment_data::point::start])}; + auto const b{line.along(path.points[segment_data::point::end])}; + spans.push_back({line.tracks[i], line.paths[i], std::min(a, b), std::max(a, b), a < b}); + } + std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); + }; + int first{-1}; + int last{-1}; + auto const select = [&](double const End) { + first = -1; + last = -1; + for (int i = 0; i < static_cast(spans.size()); ++i) + { + if (spans[i].to > From && spans[i].from < End) + { + if (first < 0) + first = i; + last = i; + } + } + }; + auto const keep_regular = [&]() { + if (first < 0) + return; + auto const centre{(std::max(From, 0.0) + std::min(To, line.length)) * 0.5}; + int middle{first}; + for (int i = first; i <= last; ++i) + if (spans[i].from <= centre && spans[i].to >= centre) + middle = i; + if (spans[middle].track->eType != tt_Normal) + { + int best{-1}; + for (int i = first; i <= last; ++i) + if (spans[i].track->eType == tt_Normal && (best < 0 || std::abs(i - middle) < std::abs(best - middle))) + best = i; + if (best < 0) + { + first = -1; + last = -1; + return; + } + middle = best; + } + int low{middle}; + int high{middle}; + while (low > first && spans[low - 1].track->eType == tt_Normal) + --low; + while (high < last && spans[high + 1].track->eType == tt_Normal) + ++high; + first = low; + last = high; + }; + collect(); + select(To); + keep_regular(); + if (first < 0) + return; + + std::vector> splitstates; + std::vector splitcreated; + auto end{To}; + if (last + 1 >= static_cast(spans.size())) + { + auto const final{spans.back()}; + if (final.track->eType != tt_Normal) + return; + auto const length{final.to - final.from}; + auto const cut{To < final.to - 1.0 ? std::max(To, final.from + std::min(1.0, length * 0.5)) : final.from + length * 0.7}; + splitstates.emplace_back(final.track, editor_track::capture(*final.track)); + editor_track::chain split; + split.tracks = {final.track}; + split.forward = {final.forward}; + std::vector pieces(2); + pieces[0].points[segment_data::point::start] = point(final.from); + pieces[0].points[segment_data::point::end] = point(cut); + pieces[1].points[segment_data::point::start] = point(cut); + pieces[1].points[segment_data::point::end] = point(final.to); + splitcreated = editor_track::relay(split, pieces); + line = editor_track::find_straight(*line.tracks.front(), state.tolerance); + collect(); + end = std::min(To, cut); + select(end); + keep_regular(); + if (first < 0 || last + 1 >= static_cast(spans.size())) + { + for (auto *track : splitcreated) + editor_track::retire(*track); + editor_track::apply(*splitstates.front().first, splitstates.front().second); + editor_track::commit({splitstates.front().first}); + return; + } + } + auto const undo_split = [&]() { + for (auto *track : splitcreated) + editor_track::retire(*track); + for (auto const &entry : splitstates) + { + editor_track::apply(*entry.first, entry.second); + editor_track::commit({entry.first}); + } + }; + + editor_track::straight tail; + tail.direction = line.direction; + for (int i = last + 1; i < static_cast(spans.size()); ++i) + { + tail.tracks.push_back(spans[i].track); + tail.paths.push_back(spans[i].path); + } + tail.start = point(spans[last + 1].from); + tail.end = line.end; + tail.length = line.length - spans[last + 1].from; + tail.grade = line.grade; + + std::vector chainpaths; + for (int i = first; i <= last; ++i) + chainpaths.push_back(spans[i].track); + auto tracks{editor_track::straight_affected({tail})}; + for (auto *track : chainpaths) + if (std::find(tracks.begin(), tracks.end(), track) == tracks.end()) + tracks.push_back(track); + if (false == tracks_editable(tracks, m_straights.status)) + { + undo_split(); + return; + } + std::vector> states; + for (auto *track : tracks) + states.emplace_back(track, editor_track::capture(*track)); + + editor_track::move_straights({tail}, {{Tail(tail.start), Tail(tail.end)}}); + + m_route.from = spans[first].track; + m_route.to = spans[last].track; + route_reset(); + if (m_route.chain.tracks.empty() || false == m_route.chain.switches.empty()) + { + if (false == m_route.chain.switches.empty()) + m_route.error = STR_C("The curve would run through a switch"); + for (auto const &entry : states) + editor_track::apply(*entry.first, entry.second); + editor_track::commit(tracks); + undo_split(); + state.status = m_route.error; + return; + } + m_route.design.start_reserve = 0.0; + m_route.design.end_reserve = 0.0; + for (auto &vertex : m_route.design.vertices) + { + vertex.radius = Radius; + route_recommend(vertex); + if (false == state.auto_transitions) + { + vertex.transition_in = state.transition; + vertex.transition_out = state.transition; + } + } + route_update(); + if (false == m_route.result.valid) + { + for (auto const &entry : states) + editor_track::apply(*entry.first, entry.second); + editor_track::commit(tracks); + undo_split(); + state.status = m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front(); + return; + } + auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + editor_track::keep_heights(m_route.chain, pieces); + auto created{editor_track::relay(m_route.chain, pieces, &m_route.to)}; + editor_track::commit(tracks); + for (auto const &entry : splitstates) + { + auto const found{std::find_if(states.begin(), states.end(), [&](auto const &State) { return State.first == entry.first; })}; + if (found != states.end()) + found->second = entry.second; + else + states.push_back(entry); + } + created.insert(created.begin(), splitcreated.begin(), splitcreated.end()); + { + auto changed{created}; + for (auto const &entry : states) + changed.push_back(entry.first); + digitize_drawn(changed); + } + push_track_snapshot(std::move(states), std::move(created)); + std::string error; + editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true); + straight_refresh(); + state.status = STR_C("Done, select a curve to adjust it in the Curve mode"); +} + +double editor_mode::straight_tool_radius(editor_track::straight const &Line) const +{ + if (m_straights.curve_radius > 0.0) + return m_straights.curve_radius; + double speed{-1.0}; + for (auto const *track : Line.tracks) + speed = std::max(speed, editor_track::velocity(*track)); + if (speed <= 0.0) + speed = 100.0; + auto const recommended{alignment::recommend(speed, 1000.0, m_route.design.norms)}; + return std::max(150.0, std::ceil(recommended.radius_min * 1.5 / 50.0) * 50.0); +} + +bool editor_mode::place_straight_tool() +{ + auto &state{m_straights}; + if (ui()->mode() != nodebank_panel::TRACK || m_track_tab != track_tab::straights || state.tool == 0) + return false; + if (state.tool == 2) + { + add_detour_point(); + return true; + } + auto const &line{current_straight()}; + if (line.tracks.empty()) + return false; + glm::dvec3 const ground{cursor_ground()}; + auto const along{std::clamp(line.along(ground), 0.0, line.length)}; + glm::dvec3 const axis{line.direction.x, line.grade, line.direction.y}; + state.tool_line = line; + state.tool_at = along; + state.tool_point = line.start + axis * along; + auto const reach{std::min(50.0, std::max(1.0, line.length - along))}; + state.tool_handle = state.tool_point + axis * reach; + state.tool_placed = true; + state.handle = -1; + return true; +} + +void editor_mode::apply_straight_tool() +{ + auto &state{m_straights}; + auto const line{state.tool_line}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; + auto const radius{straight_tool_radius(line)}; + if (state.tool == 1) + { + if (glm::length(offset) < 1e-3) + return; + auto const unit{glm::normalize(offset)}; + auto const angle{std::atan2(line.direction.x * unit.y - line.direction.y * unit.x, glm::dot(line.direction, unit))}; + if (std::abs(angle) < 1e-5) + return; + glm::dvec2 const pivot{state.tool_point.x, state.tool_point.z}; + double speed{-1.0}; + for (auto const *track : line.tracks) + speed = std::max(speed, editor_track::velocity(*track)); + auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; + auto const reach{radius * std::tan(std::min(std::abs(angle), 3.0) * 0.5) + transition * 0.5 + 10.0}; + straight_reshape(line, state.tool_at - reach, state.tool_at + reach, + [pivot, angle](glm::dvec3 const &Point) { + glm::dvec2 const relative{Point.x - pivot.x, Point.z - pivot.y}; + auto const c{std::cos(angle)}; + auto const s{std::sin(angle)}; + return glm::dvec3{pivot.x + relative.x * c - relative.y * s, Point.y, pivot.y + relative.x * s + relative.y * c}; + }, + radius); + } + else + { + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const shift{glm::dot(offset, normal)}; + auto const length{glm::dot(offset, line.direction)}; + if (std::abs(shift) < 1e-3 || length < 1.0) + return; + auto const translation{normal * shift}; + straight_reshape(line, state.tool_at, state.tool_at + length, + [translation](glm::dvec3 const &Point) { return glm::dvec3{Point.x + translation.x, Point.y, Point.z + translation.y}; }, + radius); + } + state.tool_placed = false; +} + +void editor_mode::start_straight_gesture(int const Tool) +{ + auto &state{m_straights}; + state.tool_before = state.tool; + state.tool = Tool; + if (false == place_straight_tool()) + { + state.tool = state.tool_before; + return; + } + state.tool_mouse = true; +} + +void editor_mode::finish_straight_gesture() +{ + auto &state{m_straights}; + state.tool_mouse = false; + apply_straight_tool(); + state.tool = state.tool_before; + state.tool_placed = false; +} + +void editor_mode::toggle_straight_set(TTrack &Track) +{ + auto &state{m_straights}; + auto const line{editor_track::find_straight(Track, state.tolerance)}; + if (line.tracks.empty()) + return; + if (in_straight_set(line)) + state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Other) { return std::find(Member.tracks.begin(), Member.tracks.end(), Other) != Member.tracks.end(); }); }), state.set.end()); + else + state.set.push_back(line); + m_node = &Track; + ui()->set_node(m_node); + m_track_tab = track_tab::straights; +} + +std::vector editor_mode::detour_outline() const +{ + auto const &state{m_straights}; + std::vector result; + if (state.detour.empty()) + return result; + auto const &line{state.detour_line}; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto points{state.detour}; + if (points.size() < 4) + points.push_back(state.detour_mouse); + auto const offset{points.size() >= 2 ? glm::dot(plan_of(points[1] - line.start), normal) : 0.0}; + auto const place = [&](double const Along, double const Lateral) { + auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; + return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; + }; + for (std::size_t i = 0; i < points.size(); ++i) + result.push_back(place(line.along(points[i]), (i == 1 || i == 2) ? offset : 0.0)); + return result; +} + +void editor_mode::add_detour_point() +{ + auto &state{m_straights}; + if (state.detour.empty()) + { + auto const &line{current_straight()}; + if (line.tracks.empty()) + return; + state.detour_line = line; + } + state.detour.push_back(cursor_ground()); + if (state.detour.size() >= 4) + { + apply_detour(); + state.detour.clear(); + } +} + +void editor_mode::apply_detour() +{ + auto &state{m_straights}; + auto line{editor_track::find_straight(*state.detour_line.tracks.front(), state.tolerance)}; + if (line.tracks.empty()) + return; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const offset{glm::dot(plan_of(state.detour[1] - line.start), normal)}; + std::array stations{line.along(state.detour[0]), line.along(state.detour[1]), line.along(state.detour[2]), line.along(state.detour[3])}; + if (stations[0] > stations[3]) + { + std::swap(stations[0], stations[3]); + std::swap(stations[1], stations[2]); + } + for (auto &station : stations) + station = std::clamp(station, 0.0, line.length); + stations[1] = std::clamp(stations[1], stations[0] + 1.0, stations[3] - 1.0); + stations[2] = std::clamp(stations[2], stations[1], stations[3] - 1.0); + if (std::abs(offset) < 0.01 || stations[3] - stations[0] < 3.0) + return; + + auto const radius{straight_tool_radius(line)}; + auto const slope{std::atan(std::abs(offset) / std::max(1.0, std::min(stations[1] - stations[0], stations[3] - stations[2])))}; + double speed{-1.0}; + for (auto const *track : line.tracks) + speed = std::max(speed, editor_track::velocity(*track)); + auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; + auto const reach{radius * std::tan(slope * 0.5) + transition * 0.5 + 5.0}; + auto const from{std::max(0.0, stations[0] - reach)}; + auto const to{std::min(line.length, stations[3] + reach)}; + + struct span + { + TTrack *track; + double from; + double to; + }; + std::vector spans; + for (std::size_t i = 0; i < line.tracks.size(); ++i) + { + auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; + auto const a{line.along(path.points[segment_data::point::start])}; + auto const b{line.along(path.points[segment_data::point::end])}; + spans.push_back({line.tracks[i], std::min(a, b), std::max(a, b)}); + } + std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); + int first{-1}; + int last{-1}; + for (int i = 0; i < static_cast(spans.size()); ++i) + { + if (spans[i].to > from && spans[i].from < to && spans[i].track->eType == tt_Normal) + { + if (first < 0) + first = i; + last = i; + } + else if (first >= 0 && spans[i].from < to) + { + if (spans[i].from > stations[3]) + break; + first = -1; + last = -1; + } + } + if (first < 0) + return; + + m_route.from = spans[first].track; + m_route.to = spans[last].track; + route_reset(); + if (m_route.chain.tracks.empty()) + return; + auto &design{m_route.design}; + auto const startalong{line.along(design.start)}; + auto const endalong{line.along(design.end)}; + auto const place = [&](double const Along, double const Lateral) { + auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; + return planar; + }; + alignment::vertex vertex; + vertex.radius = radius; + route_recommend(vertex); + if (false == state.auto_transitions) + { + vertex.transition_out = state.transition; + vertex.transition_in = vertex.transition_out; + } + design.vertices.clear(); + auto first_vertex{vertex}; + first_vertex.offset = std::max(0.1, stations[0] - startalong); + design.vertices.push_back(first_vertex); + if (stations[2] - stations[1] < 1.0) + { + auto middle{vertex}; + middle.position = place((stations[1] + stations[2]) * 0.5, offset); + design.vertices.push_back(middle); + } + else + { + auto second{vertex}; + second.position = place(stations[1], offset); + auto third{vertex}; + third.position = place(stations[2], offset); + design.vertices.push_back(second); + design.vertices.push_back(third); + } + auto last_vertex{vertex}; + last_vertex.offset = std::max(0.1, endalong - stations[3]); + design.vertices.push_back(last_vertex); + route_update(); + if (false == m_route.result.valid) + return; + route_apply(); + straight_refresh(); +} + +bool editor_mode::start_curve_fit(editor_track::straight const &Line, int const Handle) +{ + auto &state{m_straights}; + auto *before{editor_track::outside_neighbour(Line, false)}; + auto *after{editor_track::outside_neighbour(Line, true)}; + auto const curve_at = [&](TTrack *Neighbour, editor_track::curve &Curve) { + Curve = {}; + return Neighbour != nullptr && Neighbour->eType == tt_Normal && editor_track::find_curve(*Neighbour, state.tolerance, m_route.design.norms.gauge, Curve) && Curve.reversals == 0 && std::abs(Curve.turn) > 1e-4; + }; + state.fit_has_before = curve_at(before, state.fit_before); + state.fit_has_after = curve_at(after, state.fit_after); + if (false == state.fit_has_before && false == state.fit_has_after) + return false; + auto const side_follows = [&](bool const Curve, TTrack const *Neighbour, bool const Pivot) { return Curve || Neighbour == nullptr || Pivot; }; + if (false == side_follows(state.fit_has_before, before, Handle == 1) || false == side_follows(state.fit_has_after, after, Handle == 0)) + return false; + auto const own_end = [&](glm::dvec3 const &Point) -> TTrack * { + for (auto *track : Line.tracks) + if (editor_track::touches(*track, Point)) + return track; + return Line.tracks.empty() ? nullptr : Line.tracks.front(); + }; + auto const other_end = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; + m_route.from = state.fit_has_before ? other_end(state.fit_before, Line.start) : own_end(Line.start); + m_route.to = state.fit_has_after ? other_end(state.fit_after, Line.end) : own_end(Line.end); + route_reset(); + if (m_route.chain.tracks.empty()) + return false; + return true; +} + +void editor_mode::update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &End) +{ + auto &state{m_straights}; + auto &design{m_route.design}; + glm::dvec2 const point{Start.x, Start.z}; + glm::dvec2 const direction{glm::normalize(plan_of(End - Start))}; + glm::dvec2 const start{design.start.x, design.start.z}; + glm::dvec2 const end{design.end.x, design.end.z}; + auto const startdirection{glm::normalize(design.start_direction)}; + auto const enddirection{glm::normalize(design.end_direction)}; + auto const first{cross(point - start, direction) / cross(startdirection, direction)}; + auto const last{cross(end - point, direction) / cross(enddirection, direction)}; + auto const make = [&](editor_track::curve const &Curve, double const Offset) { + alignment::vertex vertex; + vertex.radius = Curve.radius > 0.0 ? std::round(Curve.radius) : 1000.0; + route_recommend(vertex); + if (Curve.transition_in > 0.0) + vertex.transition_in = std::round(Curve.transition_in); + if (Curve.transition_out > 0.0) + vertex.transition_out = std::round(Curve.transition_out); + if (Curve.cant > 0.0) + vertex.cant = std::round(Curve.cant); + compound_from(Curve, vertex); + vertex.offset = std::max(0.1, Offset); + return vertex; + }; + design.vertices.clear(); + if (state.fit_has_before) + design.vertices.push_back(make(state.fit_before, first)); + else + { + design.start = Start; + design.start_direction = direction; + design.start_reserve = 0.0; + } + if (state.fit_has_after) + design.vertices.push_back(make(state.fit_after, last)); + else + { + design.end = End; + design.end_direction = direction; + design.end_reserve = 0.0; + } + route_update(); +} diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp new file mode 100644 index 00000000..a0b72800 --- /dev/null +++ b/application/editormodeswitchplace.cpp @@ -0,0 +1,1317 @@ +/* +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: +*/ + + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/TrackFeatures.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application/editormodetrackshared.h" + +using namespace track_mode; + +std::vector editor_mode::switch_preview() const +{ + auto const &tool{m_switch}; + if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return {}; + if (tool.curved) + { + auto const sample{sample_at(tool.frame, tool.along)}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{sample.tangent * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + return curved_switch_paths(tool.templates[tool.armed], tool.frame, tool.along, forward, side); + } + if (switch_link plan; plan_switch_link(plan)) + { + plan.paths.insert(plan.paths.end(), plan.link.pieces.begin(), plan.link.pieces.end()); + return plan.paths; + } + auto const &line{tool.line}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{line.direction * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + return editor_track::place_switch(tool.templates[tool.armed], tool.point, direction, side, line.grade * forward); +} + +bool editor_mode::start_switch_placement() +{ + auto &tool{m_switch}; + if (tool.armed < 0 || m_track_tab != track_tab::turnout) + return false; + // with no plain track selected, or a press away from it, the click selects + auto *selected{selected_track()}; + if (selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) + return false; + glm::dvec3 const ground{cursor_level(selected->m_paths.front().points[segment_data::point::start].y)}; + tool.curved = false; + tool.status.clear(); + if (current_straight().tracks.empty()) + { + auto *track{selected}; + if (tool.templates[tool.armed].double_slip) + { + tool.status = STR_C("A double slip goes only into a straight crossing another one"); + return false; + } + editor_track::curve curve; + editor_track::chain chain; + std::string error; + if (false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + tool.status = STR_C("The selected track is neither a recognized straight nor a curve"); + return false; + } + if (false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + { + tool.status = error.empty() ? std::string{STR_C("The curve can't be followed")} : error; + return false; + } + for (auto const &part : editor_track::chain_parts(chain)) + if (std::find(part.tracks.begin(), part.tracks.end(), track) != part.tracks.end()) + { + chain = part; + break; + } + tool.frame = sample_chain(chain); + tool.frame_tracks = chain.tracks; + if (tool.frame.size() < 2) + return false; + tool.curved = true; + tool.along = nearest_station(tool.frame, ground); + tool.point = sample_at(tool.frame, tool.along).position; + if (plan_distance(tool.point, ground) > kSwitchReach) + return false; + tool.mouse = ground; + tool.placing = true; + return true; + } + auto const &line{current_straight()}; + auto const along{std::clamp(line.along(ground), 0.0, line.length)}; + auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along}; + if (plan_distance(point, ground) > kSwitchReach) + return false; + if (tool.templates[tool.armed].double_slip) + { + insert_double_slip(line, ground, tool.templates[tool.armed]); + return true; + } + tool.along = along; + tool.point = point; + tool.mouse = ground; + tool.line = line; + tool.placing = true; + return true; +} + +void editor_mode::finish_switch_placement() +{ + auto &tool{m_switch}; + if (tool.connection >= 0) + { + if (false == tool.curved) + { + insert_connection(); + return; + } + tool.placing = false; + tool.status = STR_C("A connection is laid between straights"); + ui()->set_status(tool.status, true); + return; + } + switch_link plan; + auto const linked{plan_switch_link(plan)}; + tool.placing = false; + if (tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return; + if (linked) + { + insert_linked_switch(plan); + return; + } + if (tool.curved) + { + auto const sample{sample_at(tool.frame, tool.along)}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{sample.tangent * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + insert_curved_switch(forward, side); + return; + } + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, tool.line.direction) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{tool.line.direction * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + insert_switch(tool.line, tool.along, forward, side); + if (false == plan.reason.empty()) + ui()->set_status(plan.reason, true); +} + +void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side) +{ + std::vector> states; + std::vector created; + std::vector removed; + if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed, switch_name_for_new())) + return; + auto const placed{created}; + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + switch_placed(placed); +} + + +void editor_mode::switch_placed(std::vector const &Created) +{ + auto const found{std::find_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; + if (found == Created.end()) + return; + auto const slip{std::count_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; }) > 1}; + m_switch.placing = false; + first_step_done(first_step::turnout); + m_track_point = {}; + m_node = *found; + ui()->set_node(m_node); + if (slip) + { + auto const known{switch_store::named(**found)}; + if (false == known.has_value() || (known->kind != "double" && known->kind != "slip")) + return; + std::vector driven; + for (auto *track : Created) + if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track)) + { + join_last_steps(); + driven.push_back(track->name()); + } + // a slip has a drive at each of its switches + if (known->kind == "slip") + { + if (false == driven.empty()) + { + std::string names; + for (auto const &name : driven) + names += (names.empty() ? "" : ", ") + name; + m_switch.status = format(STR_C("Slip with drives at its switches %s: the events of the name of a switch with + and - switch it, also in the map"), names.c_str()); + ui()->set_status(m_switch.status, false); + } + return; + } + if (driven.size() == 2) + { + m_switch.status = format(STR_C("Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map"), driven[0].c_str(), driven[1].c_str(), driven[0].c_str(), driven[0].c_str(), driven[1].c_str(), driven[1].c_str()); + ui()->set_status(m_switch.status, false); + } + advance_switch_name(); + return; + } + if (place_switch_drive(**found)) + join_last_steps(); + advance_switch_name(); +} + +void editor_mode::advance_switch_name() +{ + std::string name{m_switch.name}; + auto digits{name.size()}; + while (digits > 0 && std::isdigit(static_cast(name[digits - 1]))) + --digits; + if (digits < name.size() && name.size() - digits < 9) + name = name.substr(0, digits) + std::to_string(std::stoi(name.substr(digits)) + 1); + else + name.clear(); + std::snprintf(m_switch.name, sizeof(m_switch.name), "%s", name.c_str()); +} + +std::string editor_mode::switch_name_for_new() const +{ + if (m_switch.name[0] != '\0') + return m_switch.name; + for (int i = 1;; ++i) + { + auto const name{"z" + std::to_string(i)}; + if (simulation::Paths.find(name) == nullptr && simulation::Events.FindEvent(name + "+") == nullptr) + return name; + } +} + +void editor_mode::join_last_steps() +{ + if (m_history.size() < 2) + return; + auto last{std::move(m_history.back())}; + m_history.pop_back(); + auto &previous{m_history.back()}; + previous.instances.insert(previous.instances.end(), last.instances.begin(), last.instances.end()); +} + +bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style) +{ + auto const from{From}; + auto const to{To}; + std::string reason; + if (false == tracks_editable(Line.tracks, reason)) + { + m_switch.status = reason; + ui()->set_status(reason, true); + return false; + } + struct span + { + TTrack *track; + int path; + double from; + double to; + bool forward; + }; + std::vector spans; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; + auto const a{Line.along(path.points[segment_data::point::start])}; + auto const b{Line.along(path.points[segment_data::point::end])}; + spans.push_back({Line.tracks[i], Line.paths[i], std::min(a, b), std::max(a, b), a < b}); + } + std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); + int first{-1}; + int last{-1}; + for (int i = 0; i < static_cast(spans.size()); ++i) + { + if (spans[i].to > from + 1e-3 && spans[i].from < to - 1e-3) + { + if (first < 0) + first = i; + last = i; + } + } + if (first < 0) + return false; + editor_track::chain chain; + for (int i = first; i <= last; ++i) + { + if (spans[i].track->eType != tt_Normal) + return false; + chain.tracks.push_back(spans[i].track); + chain.forward.push_back(spans[i].forward); + } + if (Style != nullptr) + *Style = spans[first].track; + auto const before{std::max(0.0, from - spans[first].from)}; + auto const after{std::max(0.0, spans[last].to - to)}; + bool const hasbefore{before > 0.01}; + bool const hasafter{after > 0.01}; + auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; + if (false == hasbefore && false == hasafter) + { + for (auto *track : chain.tracks) + editor_track::retire(*track); + Removed.insert(Removed.end(), chain.tracks.begin(), chain.tracks.end()); + return true; + } + auto const count{std::max(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))}; + std::size_t countbefore{0}; + if (hasbefore && hasafter) + countbefore = std::clamp(static_cast(std::round(count * before / (before + after))), 1, count - 1); + else if (hasbefore) + countbefore = count; + std::vector pieces; + auto const straight = [&](double const Start, double const End, std::size_t const Count) { + for (std::size_t i = 0; i < Count; ++i) + { + segment_data path; + path.points[segment_data::point::start] = point(Start + (End - Start) * i / Count); + path.points[segment_data::point::end] = point(Start + (End - Start) * (i + 1) / Count); + pieces.push_back(path); + } + }; + if (hasbefore) + straight(spans[first].from, from, countbefore); + if (hasafter) + straight(to, spans[last].to, count - countbefore); + for (auto *track : chain.tracks) + States.emplace_back(track, editor_track::capture(*track)); + auto const relaid{editor_track::relay(chain, pieces)}; + Created.insert(Created.end(), relaid.begin(), relaid.end()); + return true; +} + +bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector> &States, std::vector &Created, std::vector &Removed, std::string const &Name) +{ + auto const &shape{Shape}; + if (shape.length > Line.length - 0.02) + { + m_switch.status = format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length); + return false; + } + auto const from{switch_from(Line, shape.length, Along, Direction, Snap)}; + auto const to{from + shape.length}; + auto const origin{Direction > 0 ? from : to}; + TTrack *first{nullptr}; + if (false == cut_straight(Line, from, to, States, Created, Removed, &first)) + { + m_switch.status = STR_C("The switch would overlap another switch, move it onto plain track"); + return false; + } + auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; + glm::dvec2 const direction{Line.direction * static_cast(Direction)}; + auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; + auto const made{create_switch_set(shape, paths, Style != nullptr ? *Style : *first, Name)}; + Created.insert(Created.end(), made.begin(), made.end()); + return true; +} + +std::vector editor_mode::create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name) +{ + if (Shape.twofold() && Paths.size() >= 5) + { + auto firstshape{Shape}; + firstshape.length = glm::distance(Paths[0].points[segment_data::point::start], Paths[0].points[segment_data::point::end]); + auto secondshape{Shape}; + secondshape.length = Shape.length - Shape.stagger; + auto *one{editor_track::create_switch(firstshape, Paths, Style, Name)}; + auto *two{one != nullptr ? editor_track::create_switch(secondshape, {Paths[2], Paths[3]}, Style, Name + "b") : nullptr}; + auto *link{two != nullptr ? editor_track::create_path(Style, Paths[4]) : nullptr}; + if (link == nullptr) + { + m_switch.status = STR_C("The switch couldn't be created from this template"); + return {}; + } + editor_track::commit({one, two, link}); + record_double(*one, *two, Shape); + m_switch.status = format(STR_C("Double switch %s with its second switch %s inserted"), one->name().c_str(), two->name().c_str()); + return {one, two, link}; + } + auto *track{editor_track::create_switch(Shape, Paths, Style, Name)}; + if (track == nullptr) + { + m_switch.status = STR_C("The switch couldn't be created from this template"); + return {}; + } + editor_track::commit({track}); + record_switch(*track, Shape); + m_switch.status = STR_C("Switch ") + track->name() + STR_C(" inserted"); + return {track}; +} + + +bool editor_mode::plan_crossover(crossover_plan &Plan) const +{ + auto const &tool{m_extend}; + if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || m_track_layout == track_layout::straight) + return false; + auto const &own{*tool.track}; + auto const &main{own.m_paths[0]}; + auto const &diverging{own.m_paths[1]}; + auto const origin{main.points[segment_data::point::start]}; + auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))}; + glm::dvec2 const perpendicular{-axis.y, axis.x}; + auto const &frog{diverging.points[segment_data::point::end]}; + auto const lateral{glm::dot(plan_of(frog - origin), perpendicular)}; + if (std::abs(lateral) < 0.01) + return false; + auto const side{lateral > 0.0 ? 1.0 : -1.0}; + auto const &control{(diverging.pieces.empty() ? diverging : diverging.pieces.back()).points[segment_data::point::control2]}; + auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan_of(control) : plan_of(frog - origin))}; + auto const angle{std::atan2(std::abs(cross(axis, tangent)), glm::dot(axis, tangent))}; + if (angle < 1e-4) + return false; + + auto const ground{tool.mouse}; + TTrack *target{nullptr}; + double best{8.0}; + auto const sections{simulation::Region->sections(ground, 40.f)}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *track : cell.m_paths) + { + if (track == &own || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const b{plan_of(path.points[segment_data::point::end])}; + auto const segment{b - a}; + auto const t{std::clamp(glm::dot(plan_of(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(ground), a + segment * t)}; + if (distance < best) + { + best = distance; + target = track; + } + } + } + } + if (target == nullptr) + return false; + Plan.target = editor_track::find_straight(*target, m_straights.tolerance); + auto const &line{Plan.target}; + if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0))) + return false; + editor_track::switch_template shape; + shape.source = tool.track; + shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis); + for (int i = 0; i < 2; ++i) + { + auto const &path{own.m_paths[i]}; + auto &local{shape.local[i]}; + local = path; + auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), perpendicular) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; }; + local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal); + if (side < 0.0) + { + local.rolls = {-local.rolls[0], -local.rolls[1]}; + for (auto &piece : local.pieces) + piece.rolls = {-piece.rolls[0], -piece.rolls[1]}; + } + } + Plan.shape = shape; + auto const faced{connection::face(frog, tangent, connection::of(line), 1, shape)}; + if (false == faced.valid) + return false; + Plan.along = faced.partner.along; + Plan.direction = faced.partner.forward; + Plan.side = faced.partner.side; + Plan.paths = faced.partner.paths; + Plan.insert = faced.insert; + return true; +} + +std::string editor_mode::link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const +{ + auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}}; + if (false == Link.valid) + return Link.shortfall > 0.0 ? format(STR_C("The branch can't lead into %s: the R %.0f m curve needs %.2f m more room"), name.c_str(), Link.radius, Link.shortfall) : + format(STR_C("The branch can't lead into %s"), name.c_str()); + if (Link.trim > 0.0 && (false == stretchable(Target) || geometry::chord_length(Target.track->m_paths[Target.point.path]) < Link.trim + 1.0)) + return format(STR_C("%s would have to be shortened by %.2f m, it's too short or not straight"), name.c_str(), Link.trim); + return {}; +} + +bool editor_mode::stretchable(editor_track::snap_target const &Target) const +{ + return Target.track != nullptr && Target.track->eType == tt_Normal && Target.track->m_paths.size() == 1 && editor_track::is_straight(*Target.track, m_straights.tolerance); +} + +bool editor_mode::plan_switch_link(switch_link &Plan) const +{ + auto const &tool{m_switch}; + if (false == tool.placing || tool.curved || tool.connection >= 0 || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size()) || tool.line.tracks.empty() || tool.templates[tool.armed].double_slip) + return false; + auto const &shape{tool.templates[tool.armed]}; + auto const &line{tool.line}; + std::vector const exclude(line.tracks.begin(), line.tracks.end()); + auto const category{line.tracks.front()->iCategoryFlag & 15}; + auto const orient = [&](glm::dvec3 const &Toward) { + glm::dvec2 const offset{plan_of(Toward - tool.point)}; + Plan.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1; + Plan.side = cross(line.direction * static_cast(Plan.forward), offset) >= 0.0 ? 1 : -1; + }; + auto const place = [&](double const Along) { + auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Along}; + return editor_track::place_switch(shape, point, line.direction * static_cast(Plan.forward), Plan.side, line.grade * Plan.forward); + }; + auto const &io{ImGui::GetIO()}; + if (false == m_cursor_override.has_value()) + Plan.target = snap_free_end(tool.mouse, {io.MousePos.x, io.MousePos.y}, nullptr, category, exclude); + if (Plan.target.track == nullptr) + Plan.target = editor_track::find_free_end(nullptr, category, tool.mouse, kLaySnapRadius, exclude); + if (Plan.target.track != nullptr) + orient(Plan.target.position); + else + { + orient(tool.mouse); + auto const paths{place(tool.along)}; + if (paths.size() < 2) + return false; + Plan.target = editor_track::find_free_end(nullptr, category, paths[1].points[segment_data::point::end], kLaySnapRadius, exclude); + if (Plan.target.track == nullptr) + return false; + } + if (shape.length > line.length - 0.02) + return false; + auto const kink{glm::radians(m_joints.angle * 0.5)}; + auto const plan = [&](double const Along) { + Plan.along = Along; + Plan.paths = place(Along); + if (Plan.paths.size() < 2) + return false; + auto const radius{editor_track::branch_radius(Plan.paths[1])}; + Plan.link = editor_track::link_branch(branch_start(Plan.paths[1]), radius, branch_target(Plan.target), kink, + editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(nullptr, Plan.target.track))), stretchable(Plan.target)); + Plan.reason = link_problem(Plan.target, Plan.link); + return Plan.reason.empty(); + }; + auto const from{switch_from(line, shape.length, tool.along, Plan.forward, true)}; + auto const snapped{Plan.forward > 0 ? from : from + shape.length}; + auto const paths{place(snapped)}; + if (paths.size() < 2) + return false; + if (auto const shift{editor_track::branch_shift(branch_start(paths[1]), line.direction * static_cast(Plan.forward), editor_track::branch_radius(paths[1]), branch_target(Plan.target), kink)}) + { + auto const along{snapped + *shift * Plan.forward}; + auto const begin{Plan.forward > 0 ? along : along - shape.length}; + Plan.moved = std::abs(*shift); + if (begin >= -1e-6 && begin + shape.length <= line.length + 1e-6 && plan(along) && false == Plan.link.reverse) + return true; + } + Plan.moved = 0.0; + return plan(snapped); +} + +void editor_mode::insert_linked_switch(switch_link const &Plan) +{ + std::vector> states; + std::vector created; + std::vector removed; + if (false == place_switch_on_straight(m_switch.line, m_switch.templates[m_switch.armed], nullptr, Plan.along, Plan.forward, Plan.side, false, states, created, removed, switch_name_for_new())) + return; + auto const placed{created}; + link_tracks(Plan.target, Plan.link, Plan.target.track->eType == tt_Normal ? *Plan.target.track : *m_switch.line.tracks.front(), states, created); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + m_switch.status.clear(); + switch_placed(placed); + auto summary{link_summary(Plan.target, Plan.link, editor_track::branch_radius(Plan.paths[1]))}; + if (Plan.moved >= 0.005) + summary += format(STR_C(", the switch moved %.2f m"), Plan.moved); + m_switch.status = summary + (m_switch.status.empty() ? std::string{} : "\n" + m_switch.status); + ui()->set_status(m_switch.status, false); +} + +bool editor_mode::plan_branch_link(editor_track::branch_link &Link, std::string &Reason) const +{ + auto const &tool{m_extend}; + editor_track::snap_target target; + Reason.clear(); + if (false == extend_snap(target) || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() != 2 || tool.path != 1 || false == tool.atend) + return false; + if (std::find(tool.line.begin(), tool.line.end(), target.track) != tool.line.end()) + return false; + auto const &diverging{tool.track->m_paths[1]}; + auto const radius{editor_track::branch_radius(diverging)}; + Link = editor_track::link_branch(branch_start(diverging), radius, branch_target(target), glm::radians(m_joints.angle * 0.5), + editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(tool.track, target.track))), stretchable(target)); + Reason = link_problem(target, Link); + return Reason.empty(); +} + +void editor_mode::finish_branch_link(editor_track::branch_link const &Link) +{ + auto *own{m_extend.track}; + auto const target{m_extend.snap}; + m_extend.active = false; + std::vector> states; + std::vector created; + auto const &style{target.track->eType == tt_Normal || m_extend.line.empty() ? *target.track : *m_extend.line.front()}; + auto const linked{link_tracks(target, Link, style, states, created)}; + push_track_snapshot(std::move(states), std::move(created)); + m_node = linked.empty() ? own : linked.back(); + ui()->set_node(m_node); + straight_refresh(); + ui()->set_status(link_summary(target, Link, editor_track::branch_radius(own->m_paths[1])), false); +} + +std::vector editor_mode::link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector> &States, std::vector &Created) +{ + auto *target{Target.track}; + if (Link.trim != 0.0) + { + States.emplace_back(target, editor_track::capture(*target)); + editor_track::move_point(*target, Target.point, Link.landing); + auto &path{target->m_paths[Target.point.path]}; + path.points[segment_data::point::control1] = glm::dvec3{0.0}; + path.points[segment_data::point::control2] = glm::dvec3{0.0}; + editor_track::commit({target}); + } + std::vector pieces; + for (auto const &piece : Link.pieces) + pieces.push_back(editor_track::create_path(Style, piece)); + editor_track::commit(pieces); + digitize_drawn(pieces); + Created.insert(Created.end(), pieces.begin(), pieces.end()); + return pieces; +} + +std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type) +{ + auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; }; + auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) { + segment_data path; + path.points[segment_data::point::start] = Start; + path.points[segment_data::point::end] = End; + return path; + }; + // the ends of the two diagonals at the crossing are kept apart further than the simulation joins the ends, 2 cm + auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))}; + auto const gap{std::max(0.25, 0.06 / halfsine)}; + auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0}; + auto const whole{Curve.length()}; + // the part of the slip curve from Start along Heading, turning by Turn to Finish, between the distances From and To along it; + // where it changes the radius made of pieces, with one curve from end to end for the versions which know no pieces + auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) { + auto const place = [&](double const Along) { + auto const local{Curve.point(Along)}; + auto const planar{plan_of(Start) + Heading * local.x + glm::dvec2{-Heading.y, Heading.x} * (Turn * local.y)}; + return glm::dvec3{planar.x, Start.y + (Finish.y - Start.y) * Along / whole, planar.y}; + }; + auto const bend = [&](double const Begin, double const End, double const Radius, double const Handle0, double const Handle1) { + auto const d0{turned(Heading, Turn * Curve.heading(Begin))}; + auto const d3{turned(Heading, Turn * Curve.heading(End))}; + segment_data path; + path.points[segment_data::point::start] = place(Begin); + path.points[segment_data::point::end] = place(End); + if (Radius > 0.0) + { + path.points[segment_data::point::control1] = {d0.x * Handle0, 0.0, d0.y * Handle0}; + path.points[segment_data::point::control2] = {-d3.x * Handle1, 0.0, -d3.y * Handle1}; + } + path.radius = static_cast(Radius); + return path; + }; + std::vector> zones; + for (auto const &zone : {std::pair{Curve.lead, 0.0}, {Curve.lead + Curve.ends, Curve.radius}, {Curve.lead + Curve.ends + Curve.between, Curve.middle}, {whole - Curve.lead, Curve.radius}, {whole, 0.0}}) + { + if (zone.first <= (zones.empty() ? 0.0 : zones.back().first) + 1e-9) + continue; + if (false == zones.empty() && zone.second == zones.back().second) + zones.back().first = zone.first; + else + zones.push_back(zone); + } + // broken at the tips of the blades of the tables, so the model of the switch finds them there + if (Curve.tip > 0.0) + for (auto const cut : {Curve.tip, whole - Curve.tip}) + for (std::size_t i = 0; i < zones.size(); ++i) + if (cut > (i > 0 ? zones[i - 1].first : 0.0) + 0.05 && cut < zones[i].first - 0.05) + { + zones.insert(zones.begin() + i, {cut, zones[i].second}); + break; + } + std::vector pieces; + auto from{From}; + for (auto const &[to, radius] : zones) + { + auto const upto{std::min(To, to)}; + if (upto <= from + 1e-6) + continue; + auto const handle{radius > 0.0 ? 4.0 / 3.0 * std::tan((upto - from) / radius / 4.0) * radius : 0.0}; + pieces.push_back(bend(from, upto, radius, handle, handle)); + from = upto; + } + if (pieces.size() == 1) + return pieces.front(); + // the handles of the curve from end to end reach 2/3 of the way to the crossing of the tangents + auto const chord{plan_of(place(To) - place(From))}; + auto const d0{turned(Heading, Turn * Curve.heading(From))}; + auto const d3{turned(Heading, Turn * Curve.heading(To))}; + auto const sine{cross(d0, d3)}; + auto path{bend(From, To, Curve.radius, 2.0 / 3.0 * cross(chord, d3) / sine, 2.0 / 3.0 * cross(d0, chord) / sine)}; + path.pieces = std::move(pieces); + return path; + }; + auto const back = [](segment_data const &Path) { + auto result{reversed(Path)}; + std::reverse(result.pieces.begin(), result.pieces.end()); + for (auto &piece : result.pieces) + piece = reversed(piece); + return result; + }; + auto const middle{whole * 0.5}; + auto const split{gap}; + auto const centreA{at(Crossing - First * gap, Height)}; + auto const centreB{at(Crossing + First * gap, Height)}; + auto const centreC{at(Crossing - Second * gap, Height)}; + auto const centreD{at(Crossing + Second * gap, Height)}; + std::string base; + auto const prefix{Type == "Skrz" ? "editor_skrz" : Type == "Rkp" ? "editor_dkp" : "editor_dks"}; + // a crossing keeps the names of its tracks, only its record holds the name + auto const recorded = [](std::string const &Name) { + auto const &records{switch_store::records()}; + return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; }); + }; + for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || recorded(base); ++i) + base = prefix + std::to_string(i); + std::vector parts; + if (Type == "Skrz") + { + // the two tracks just cross, their ends far from the crossing + parts.push_back(editor_track::create_path(Style, straight(PointA, PointB))); + parts.push_back(editor_track::create_path(Style, straight(PointC, PointD))); + parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); + record_slip(parts, "crossing", Type, 0.0, Angle, base); + return parts; + } + editor_track::switch_template shape; + shape.length = glm::distance(plan_of(PointA), Crossing); + bool const single{Type == "Rkp"}; + parts.push_back(editor_track::create_switch(shape, {straight(PointA, centreA), arc(PointA, First, side, PointD, 0.0, middle - split)}, Style, base + "_a")); + if (single) + parts.push_back(editor_track::create_path(Style, straight(PointB, centreB))); + else + parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), back(arc(PointC, Second, -side, PointB, middle + split, whole))}, Style, base + "_b")); + if (single) + parts.push_back(editor_track::create_path(Style, straight(PointC, centreC))); + else + parts.push_back(editor_track::create_switch(shape, {straight(PointC, centreC), arc(PointC, Second, -side, PointB, 0.0, middle - split)}, Style, base + "_c")); + parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), back(arc(PointA, First, side, PointD, middle + split, whole))}, Style, base + "_d")); + parts.push_back(editor_track::create_path(Style, straight(centreA, centreB))); + parts.push_back(editor_track::create_path(Style, straight(centreC, centreD))); + parts.push_back(editor_track::create_path(Style, arc(PointA, First, side, PointD, middle - split, middle + split))); + if (false == single) + parts.push_back(editor_track::create_path(Style, arc(PointC, Second, -side, PointB, middle - split, middle + split))); + parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); + record_slip(parts, "slip", Type, Curve.radius, Angle, base, Curve.middle); + return parts; +} + +bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape) +{ + auto &state{m_straights}; + state.status.clear(); + { + auto const sections{simulation::Region->sections(Point, 40.f)}; + for (auto *section : sections) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + if (Shape.type == "Rkpd" && track->eType == tt_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan_of(track->location()), plan_of(Point)) < 25.0) + return replace_double_slip(*track, Shape); + } + TTrack *other{nullptr}; + glm::dvec2 crossing{0.0}; + double best{40.0}; + auto const sections{simulation::Region->sections(Point, 60.f)}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *track : cell.m_paths) + { + if (std::find(Line.tracks.begin(), Line.tracks.end(), track) != Line.tracks.end() || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const b{plan_of(path.points[segment_data::point::end])}; + auto const segment{b - a}; + auto const determinant{cross(Line.direction, segment)}; + if (std::abs(determinant) < 1e-9) + continue; + auto const offset{a - plan_of(Line.start)}; + auto const along{cross(offset, segment) / determinant}; + auto const fraction{cross(offset, Line.direction) / determinant}; + if (fraction < -0.01 || fraction > 1.01 || along < 0.0 || along > Line.length) + continue; + auto const position{plan_of(Line.start) + Line.direction * along}; + auto const distance{glm::distance(position, plan_of(Point))}; + if (distance < best) + { + best = distance; + other = track; + crossing = position; + } + } + } + } + if (other == nullptr) + { + state.status = STR_C("No straight crossing the selected one near the click"); + return false; + } + auto const second{editor_track::find_straight(*other, m_straights.tolerance)}; + if (second.tracks.empty()) + return false; + auto const first{Line.direction}; + auto direction{second.direction}; + if (glm::dot(first, direction) < 0.0) + direction = -direction; + auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; + bool const plain{Shape.type == "Skrz"}; + if (angle < glm::radians(0.5) || angle > glm::radians(plain ? 89.0 : 30.0)) + { + state.status = STR_C("The straights cross at an angle unsuitable for a double slip"); + return false; + } + auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)}; + auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)}; + auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5}; + auto const *gauged{Line.tracks.front()}; + auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; + auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)}; + curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); + auto half{plain ? Shape.a : curve.tangent()}; + if (plain && half > available) + { + half = available; + if (half < 2.0) + { + state.status = STR_C("Not enough straight track around the crossing"); + return false; + } + } + else if (half > available) + { + curve = curve.scaled(available / half); + half = available; + if (curve.radius < 50.0) + { + state.status = STR_C("Not enough straight track around the crossing"); + return false; + } + state.status = STR_C("R reduced to ") + std::to_string(static_cast(curve.radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast(available)) + STR_C(" m from the crossing"); + } + + auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; + auto const heightsecond = [&](double const Along) { return second.start.y + second.grade * Along; }; + auto const sign{glm::dot(second.direction, direction) > 0.0 ? 1.0 : -1.0}; + auto const at = [&](glm::dvec2 const &Planar, double const Height) { return glm::dvec3{Planar.x, Height, Planar.y}; }; + auto const pointA{at(crossing - first * half, heightfirst(alongfirst - half))}; + auto const pointB{at(crossing + first * half, heightfirst(alongfirst + half))}; + auto const pointC{at(crossing - direction * half, heightsecond(alongsecond - sign * half))}; + auto const pointD{at(crossing + direction * half, heightsecond(alongsecond + sign * half))}; + + std::vector> states; + std::vector created; + std::vector removed; + TTrack *style{nullptr}; + if (false == cut_straight(Line, alongfirst - half, alongfirst + half, states, created, removed, &style)) + return false; + auto const secondline{editor_track::find_straight(*other, m_straights.tolerance)}; + auto const secondalong{glm::dot(crossing - plan_of(secondline.start), secondline.direction)}; + if (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr)) + { + for (auto *track : created) + editor_track::retire(*track); + for (auto const &entry : states) + { + editor_track::apply(*entry.first, entry.second); + editor_track::commit({entry.first}); + } + for (auto *track : removed) + { + track->m_editorremoved = false; + editor_track::commit({track}); + } + return false; + } + auto &styled{style != nullptr ? *style : *Line.tracks.front()}; + // a single slip joins the pair of the tracks on the side of the click + auto const nearer{glm::distance((plan_of(pointA) + plan_of(pointD)) * 0.5, plan_of(Point)) <= glm::distance((plan_of(pointC) + plan_of(pointB)) * 0.5, plan_of(Point))}; + auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, curve, styled, Shape.type) + : build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, curve, styled, Shape.type)}; + editor_track::commit(parts); + created.insert(created.end(), parts.begin(), parts.end()); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + switch_placed(parts); + return true; +} + +bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape) +{ + auto &state{m_straights}; + auto const base{Part.name().substr(0, Part.name().size() - 2)}; + std::array switches{}; + for (int i = 0; i < 4; ++i) + { + switches[i] = simulation::Paths.find(base + "_" + static_cast('a' + i)); + if (switches[i] == nullptr || switches[i]->m_editorremoved || switches[i]->eType != tt_Switch || switches[i]->m_paths.size() < 2) + { + state.status = STR_C("The double slip isn't complete, it can't be replaced"); + return false; + } + } + struct end + { + TTrack *track; + glm::dvec3 point; + glm::dvec2 inward; + TTrack *outer; + }; + std::vector ends; + std::vector parts(switches.begin(), switches.end()); + for (auto *track : switches) + { + segment_data const *straight{nullptr}; + for (auto const &path : track->m_paths) + if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) + straight = &path; + if (straight == nullptr) + straight = &track->m_paths.front(); + auto const start{straight->points[segment_data::point::start]}; + ends.push_back({track, start, glm::normalize(plan_of(straight->points[segment_data::point::end] - start)), track->SwitchExtension->pPrevs[0]}); + for (int i = 0; i < 2; ++i) + { + auto *inner{track->SwitchExtension->pNexts[i]}; + if (inner != nullptr && inner->eType == tt_Normal && std::find(parts.begin(), parts.end(), inner) == parts.end() && inner->Length() < 5.0) + parts.push_back(inner); + } + } + int partner{-1}; + for (int i = 1; i < 4; ++i) + if (glm::dot(ends[0].inward, ends[i].inward) < -0.999) + partner = i; + if (partner < 0) + { + state.status = STR_C("The double slip geometry isn't recognized"); + return false; + } + auto const &a{ends[0]}; + auto const &b{ends[partner]}; + std::vector others; + for (int i = 1; i < 4; ++i) + if (i != partner) + others.push_back(i); + if (glm::dot(ends[0].inward, ends[others[0]].inward) < 0.0) + std::swap(others[0], others[1]); + auto const &c{ends[others[0]]}; + auto const &d{ends[others[1]]}; + auto const first{a.inward}; + auto direction{c.inward}; + auto const determinant{cross(first, direction)}; + if (std::abs(determinant) < 1e-9) + return false; + auto const t{cross(plan_of(c.point) - plan_of(a.point), direction) / determinant}; + glm::dvec2 const crossing{plan_of(a.point) + first * t}; + auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; + auto const oldhalf{glm::distance(plan_of(a.point), crossing)}; + auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; + auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(switches[0]->fTrackWidth) + switches[0]->rail_dimensions().head + 0.002, blade.first, blade.second)}; + curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); + auto const full{curve.tangent()}; + auto radius{curve.radius}; + auto half{full}; + auto const height{(a.point.y + b.point.y) * 0.5}; + + std::vector> states; + std::vector created; + std::vector removed; + if (half > oldhalf) + { + for (auto const *e : {&a, &b, &c, &d}) + { + if (e->outer == nullptr || false == editor_track::is_straight(*e->outer, m_straights.tolerance)) + { + half = oldhalf; + radius = curve.radius * half / full; + state.status = STR_C("R limited to ") + std::to_string(static_cast(radius)) + STR_C(" m, a neighbouring path isn't straight"); + break; + } + auto const line{editor_track::find_straight(*e->outer, m_straights.tolerance)}; + auto const reach{std::abs(glm::dot(plan_of(line.end) - crossing, -e->inward))}; + auto const reachstart{std::abs(glm::dot(plan_of(line.start) - crossing, -e->inward))}; + if (std::max(reach, reachstart) - 0.5 < half) + { + half = std::min(half, std::max(reach, reachstart) - 0.5); + radius = curve.radius * half / full; + state.status = STR_C("R reduced to ") + std::to_string(static_cast(radius)) + STR_C(" m to fit the straights"); + } + } + } + for (auto *track : parts) + editor_track::retire(*track); + removed.insert(removed.end(), parts.begin(), parts.end()); + TTrack *style{a.outer != nullptr && a.outer->eType == tt_Normal ? a.outer : switches[0]}; + std::array points; + std::array order{&a, &b, &c, &d}; + for (int i = 0; i < 4; ++i) + { + auto const &e{*order[i]}; + auto const outward{-e.inward}; + auto const target{crossing + outward * half}; + points[i] = {target.x, e.point.y, target.y}; + if (half < oldhalf - 0.01) + { + auto *filler{editor_track::create_path(*style, [&] { + segment_data path; + path.points[segment_data::point::start] = points[i]; + path.points[segment_data::point::end] = e.point; + return path; + }())}; + created.push_back(filler); + } + else if (half > oldhalf + 0.01 && e.outer != nullptr) + { + auto const line{editor_track::find_straight(*e.outer, m_straights.tolerance)}; + auto const alongold{glm::dot(plan_of(e.point) - plan_of(line.start), line.direction)}; + auto const alongnew{glm::dot(target - plan_of(line.start), line.direction)}; + cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr); + } + } + auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, curve.scaled(half / full), *style)}; + std::vector commitlist(parts2.begin(), parts2.end()); + for (auto *track : created) + if (std::find(commitlist.begin(), commitlist.end(), track) == commitlist.end()) + commitlist.push_back(track); + editor_track::commit(commitlist); + created.insert(created.end(), parts2.begin(), parts2.end()); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + // the drives of the switches replaced go with them, the new ones get theirs + std::vector drives; + for (auto const *track : switches) + if (auto const drive{find_switch_drive(*track)}) + drives.push_back(drive->standing.instance); + if (false == drives.empty() && remove_templates(drives) > 0) + join_last_steps(); + for (auto *track : parts2) + if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track)) + join_last_steps(); + return true; +} + +std::vector editor_mode::sample_chain(editor_track::chain const &Chain) +{ + std::vector result; + double station{0.0}; + for (std::size_t i = 0; i < Chain.tracks.size(); ++i) + { + auto const &path{Chain.tracks[i]->m_paths.front()}; + auto const forward{Chain.forward[i]}; + bezier const curve{path}; + auto const length{glm::distance(curve.p0, curve.p3)}; + int const steps{std::max(8, static_cast(std::ceil(length / 0.5)))}; + for (int k = (result.empty() ? 0 : 1); k <= steps; ++k) + { + auto const t{forward ? static_cast(k) / steps : 1.0 - static_cast(k) / steps}; + auto const position{curve.point(t)}; + auto const derivative{forward ? curve.first(t) : -curve.first(t)}; + if (false == result.empty()) + station += plan_distance(position, result.back().position); + curve_sample sample; + sample.station = station; + sample.position = position; + sample.tangent = glm::normalize(plan_of(derivative)); + auto const roll{path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t}; + sample.roll = static_cast(forward ? roll : -roll); + sample.radius = path.radius; + result.push_back(sample); + } + } + return result; +} + +editor_mode::curve_sample editor_mode::sample_at(std::vector const &Frame, double const Station) +{ + if (Frame.empty()) + return {}; + if (Station <= Frame.front().station) + return Frame.front(); + if (Station >= Frame.back().station) + return Frame.back(); + auto const next{std::lower_bound(Frame.begin(), Frame.end(), Station, [](curve_sample const &Sample, double const Value) { return Sample.station < Value; })}; + auto const previous{std::prev(next)}; + auto const span{std::max(1e-9, next->station - previous->station)}; + auto const f{(Station - previous->station) / span}; + curve_sample result; + result.station = Station; + result.position = glm::mix(previous->position, next->position, f); + result.tangent = glm::normalize(glm::mix(previous->tangent, next->tangent, f)); + result.roll = static_cast(previous->roll + (next->roll - previous->roll) * f); + result.radius = previous->radius; + return result; +} + +double editor_mode::nearest_station(std::vector const &Frame, glm::dvec3 const &Point) +{ + double best{std::numeric_limits::max()}; + double station{0.0}; + for (std::size_t i = 0; i + 1 < Frame.size(); ++i) + { + glm::dvec2 const a{Frame[i].position.x, Frame[i].position.z}; + glm::dvec2 const b{Frame[i + 1].position.x, Frame[i + 1].position.z}; + auto const segment{b - a}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(Point), a + segment * t)}; + if (distance < best) + { + best = distance; + station = Frame[i].station + (Frame[i + 1].station - Frame[i].station) * t; + } + } + return station; +} + +std::vector editor_mode::curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side) +{ + auto const map = [&](glm::dvec3 const &Local, glm::dvec2 &Tangent, glm::dvec2 const &Localtangent) { + auto const sample{sample_at(Frame, Station + Direction * Local.z)}; + auto const along{sample.tangent * static_cast(Direction)}; + glm::dvec2 const across{glm::dvec2{-along.y, along.x} * static_cast(Side)}; + Tangent = glm::normalize(along * Localtangent.y + across * Localtangent.x); + auto const planar{plan_of(sample.position) + across * Local.x}; + auto const cant{glm::radians(static_cast(sample.roll) * Direction)}; + auto const cone{-Local.x * Side * std::tan(cant)}; + return glm::dvec3{planar.x, sample.position.y + Local.y + cone, planar.y}; + }; + auto const hermite = [](glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection) { + auto const handle{plan_distance(Start, End) / 3.0}; + segment_data path; + path.points[segment_data::point::start] = Start; + path.points[segment_data::point::end] = End; + path.points[segment_data::point::control1] = {Startdirection.x * handle, (End.y - Start.y) / 3.0, Startdirection.y * handle}; + path.points[segment_data::point::control2] = {-Enddirection.x * handle, -(End.y - Start.y) / 3.0, -Enddirection.y * handle}; + return path; + }; + auto const bend = [&](segment_data const &Local) { + auto const &start{Local.points[segment_data::point::start]}; + auto const &end{Local.points[segment_data::point::end]}; + auto const &control1{Local.points[segment_data::point::control1]}; + auto const &control2{Local.points[segment_data::point::control2]}; + glm::dvec2 const chord{end.x - start.x, end.z - start.z}; + auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)}; + auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; + glm::dvec2 startdirection; + glm::dvec2 enddirection; + auto const p0{map(start, startdirection, localstart)}; + auto const p3{map(end, enddirection, localend)}; + auto path{hermite(p0, startdirection, p3, enddirection)}; + { + auto const cone = [&](double const T) { + auto const point{bezier{path}.point(T)}; + auto const sample{sample_at(Frame, nearest_station(Frame, point))}; + auto const lateral{glm::dot(plan_of(point) - plan_of(sample.position), glm::dvec2{-sample.tangent.y, sample.tangent.x})}; + return sample.position.y - lateral * std::tan(glm::radians(static_cast(sample.roll))); + }; + auto const h1{cone(1.0 / 3.0)}; + auto const h2{cone(2.0 / 3.0)}; + auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * p0.y + p3.y / 3.0}; + auto const y2{3.0 * h2 - 1.5 * h1 + p0.y / 3.0 - 5.0 / 6.0 * p3.y}; + path.points[segment_data::point::control1].y = y1 - p0.y; + path.points[segment_data::point::control2].y = y2 - p3.y; + } + auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction}; + auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction}; + path.rolls = {static_cast(rollstart), static_cast(rollend)}; + path.radius = Local.radius; + { + bezier const curve{path}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; + if (Local.radius != 0.f && std::abs(turn) > 1e-6) + path.radius = static_cast(std::copysign(curve.plan_length() / std::abs(turn), static_cast(Local.radius))); + } + return path; + }; + std::vector locals{Shape.local.begin(), Shape.local.end()}; + if (Shape.twofold()) + { + locals.insert(locals.end(), Shape.second.begin(), Shape.second.end()); + locals.push_back(Shape.link); + } + std::vector result; + for (std::size_t i = 0; i < locals.size(); ++i) + { + auto const &local{locals[i]}; + auto path{bend(local)}; + for (auto const &piece : local.pieces) + path.pieces.push_back(bend(piece)); + // the main tracks follow the curve they are laid into + if (i % 2 == 0) + path.radius = sample_at(Frame, Station + Direction * 0.5 * (local.points[segment_data::point::start].z + local.points[segment_data::point::end].z)).radius; + result.push_back(path); + } + return result; +} + +void editor_mode::insert_curved_switch(int const Direction, int const Side) +{ + auto &tool{m_switch}; + auto const &shape{tool.templates[tool.armed]}; + auto const total{tool.frame.back().station}; + if (shape.length > total - 0.02) + { + tool.status = format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length); + return; + } + auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; + auto const from{Direction > 0 ? station : station - shape.length}; + auto const to{from + shape.length}; + std::vector> states; + std::vector created; + std::vector removed; + std::vector candidates{tool.frame_tracks}; + for (auto const cut : {from, to}) + { + auto const point{sample_at(tool.frame, cut).position}; + for (auto *candidate : std::vector(candidates)) + { + if (candidate->m_editorremoved || candidate->eType != tt_Normal) + continue; + auto const t{editor_track::nearest_parameter(*candidate, point)}; + auto const found{editor_track::point_at(*candidate, t)}; + if (plan_distance(found, point) > 0.05) + continue; + if (t > 0.002 && t < 0.998) + { + if (std::find(created.begin(), created.end(), candidate) == created.end() && std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == candidate; })) + states.emplace_back(candidate, editor_track::capture(*candidate)); + if (auto *second{editor_track::split_path(*candidate, t)}) + { + created.push_back(second); + candidates.push_back(second); + } + } + break; + } + } + for (auto *candidate : candidates) + { + if (candidate->m_editorremoved) + continue; + auto const middle{editor_track::point_at(*candidate, 0.5)}; + auto const where{nearest_station(tool.frame, middle)}; + if (where > from + 0.01 && where < to - 0.01) + { + editor_track::retire(*candidate); + removed.push_back(candidate); + } + } + auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; + auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; + created.insert(created.end(), made.begin(), made.end()); + if (made.size() == 1) + tool.status = STR_C("Switch ") + made.front()->name() + STR_C(" inserted into the curve"); + auto const placed{created}; + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + switch_placed(placed); +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 75f5211a..ab708a5d 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -41,214 +41,10 @@ http: #include #include -namespace -{ +#include "application/editormodetrackshared.h" -using geometry::bezier; -using geometry::cross; -using geometry::grade_of; -using geometry::plan_distance; -using geometry::plan_of; -using geometry::signed_angle; -using geometry::turned; -using geometry::arc_pieces; -using geometry::arc_through; -using geometry::has_controls; -using geometry::level; -using geometry::reversed; -using geometry::straight_piece; +using namespace track_mode; -double constexpr kParallelFindAngle{15.0}; // deg, neighbours counted as beside the straight -double constexpr kParallelSnapAngle{5.0}; // deg, close enough to snap parallel -double constexpr kParallelSnapWindow{0.45}; // m from 4.00 / 4.50 / 4.75 m -double constexpr kParallelHintWindow{1.25}; // m, hint before the snap takes - -double piece_plan_radius(segment_data const &Path) -{ - if (std::abs(Path.radius) > 0.5f) - return std::abs(Path.radius); - bezier const curve{Path}; - auto const begin{plan_of(curve.first(0.0))}; - auto const finish{plan_of(curve.first(1.0))}; - if (glm::length(begin) < 1e-9 || glm::length(finish) < 1e-9) - return 0.0; - auto const turn{std::abs(signed_angle(begin, finish))}; - if (turn < 1e-4) - return 0.0; - return curve.plan_length() / turn; -} - -std::string lines_of(std::vector const &Texts) -{ - std::string text; - for (auto const &line : Texts) - text += (text.empty() ? "" : "\n") + line; - return text; -} - -std::string narrowed(std::vector const &Pieces, editor_track::layout const Layout, double const Radius) -{ - if (Layout != editor_track::layout::arc_straight && Layout != editor_track::layout::straight_arc) - return {}; - double sharpest{0.0}; - for (auto const &piece : Pieces) - if (auto const radius{piece_plan_radius(piece)}; radius >= 0.5 && (sharpest == 0.0 || radius < sharpest)) - sharpest = radius; - if (sharpest == 0.0 || sharpest >= Radius * 0.99) - return {}; - return format(STR_C("R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m"), Radius, sharpest); -} - -std::string describe(editor_track::straight const &Line) -{ - return format("L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); -} - -char const *point_label(editor_track::point_kind const Kind) -{ - switch (Kind) - { - case editor_track::point_kind::start: return STR_C("Start"); - case editor_track::point_kind::control1: return STR_C("Control 1"); - case editor_track::point_kind::control2: return STR_C("Control 2"); - default: return STR_C("End"); - } -} - -editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, editor_track::point_kind::control1, editor_track::point_kind::control2, editor_track::point_kind::end}; - -float const kHandleRadius{10.0f}; -double const kLaySnapRadius{5.0}; -float const kSnapPixels{16.0f}; -double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch -double const kLineReach{2000.0}; // how far each way the Curve tab follows the line -double const kDigitizeFit{0.5}; -double const kDrawnReach{100000.0}; -double const kJointRange{400.0}; // the ends of the paths are drawn for the tracks this close to the camera -ImU32 const kJointColor{IM_COL32(255, 255, 255, 200)}; -int const kRailCategory{1}; - -editor_track::branch_end branch_start(segment_data const &Diverging) -{ - auto const tangent{bezier{Diverging}.first(1.0)}; - return {Diverging.points[segment_data::point::end], plan_of(tangent), grade_of(tangent), static_cast(Diverging.rolls[1])}; -} - -editor_track::branch_end branch_target(editor_track::snap_target const &Target) -{ - return {Target.position, plan_of(Target.direction), grade_of(Target.direction), Target.track != nullptr ? editor_track::joint_roll(*Target.track, Target.point) : 0.0}; -} - -double switch_from(editor_track::straight const &Line, double const Length, double const Along, int const Direction, bool const Snap) -{ - auto from{std::clamp(Direction > 0 ? Along : Along - Length, 0.0, Line.length - Length)}; - if (false == Snap) - return from; - std::vector joints{0.0, Line.length}; - for (std::size_t i = 0; i < Line.tracks.size(); ++i) - { - auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; - for (auto const index : {segment_data::point::start, segment_data::point::end}) - joints.push_back(Line.along(path.points[index])); - } - auto const nearest = [&](double const Value) { - double best{Value}; - for (auto const joint : joints) - if (std::abs(joint - Value) < std::abs(best - Value) || best == Value) - best = std::abs(joint - Value) < 1.0 ? joint : best; - return best; - }; - auto const snappedstart{nearest(from)}; - auto const snappedend{nearest(from + Length)}; - if (snappedstart != from) - from = snappedstart; - else if (snappedend != from + Length) - from = snappedend - Length; - return std::clamp(from, 0.0, Line.length - Length); -} - -std::string link_summary(editor_track::snap_target const &Target, editor_track::branch_link const &Link, double const Switchradius) -{ - auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}}; - auto text{Link.reverse ? format(STR_C("The branch leads into %s with reverse curves R %.0f m"), name.c_str(), Link.radius) : - Link.radius > 0.0 ? format(STR_C("The branch leads into %s with an R %.0f m curve"), name.c_str(), Link.radius) : format(STR_C("The branch leads straight into %s"), name.c_str())}; - if (Link.insert > 0.0) - text += format(STR_C(", a straight of %.1f m between them"), Link.insert); - if (Link.reverse && Link.radius < Switchradius - 0.5) - text += format(STR_C(", sharper than the switch (R %.0f m)"), Switchradius); - if (Link.trim > 0.0) - text += format(STR_C(", %s shortened %.2f m"), name.c_str(), Link.trim); - else if (Link.trim < 0.0) - text += format(STR_C(", %s lengthened %.2f m"), name.c_str(), -Link.trim); - return text; -} - -std::optional eased_join(alignment::join_request const &Request, editor_track::easing const &In, editor_track::easing const &Out) -{ - if (In.pieces.empty() && Out.pieces.empty()) - return std::nullopt; - auto middle{Request}; - if (false == In.pieces.empty()) - { - middle.start.position = In.end; - middle.start.direction = In.direction; - middle.start.roll = In.pieces.back().rolls[1]; - } - if (false == Out.pieces.empty()) - { - middle.end.position = Out.end; - middle.end.direction = -Out.direction; - middle.end.roll = -Out.pieces.back().rolls[1]; - } - if (false == editor_track::eases_room(In, Out, Request.start.position, Request.start.direction, Request.end.position, Request.end.direction)) - return std::nullopt; - auto eased{alignment::join(middle)}; - if (eased.kind == alignment::join_kind::none || eased.kind == alignment::join_kind::bezier) - return std::nullopt; - eased.length = editor_track::eased_length(In, eased.length, Out); - eased.pieces = editor_track::eased_pieces(In, eased.pieces, Out); - return eased; -} - -// 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) -{ - if (false == Curve.compound || Curve.arcs.size() < 2) - return; - Vertex.compound = true; - Vertex.radius = std::round(Curve.arcs.front().radius); - Vertex.share = Curve.arcs.front().turn; - Vertex.arcs.clear(); - for (std::size_t i = 1; i < Curve.arcs.size(); ++i) - Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); -} - -char const *type_name(TTrack const &Track) -{ - switch (Track.eType) - { - case tt_Normal: return "normal"; - case tt_Switch: return "switch"; - case tt_Cross: return "cross"; - case tt_Table: return "turntable"; - case tt_Tributary: return "tributary"; - default: return "unknown"; - } -} - -} TTrack *editor_mode::selected_track() const { @@ -1428,2178 +1224,6 @@ void editor_mode::render_path_ui() } -bool editor_mode::route_active() const -{ - return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && 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; -} - -void editor_mode::route_bind_ends() -{ - auto const &chain{m_route.chain}; - auto &design{m_route.design}; - design.start = chain.start; - design.end = chain.end; - design.start_direction = glm::normalize(plan_of(chain.start_direction)); - design.end_direction = glm::normalize(plan_of(chain.end_direction)); - design.start_grade = grade_of(chain.start_direction); - design.end_grade = grade_of(chain.end_direction); - design.start_radius = chain.start_radius; - design.end_radius = chain.end_radius; - design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance)); - design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance)); -} - -void editor_mode::route_reset() -{ - auto &route{m_route}; - route.chain = {}; - route.error.clear(); - route.status.clear(); - route.vertex = -1; - route.grip = -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, true)) - return; - if (false == route.chain.switches.empty()) - route.status = format(STR_C("The fragment runs through %zu switches along their main tracks. They move as a whole with the line, " - "so they have to fall on its straights"), - route.chain.switches.size()); - - auto const &chain{route.chain}; - alignment::design design; - design.norms = route.design.norms; - design.shape = route.design.shape; - design.transition_pieces = route.design.transition_pieces; - design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; - route.design = design; - route_bind_ends(); - design = route.design; - - 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{plan_distance(design.start, design.end)}; - vertex.offset = distance / 3.0; - route.design.vertices.push_back(vertex); - route.design.vertices.push_back(vertex); - } - } - route_update(); -} - -bool editor_mode::route_from_curve(TTrack &Track) -{ - editor_track::curve curve; - if (false == editor_track::find_curve(Track, m_straights.tolerance, m_route.design.norms.gauge, curve)) - { - m_route.chain = {}; - m_route.result = {}; - m_route.vertex = -1; - m_route.grip = -1; - m_route.error = STR_C("No curve here: LMB on a curve"); - return false; - } - m_route.from = curve.from; - m_route.to = curve.to; - route_reset(); - if (m_route.chain.tracks.empty()) - return false; - auto &design{m_route.design}; - auto vertex{route_vertex_of(curve)}; - m_route.designed = curve.designed.has_value() ? 1 : 0; - if (curve.reversals == 0) - { - design.vertices = {vertex}; - } - else - { - auto const distance{plan_distance(design.start, design.end)}; - vertex.offset = distance / 3.0; - vertex.compound = false; - vertex.reverse_turn = false; - design.vertices = {vertex, vertex}; - } - route_update(); - m_route.status = STR_C("Curve of ") + std::to_string(m_route.chain.tracks.size()) + STR_C(" paths, turning ") + std::to_string(static_cast(std::round(glm::degrees(curve.turn)))) + STR_C(" deg"); - return true; -} - -alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const -{ - if (Curve.designed.has_value()) - return *Curve.designed; - alignment::vertex vertex; - vertex.radius = Curve.radius > 0.0 ? std::max(1.0, Curve.radius) : 1000.0; - vertex.transition_in = std::max(0.0, Curve.transition_in); - vertex.transition_out = std::max(0.0, Curve.transition_out); - vertex.cant = std::max(0.0, Curve.cant); - vertex.reverse_turn = std::abs(Curve.turn) > glm::pi(); - compound_from(Curve, vertex); - return vertex; -} - - -bool editor_mode::route_load(TTrack &Track) -{ - m_route.designed = 0; - if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track))) - return false; - route_read_stored(Track); - return true; -} - -// the design kept in the scenery for the fragment takes the place of the one read from the paths; its chainage is carried on -void editor_mode::route_read_stored(TTrack &Track) -{ - auto &route{m_route}; - route.departed = -1; - route.departure = 0.0; - route.origin = 0.0; - if (route.chain.tracks.empty()) - return; - auto const chain{route.chain}; - bool reversed; - auto index{alignment_store::find(chain.start, chain.end, reversed)}; - route.departed = alignment_store::departed(chain.tracks, route.departure); - auto const loose{std::any_of(chain.tracks.begin(), chain.tracks.end(), [](TTrack const *Track) { return false == alignment_store::on_track(*Track).has_value(); })}; - bool digitized{false}; - int parts{0}; - int skipped{0}; - std::string failure; - if (index < 0 && route.departed < 0 && loose && false == route.single) - { - parts = digitize_parts(chain, &failure, skipped); - if (parts == 1 && skipped == 0) - { - alignment_store::settle(); - index = alignment_store::find(chain.start, chain.end, reversed); - digitized = true; - } - else if (parts > 0) - { - alignment_store::settle(); - route_from_line(Track); - route.departed = alignment_store::departed(route.chain.tracks, route.departure); - } - } - if (index >= 0 && false == reversed && false == route.single) - { - route_take_stored(index); - } - else if (auto const km{alignment_store::chainage_at(chain.start)}) - { - auto const ahead{chain.start + glm::dvec3{route.design.start_direction.x, 0.0, route.design.start_direction.y}}; - auto const further{alignment_store::chainage_at(ahead)}; - if (further.has_value() && *further > *km) - route.origin = *km; - } - if (digitized && route.designed == 0) - route.status += STR_C("\nThe line is digitized now, a straight"); - else if (digitized) - route.status += format(STR_C("\nThe line is digitized now, curves kept in the scenery: %d"), route.designed); - else if (parts > 0) - route.status += format(STR_C("\nThe line is digitized in parts: %d, paths left out: %d"), parts, skipped) + (failure.empty() ? std::string{} : " - " + failure); - else if (skipped > 0 && loose && skipped < static_cast(chain.tracks.size())) - route.status += format(STR_C("\nPaths left out of the digitized line: %d"), skipped) + (failure.empty() ? std::string{} : " - " + failure); - else if (false == failure.empty()) - route.status += STR_C("\nThe line can't be digitized: ") + failure; - else if (route.designed > 0) - route.status += format(STR_C("\nCurves read from the design kept in the scenery: %d"), route.designed); - else if (false == route.design.vertices.empty()) - route.status += STR_C("\nCurves recognized from the paths, the scenery keeps no design for them"); -} - -void editor_mode::route_take_stored(int const Index) -{ - auto &route{m_route}; - auto const &records{alignment_store::records()}; - if (Index < 0 || Index >= static_cast(records.size())) - return; - auto const &stored{records[Index]}; - auto &design{route.design}; - design.vertices = stored.design.vertices; - design.shape = stored.design.shape; - design.speed = stored.design.speed; - design.norms = stored.design.norms; - design.transition_pieces = stored.design.transition_pieces; - design.arc_piece_angle = stored.design.arc_piece_angle; - route.designed = static_cast(design.vertices.size()); - route.origin = stored.origin; - route.vertex = -1; - route.grip = -1; - route_update(); -} - -void editor_mode::render_route_stored() -{ - auto &route{m_route}; - ImGui::PushItemWidth(120.0f); - auto km{route.origin / 1000.0}; - if (ImGui::InputDouble(STR_C("Km of the start"), &km, 0.0, 0.0, "%.3f")) - route.origin = km * 1000.0; - ImGui::PopItemWidth(); - item_tooltip("Chainage of the start of the fragment, kept with the design in the scenery; the vertical profile of these paths counts from it"); - if (ImGui::SmallButton(STR_C("Digitize all the tracks"))) - { - digitize_all(); - return; - } - item_tooltip("Every line of the scenery not digitized yet is kept in it with its straights, arcs and transition curves, the way the drawn tracks are kept; the paths stay as they are"); - auto const &records{alignment_store::records()}; - if (route.departed < 0 || route.departed >= static_cast(records.size())) - return; - auto const &layer{scene::Layers.layer(records[route.departed].layer).name}; - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The design kept in %s for this fragment departs from its paths by up to %.2f m"), layer.c_str(), route.departure); - if (ImGui::SmallButton(STR_C("Take the kept design"))) - { - route_take_stored(route.departed); - route.status = STR_C("The kept design is loaded, applying it lays the paths by it again"); - } - item_tooltip("Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied"); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Forget the kept design"))) - { - alignment_store::forget(route.departed); - alignment_store::settle(); - route.departed = -1; - route.status = STR_C("The kept design will be removed from ") + layer + STR_C(" on save"); - } - item_tooltip("The paths stay as they are, the scenery no longer keeps a design for them"); -} - -void editor_mode::route_store() -{ - auto &route{m_route}; - if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) - return; - alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front())); - route.departed = -1; - route.departure = 0.0; -} - -bool editor_mode::digitize_drawn(std::vector const &Created, bool const Alone) -{ - std::vector drawn; - for (auto *track : Created) - if (track != nullptr && track->eType == tt_Normal && false == track->m_editorremoved && false == track->m_paths.empty()) - drawn.push_back(track); - if (drawn.empty() || scene::Layers.empty()) - return false; - editor_track::chain chain; - std::string error; - if (editor_track::find_run(*drawn.front(), kDrawnReach, chain, error) && digitize_chain(chain)) - return true; - if (Alone && editor_track::find_chain(drawn.front(), drawn.back(), chain, error) && chain.tracks.size() == drawn.size() && digitize_chain(chain)) - return true; - WriteLog("Editor: the drawn paths don't make a line of straights, arcs and transition curves, no design kept for them", logtype::generic); - return false; -} - -void editor_mode::digitize_all() -{ - std::set visited; - int kept{0}; - int failed{0}; - std::string first; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || false == editor_track::is_supported(*track) || visited.count(track) > 0) - continue; - if (alignment_store::on_track(*track).has_value()) - continue; - editor_track::chain chain; - std::string error; - if (false == editor_track::find_run(*track, kDrawnReach, chain, error) || chain.tracks.empty()) - { - visited.insert(track); - continue; - } - visited.insert(chain.tracks.begin(), chain.tracks.end()); - std::string reason; - kept += digitize_parts(chain, &reason, failed); - if (first.empty()) - first = reason; - } - alignment_store::settle(); - auto text{format(STR_C("Lines digitized: %d, paths left out: %d"), kept, failed)}; - if (false == first.empty()) - text += STR_C(", the first: ") + first; - WriteLog("Editor: " + text, logtype::generic); - ui()->set_status(text, failed > 0); - if (false == m_route.chain.tracks.empty()) - route_load(*m_route.chain.tracks[m_route.chain.tracks.size() / 2]); -} - -int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *Reason, int &Skipped) -{ - auto const count{Chain.tracks.size()}; - if (count == 0 || std::all_of(Chain.tracks.begin(), Chain.tracks.end(), [](TTrack const *Track) { return alignment_store::on_track(*Track).has_value(); })) - return 0; - TTrack *culprit{nullptr}; - std::string reason; - if (digitize_chain(Chain, &reason, &culprit)) - return 1; - if (count == 1) - { - ++Skipped; - reason = (Chain.tracks.front()->name().empty() ? std::string{"(noname)"} : Chain.tracks.front()->name()) + ": " + reason; - WriteLog("Editor: digitizing - " + reason, logtype::generic); - if (Reason != nullptr && Reason->empty()) - *Reason = reason; - return 0; - } - auto const found{static_cast(std::find(Chain.tracks.begin(), Chain.tracks.end(), culprit) - Chain.tracks.begin())}; - auto const split{found < count ? std::max(1, found) : count / 2}; - int parts{0}; - for (auto const &[first, last] : {std::pair{0, split - 1}, std::pair{split, count - 1}}) - { - editor_track::chain part; - std::string error; - if (editor_track::find_chain(Chain.tracks[first], Chain.tracks[last], part, error) && part.tracks.size() == last - first + 1) - parts += digitize_parts(part, Reason, Skipped); - else - Skipped += static_cast(last - first + 1); - } - return parts; -} - -bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string *Reason, TTrack **Culprit) -{ - if (Chain.tracks.empty()) - return false; - auto const name = [](TTrack const *Track) { return Track == nullptr || Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; - auto const nearest = [&](glm::dvec2 const &Point) { - TTrack *found{nullptr}; - double distance{std::numeric_limits::max()}; - for (auto *track : Chain.tracks) - for (auto const end : {segment_data::point::start, segment_data::point::end}) - if (auto const d{glm::distance(plan_of(track->m_paths.front().points[end]), Point)}; d < distance) - { - distance = d; - found = track; - } - return found; - }; - alignment::design design; - design.norms = m_route.design.norms; - design.transition_pieces = m_route.design.transition_pieces; - design.arc_piece_angle = m_route.design.arc_piece_angle; - design.speed = Chain.velocity > 0.0 ? Chain.velocity : 100.0; - design.start = Chain.start; - design.end = Chain.end; - if (glm::length(plan_of(Chain.start_direction)) < 1e-9 || glm::length(plan_of(Chain.end_direction)) < 1e-9) - return false; - design.start_direction = glm::normalize(plan_of(Chain.start_direction)); - design.end_direction = glm::normalize(plan_of(Chain.end_direction)); - design.start_grade = grade_of(Chain.start_direction); - design.end_grade = grade_of(Chain.end_direction); - design.start_radius = Chain.start_radius; - design.end_radius = Chain.end_radius; - auto const preferred{m_route.design.shape}; - auto const other{preferred == alignment::transition_shape::clothoid ? alignment::transition_shape::cubic_parabola : alignment::transition_shape::clothoid}; - alignment::result result; - alignment::design best; - alignment::result bestresult; - double closest{std::numeric_limits::max()}; - TTrack *farthest{nullptr}; - bool fits{false}; - for (auto const attempt : {0, 1, 2, 3}) - { - auto const shape{attempt % 2 == 0 ? preferred : other}; - design.shape = shape; - if (attempt == 2) - { - design.norms.arc_min = 0.0; - design.norms.arc_min_factor = 0.0; - } - design.vertices.clear(); - for (auto const &entry : editor_track::recognize_line(Chain, m_straights.tolerance, design.norms.gauge, shape)) - { - if (entry.kink) - { - if (Reason != nullptr) - *Reason = format(STR_C("the straights meet with no curve at path %s"), name(nearest(entry.position)).c_str()); - if (Culprit != nullptr) - *Culprit = nearest(entry.position); - return false; - } - auto vertex{route_vertex_of(entry.shape)}; - vertex.position = entry.position; - design.vertices.push_back(vertex); - } - if (design.vertices.size() >= 2) - { - design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); - design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); - } - result = alignment::compute(design); - if (alignment_store::follows(result, Chain.tracks)) - { - fits = true; - break; - } - TTrack *worst{nullptr}; - if (auto const away{alignment_store::deviation(result, Chain.tracks, worst)}; away < closest) - { - closest = away; - farthest = worst; - best = design; - bestresult = result; - } - } - double fit{0.0}; - if (false == fits && closest <= kDigitizeFit) - { - design = best; - result = bestresult; - fit = closest; - fits = true; - } - if (false == fits) - { - if (Culprit != nullptr) - *Culprit = farthest; - if (Reason != nullptr) - *Reason = closest < std::numeric_limits::max() ? format(STR_C("the curves read from the paths leave path %s by %.2f m"), name(farthest).c_str(), closest) : std::string{STR_C("the paths don't make a line of straights, arcs and transition curves")}; - return false; - } - auto const &records{alignment_store::records()}; - std::set covered; - for (auto *track : Chain.tracks) - if (auto const found{alignment_store::on_track(*track)}) - covered.insert(found->record); - double origin{0.0}; - for (auto const index : covered) - { - auto const &old{records[index]}; - auto const start{alignment_store::chainage_on(result, old.design.start)}; - auto const end{alignment_store::chainage_on(result, old.design.end)}; - if (false == start.has_value() || false == end.has_value()) - return false; - if (index == *covered.begin() && *end > *start) - origin = old.origin - *start; - } - auto const layer{covered.empty() ? alignment_store::layer_of(*Chain.tracks.front()) : records[*covered.begin()].layer}; - for (auto index{covered.rbegin()}; index != covered.rend(); ++index) - alignment_store::forget(*index); - alignment_store::store(design, result, origin, layer, "drawn", fit); - return true; -} - -bool editor_mode::route_from_line(TTrack &Track) -{ - auto &route{m_route}; - editor_track::chain run; - if (false == editor_track::find_run(Track, kLineReach, run, route.error)) - return false; - route.from = run.tracks.front(); - route.to = run.tracks.back(); - route_reset(); - if (route.chain.tracks.empty()) - return false; - auto &design{route.design}; - auto const found{editor_track::recognize_line(route.chain, m_straights.tolerance, design.norms.gauge, design.shape)}; - route.designed = 0; - if (false == found.empty()) - { - design.vertices.clear(); - for (auto const &entry : found) - { - alignment::vertex vertex; - if (entry.kink) - { - vertex.radius = std::max(10.0, std::round(alignment::recommend(design.speed, 1000.0, design.norms).radius_min / 10.0) * 10.0); - vertex.transition_in = 0.0; - vertex.transition_out = 0.0; - vertex.cant = 0.0; - } - else - { - vertex = route_vertex_of(entry.shape); - if (entry.shape.designed.has_value()) - ++route.designed; - } - vertex.position = entry.position; - design.vertices.push_back(vertex); - } - if (design.vertices.size() >= 2) - { - design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); - design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); - } - route_update(); - } - route.status = format(STR_C("Line of %zu paths, %.0f m, %zu vertices"), route.chain.tracks.size(), route.chain.length, design.vertices.size()); - return true; -} - -void editor_mode::route_apply() -{ - auto &route{m_route}; - route.status.clear(); - if (false == route.result.valid || route.chain.tracks.empty()) - return; - editor_track::chain chain; - if (false == editor_track::find_chain(route.from, route.to, chain, route.error, true)) - return; - if (glm::distance(chain.start, route.chain.start) > 1e-3 || glm::distance(chain.end, route.chain.end) > 1e-3) - { - route.error = STR_C("The fragment changed since it was selected, select it again"); - return; - } - auto involved{chain.tracks}; - for (auto const &passage : chain.switches) - involved.push_back(passage.track); - if (false == tracks_editable(involved, route.error)) - return; - - std::vector> states; - std::vector created; - // takes back what was done before the paths were re-laid - auto const undo = [&]() { - for (auto *track : created) - editor_track::retire(*track); - for (auto entry = states.rbegin(); entry != states.rend(); ++entry) - { - editor_track::apply(*entry->first, entry->second); - editor_track::commit({entry->first}); - } - }; - auto *from{route.from}; - auto *to{route.to}; - auto const startextension{route.result.start_extension}; - auto const endextension{route.result.end_extension}; - if (startextension > 0.0 || endextension > 0.0) - { - auto const before{chain}; - if (startextension > 0.0) - from = editor_track::take_straight(editor_track::straight_beyond(before, false, m_straights.tolerance), from, before.start, route.result.start, states, created); - if (endextension > 0.0) - to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); - std::string error; - auto const found{editor_track::find_chain(from, to, chain, error, true)}; - auto const startoff{found ? plan_distance(chain.start, route.result.start) : 0.0}; - auto const endoff{found ? plan_distance(chain.end, route.result.end) : 0.0}; - if (false == found || startoff > 5.0 || endoff > 5.0) - { - auto const startrun{editor_track::straight_beyond(before, false, m_straights.tolerance)}; - auto const endrun{editor_track::straight_beyond(before, true, m_straights.tolerance)}; - WriteLog(format("editor: cutting the straights failed: found %d, start %.2f m off (extension %.2f, run %zu paths %.2f m), end %.2f m off (extension %.2f, run %zu paths %.2f m), chain %zu paths, from %s, to %s, error '%s'", found ? 1 : 0, startoff, - startextension, startrun.tracks.size(), startrun.length, endoff, endextension, endrun.tracks.size(), endrun.length, chain.tracks.size(), from != nullptr ? from->name().c_str() : "-", to != nullptr ? to->name().c_str() : "-", error.c_str())); - undo(); - route.error = error.empty() ? format(STR_C("The adjoining straights can't be cut at the ends of the curve: the start misses by %.1f m, the end by %.1f m"), startoff, endoff) : error; - return; - } - if (false == tracks_editable(chain.tracks, route.error)) - { - undo(); - return; - } - } - for (auto *track : chain.tracks) - if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) - states.emplace_back(track, editor_track::capture(*track)); - std::vector relaid; - if (chain.switches.empty()) - { - auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; - editor_track::keep_heights(chain, pieces); - relaid = editor_track::relay(chain, pieces, &to); - } - else if (false == route_relay_through_switches(chain, states, relaid, to)) - { - undo(); - return; - } - // the straights were cut along their own direction, the curve ends where the design says: the ends are brought together - auto const added{relaid.size()}; - created.insert(created.end(), relaid.begin(), relaid.end()); - push_track_snapshot(std::move(states), std::move(created)); - - route.from = from; - route.to = to; - editor_track::find_chain(route.from, route.to, route.chain, route.error, true); - if (startextension > 0.0 || endextension > 0.0) - { - route_bind_ends(); - auto &vertices{route.design.vertices}; - if (vertices.size() >= 2) - { - vertices.front().offset += startextension; - vertices.back().offset += endextension; - } - route_update(); - } - route_store(); - records_to_history(); - route.status = STR_C("Re-laid ") + std::to_string(chain.tracks.size()) + STR_C(" paths") + (added > 0 ? STR_C(", added ") + std::to_string(added) : std::string{}); - if (startextension > 0.0 || endextension > 0.0) - route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); - WriteLog("Editor: route design - " + route.status, logtype::generic); -} - -// the switches of the chain are moved as a whole onto the designed line, the regular paths between them re-laid -bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created, TTrack *&Last) -{ - auto &route{m_route}; - auto const &result{route.result}; - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; - auto const plan = [](glm::dvec3 const &Vector) { return glm::normalize(plan_of(Vector)); }; - struct placement - { - TTrack *track; - double from, length; // chainage along the new line - glm::dvec3 entry; // of the main track, where the chain comes in - double angle; - }; - // chainages along the old line, the switches keep their distance from the nearer end of the chain - double before{0.0}; - std::vector starts; - std::size_t next{0}; - for (std::size_t i = 0; i <= Chain.tracks.size(); ++i) - { - for (; next < Chain.switches.size() && Chain.switches[next].before == i; ++next) - { - starts.push_back(before); - before += bezier{Chain.switches[next].track->m_paths.front()}.plan_length(); - } - if (i < Chain.tracks.size()) - before += bezier{Chain.tracks[i]->m_paths.front()}.plan_length(); - } - auto const oldlength{before}; - std::vector placements; - std::vector breaks; - for (std::size_t k = 0; k < Chain.switches.size(); ++k) - { - auto const &passage{Chain.switches[k]}; - auto const &main{passage.track->m_paths.front()}; - auto const length{bezier{main}.plan_length()}; - auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])}; - if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01)) - { - route.error = STR_C("The switch ") + name(passage.track) + STR_C(" doesn't fit on the designed line"); - return false; - } - auto const entry{main.points[passage.forward ? segment_data::point::start : segment_data::point::end]}; - auto const exit{main.points[passage.forward ? segment_data::point::end : segment_data::point::start]}; - bezier const curve{main}; - auto const direction{passage.forward ? plan(curve.first(0.0)) : -plan(curve.first(1.0))}; - auto const first{alignment::evaluate(result, from)}; - auto const last{alignment::evaluate(result, from + length)}; - auto const angle{signed_angle(direction, plan(first.direction))}; - // where the other end of the main track goes, set against the line - auto const moved{plan_of(first.position) + turned(plan_of(exit - entry), angle)}; - auto const departure{glm::distance(moved, plan_of(last.position))}; - if (departure > 0.02) - { - route.error = format(STR_C("The switch %s would leave the designed line by %.2f m at its other end: it lies in a curve of the design. " - "Move the vertices so that the switch falls on a straight"), - name(passage.track).c_str(), departure); - return false; - } - placements.push_back({passage.track, from, length, entry, angle}); - breaks.push_back(from); - breaks.push_back(from + length); - } - // the regular parts between the switches, one interval each, the switches taking the ones between - auto const parts{editor_track::chain_parts(Chain)}; - std::vector counts; - std::vector partof; - std::size_t part{0}; - for (std::size_t k = 0; k <= Chain.switches.size(); ++k) - { - auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before}; - auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before}; - counts.push_back(last - first); - partof.push_back(last > first ? static_cast(part++) : -1); - if (k < Chain.switches.size()) - counts.push_back(1); - } - if (part != parts.size()) - { - route.error = STR_C("The paths between the switches can't be read as a chain"); - return false; - } - std::vector intervals; - auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)}; - std::vector> groups(parts.size()); - for (std::size_t i = 0; i < pieces.size(); ++i) - if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0) - groups[partof[intervals[i] / 2]].push_back(pieces[i]); - for (std::size_t i = 0; i < parts.size(); ++i) - if (groups[i].size() < parts[i].tracks.size()) - { - route.error = STR_C("The designed line between the switches is too short for the paths there"); - return false; - } - - // all checked, the changes go in - auto const remember = [&](TTrack *Track) { - if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; })) - States.emplace_back(Track, editor_track::capture(*Track)); - }; - for (auto const &place : placements) - { - auto joints{editor_track::joints(*place.track)}; - joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return std::find(Chain.tracks.begin(), Chain.tracks.end(), Joint.other) != Chain.tracks.end(); }), joints.end()); - remember(place.track); - std::vector changed{place.track}; - for (auto const &joint : joints) - { - remember(joint.other); - if (std::find(changed.begin(), changed.end(), joint.other) == changed.end()) - changed.push_back(joint.other); - } - // the heights stay, the paths between the switches keep theirs too - auto target{alignment::evaluate(result, place.from).position}; - target.y = place.entry.y; - editor_track::place(*place.track, place.entry, target, place.angle); - editor_track::follow(*place.track, joints); - editor_track::commit(changed); - } - for (std::size_t i = 0; i < parts.size(); ++i) - { - auto group{groups[i]}; - editor_track::keep_heights(parts[i], group); - TTrack *last{nullptr}; - auto const added{editor_track::relay(parts[i], group, &last)}; - if (last != nullptr && parts[i].tracks.back() == Chain.tracks.back()) - Last = last; - Created.insert(Created.end(), added.begin(), added.end()); - } - return true; -} - -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; - - auto const height{Vertex < static_cast(result.vertex_elevations.size()) ? result.vertex_elevations[Vertex] : design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0)}; - return {position.x, height, position.y}; -} - -bool editor_mode::route_hit(int &Vertex, int &Grip) const -{ - if (false == route_active()) - return false; - screen_projection const projection; - float best = kHandleRadius * kHandleRadius; - int hit{-1}; - for (int i = 0; i < static_cast(m_route.design.vertices.size()); ++i) - { - if (auto const distance{projection.mouse_distance2(route_vertex_position(i))}; distance < best) - { - best = distance; - hit = i; - } - } - int griphit{-1}; - for (int i = 0; i < static_cast(m_route.result.vertex_chainages.size()) && m_route.result.length > 0.0; ++i) - { - if (auto const distance{projection.mouse_distance2(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position)}; distance < best) - { - best = distance; - griphit = i; - hit = -1; - } - } - Vertex = hit; - Grip = griphit; - return hit >= 0 || griphit >= 0; -} - -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(ImGui::GetMainViewport()); - auto const &start{result.valid ? result.start : design.start}; - auto const &end{result.valid ? result.end : design.end}; - - std::vector polygon{start}; - for (int i = 0; i < static_cast(design.vertices.size()); ++i) - polygon.push_back(route_vertex_position(i)); - polygon.push_back(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); - - 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 ? overlay_color::grip : 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, overlay_color::highlight); - } - } - - for (auto const atend : {false, true}) - { - auto const &point{atend ? end : 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, overlay_color::marked); - if (i == route.vertex) - drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); - if (i < static_cast(result.vertex_chainages.size()) && result.length > 0.0 && projection.project(alignment::evaluate(result, result.vertex_chainages[i]).position, screen)) - { - drawlist->AddCircleFilled(screen, 6.0f, overlay_color::grip, 4); - if (i == route.grip) - drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); - auto const *report{i < static_cast(result.curves.size()) ? &result.curves[i] : nullptr}; - auto radius{format("R %.0f", report != nullptr ? report->radius : design.vertices[i].radius)}; - if (report != nullptr && report->radii.size() >= 2) - { - radius = "R"; - for (std::size_t k = 0; k < report->radii.size(); ++k) - radius += format(k == 0 ? " %.0f" : " / %.0f", report->radii[k]); - } - drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), overlay_color::grip, radius.c_str()); - } - drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); - } -} - -void editor_mode::render_route_gizmo() -{ - auto &route{m_route}; - auto const count{static_cast(route.design.vertices.size())}; - bool const grip{route.grip >= 0 && route.grip < count && route.grip < static_cast(route.result.vertex_chainages.size())}; - if (false == grip && (route.vertex < 0 || route.vertex >= count)) - { - m_route_dragging = false; - return; - } - - auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)}; - glm::dvec3 moved{position}; - if (false == handle_drag_step(moved)) - { - if (m_route_dragging) - { - m_route_dragging = false; - route_apply(); - } - return; - } - m_route_dragging = true; - if (grip) - { - auto &vertex{route.design.vertices[route.grip]}; - auto const corner{route_vertex_position(route.grip)}; - auto const distance{plan_distance(moved, corner)}; - for (auto const &curve : route.result.curves) - { - if (curve.vertex != route.grip || vertex.reverse_turn) - continue; - auto const secant{1.0 / std::cos(curve.deflection * 0.5) - 1.0}; - if (secant > 1e-6) - vertex.radius = std::max(10.0, std::round(distance / secant)); - } - route_update(); - return; - } - auto &vertex{route.design.vertices[route.vertex]}; - glm::dvec2 const point{moved.x, moved.z}; - if (count >= 2) - { - if (route.vertex == 0) - vertex.offset = std::max(0.1, glm::dot(point - plan_of(route.design.start), glm::normalize(route.design.start_direction))); - else if (route.vertex == count - 1) - vertex.offset = std::max(0.1, glm::dot(plan_of(route.design.end) - point, glm::normalize(route.design.end_direction))); - else - vertex.position = point; - } - route_update(); -} - -void editor_mode::render_route_ui() -{ - auto &route{m_route}; - render_norms_choice(); - ImGui::Separator(); - if (false == route.error.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); - if (route.chain.tracks.empty()) - return; - - ImGui::TextDisabled(STR_C("%zu paths, %.2f m"), route.chain.tracks.size(), route.chain.length); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Reset design"))) - { - auto *track{route.chain.tracks[route.chain.tracks.size() / 2]}; - route_load(*track); - return; - } - item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); - render_route_stored(); - auto changed{render_route_vertices()}; - if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) - changed |= render_route_parameters(); - if (changed) - route_update(); - - ImGui::Separator(); - render_route_result(); -} - -bool editor_mode::render_route_parameters() -{ - auto &design{m_route.design}; - bool changed{false}; - ImGui::PushItemWidth(120.0f); - changed |= ImGui::InputDouble(STR_C("Design speed (km/h)"), &design.speed, 0.0, 0.0, "%.0f"); - item_tooltip("Speed the cant, the transitions and the recommended radius are designed for"); - design.speed = std::max(1.0, design.speed); - int shape = static_cast(design.shape); - char const *shapes[] = {STR_C("Cubic parabola"), STR_C("Clothoid")}; - if (ImGui::Combo(STR_C("Transition curve"), &shape, shapes, IM_ARRAYSIZE(shapes))) - { - design.shape = static_cast(shape); - changed = true; - } - item_tooltip("Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length"); - ImGui::PopItemWidth(); - - if (ImGui::TreeNode(STR_C("Limits"))) - { - auto &norms{design.norms}; - ImGui::PushItemWidth(100.0f); - changed |= ImGui::InputDouble(STR_C("Rail axes distance (mm)"), &norms.gauge, 0.0, 0.0, "%.0f"); - changed |= ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &norms.unbalanced, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble(STR_C("Maximum cant (mm)"), &norms.cant_max, 0.0, 0.0, "%.0f"); - changed |= ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &norms.cant_rate, 0.0, 0.0, "%.0f"); - changed |= ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &norms.ramp_factor, 0.0, 0.0, "%.1f"); - changed |= ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &norms.jerk, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble(STR_C("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(); - } - return changed; -} - -bool editor_mode::render_route_vertices() -{ - bool changed{false}; - auto const count{static_cast(m_route.design.vertices.size())}; - for (int i = 0; i < count; ++i) - { - ImGui::PushID(i); - changed |= render_route_vertex(i); - ImGui::PopID(); - } - return changed; -} - -bool editor_mode::render_route_vertex(int const Index) -{ - auto &route{m_route}; - auto &design{route.design}; - auto &vertex{design.vertices[Index]}; - auto const count{static_cast(design.vertices.size())}; - bool changed{false}; - if (count > 1) - ImGui::TextDisabled(STR_C("Curve %d of the S-curve"), Index + 1); - ImGui::PushItemWidth(110.0f); - changed |= ImGui::DragScalar(vertex.compound ? STR_C("Radius R1 (m)") : STR_C("Radius R (m)"), ImGuiDataType_Double, &vertex.radius, 5.0f, nullptr, nullptr, "%.0f"); - if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound)) - { - changed = true; - if (vertex.compound && vertex.arcs.empty()) - vertex.arcs.push_back({std::max(10.0, std::round(vertex.radius * 0.6 / 10.0) * 10.0), 0.0, 1.0}); - } - item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions"); - if (vertex.compound) - { - auto total{std::max(0.0, vertex.share)}; - for (auto const &arc : vertex.arcs) - total += std::max(0.0, arc.share); - auto const percent = [&](double const Share) { return total > 0.0 ? 100.0 * std::max(0.0, Share) / total : 0.0; }; - changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), 1).c_str(), ImGuiDataType_Double, &vertex.share, 0.01f, nullptr, nullptr, "%.2f"); - ImGui::SameLine(); - ImGui::TextDisabled("%.0f%%", percent(vertex.share)); - int removed{-1}; - for (int k = 0; k < static_cast(vertex.arcs.size()); ++k) - { - auto &arc{vertex.arcs[k]}; - ImGui::PushID(k); - ImGui::Separator(); - changed |= ImGui::DragScalar(format(STR_C("Radius R%d (m)"), k + 2).c_str(), ImGuiDataType_Double, &arc.radius, 5.0f, nullptr, nullptr, "%.0f"); - ImGui::SameLine(); - if (ImGui::SmallButton("x")) - removed = k; - item_tooltip("Takes this radius out of the curve"); - changed |= ImGui::DragScalar(format(STR_C("Transition R%d-R%d (m)"), k + 1, k + 2).c_str(), ImGuiDataType_Double, &arc.transition, 1.0f, nullptr, nullptr, "%.0f"); - changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f"); - ImGui::SameLine(); - ImGui::TextDisabled("%.0f%%", percent(arc.share)); - arc.radius = std::max(1.0, arc.radius); - arc.transition = std::max(0.0, arc.transition); - arc.share = std::max(0.0, arc.share); - ImGui::PopID(); - } - vertex.share = std::max(0.0, vertex.share); - if (removed >= 0) - { - vertex.arcs.erase(vertex.arcs.begin() + removed); - if (vertex.arcs.empty()) - vertex.compound = false; - changed = true; - } - if (ImGui::SmallButton(STR_C("Add a radius"))) - { - auto const last{vertex.arcs.empty() ? vertex.radius : vertex.arcs.back().radius}; - vertex.arcs.push_back({std::max(10.0, std::round(last * 0.8 / 10.0) * 10.0), 0.0, 1.0}); - changed = true; - } - item_tooltip("Another arc after the last one, the angle of the curve is shared out by the shares"); - ImGui::Separator(); - } - changed |= ImGui::DragScalar(STR_C("Transition in (m)"), ImGuiDataType_Double, &vertex.transition_in, 1.0f, nullptr, nullptr, "%.0f"); - changed |= ImGui::DragScalar(STR_C("Transition out (m)"), ImGuiDataType_Double, &vertex.transition_out, 1.0f, nullptr, nullptr, "%.0f"); - changed |= ImGui::DragScalar(STR_C("Cant (mm)"), ImGuiDataType_Double, &vertex.cant, 1.0f, nullptr, nullptr, "%.0f"); - item_tooltip("Height of the outer rail above the inner one"); - 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); - if (false == m_norms_free) - { - if (ImGui::SmallButton(STR_C("Apply recommended for V"))) - { - route_recommend(vertex); - changed = true; - } - item_tooltip("Cant and transitions recommended for the design speed and the radius"); - auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; - ImGui::TextDisabled(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); - } - if (ImGui::TreeNode(STR_C("Details"))) - { - for (auto const &curve : route.result.curves) - { - if (curve.vertex != Index) - continue; - ImGui::Text(STR_C("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(STR_C("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); - } - changed |= ImGui::Checkbox(STR_C("Turn the longer way (over 180 deg)"), &vertex.reverse_turn); - item_tooltip("For the loops: the curve turns by more than a half circle"); - ImGui::TreePop(); - } - return changed; -} - -void editor_mode::render_route_result() -{ - auto const &route{m_route}; - auto const &design{route.design}; - auto const &result{route.result}; - if (result.length > 0.0) - { - auto const pieces{alignment::minimum_pieces(result, design)}; - ImGui::Text(STR_C("Length %.2f m (now %.2f m), %zu elements"), result.length, route.chain.length, result.elements.size()); - ImGui::TextDisabled(STR_C("Paths: %zu in the fragment, %zu needed%s"), route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? STR_C(" (copies of the neighbours will be added)") : ""); - if (result.start_extension > 0.0 || result.end_extension > 0.0) - ImGui::Text(STR_C("Takes over %.2f m of the straight at the start, %.2f m at the end"), result.start_extension, result.end_extension); - } - 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(STR_C("Elements"))) - { - ImGui::TextDisabled(STR_C("Straight available beyond the ends: %.2f / %.2f m"), design.start_reserve, design.end_reserve); - char const *kinds[] = {"straight", "transition", "curve", "transition", "spiral"}; - for (auto const &e : result.elements) - ImGui::Text("km %s %-10s L %8.2f m%s", profile::format_chainage(m_route.origin + e.chainage).c_str(), 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) - { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) - route_apply(); - ImGui::PopStyleColor(2); - } - else - { - ImGui::TextDisabled(STR_C("Correct the errors to apply the design")); - } - if (false == route.status.empty()) - ImGui::TextUnformatted(route.status.c_str()); -} - -editor_track::straight const &editor_mode::current_straight() -{ - auto *track{selected_track()}; - if (track != m_straights.current_for) - { - m_straights.current_for = track; - m_straights.handle = -1; - m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{}; - find_neighbour_straights(); - } - return m_straights.current; -} - -void editor_mode::draw_straights_overlay() const -{ - screen_projection const projection; - ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); - glm::dvec3 const camera{Global.pCamera.Pos}; - auto const nearby = [&](editor_track::straight const &Line) { - auto const offset{camera - Line.start}; - auto const along{std::clamp(glm::dot(plan_of(offset), Line.direction), 0.0, Line.length)}; - auto const closest{Line.start + glm::dvec3{Line.direction.x, 0.0, Line.direction.y} * along}; - return plan_distance(closest, camera) < 3000.0; - }; - auto const draw = [&](editor_track::straight const &Line, ImU32 const Color, float const Thickness) { - if (Line.tracks.empty()) - return; - projection.line(drawlist, Line.start, Line.end, Color, Thickness); - for (auto const &point : {Line.start, Line.end}) - { - ImVec2 screen; - if (projection.project(point, screen)) - drawlist->AddCircleFilled(screen, Thickness + 2.0f, Color); - } - }; - for (int i = 0; i < static_cast(m_straights.found.size()); ++i) - { - auto const &line{m_straights.found[i]}; - if (nearby(line)) - draw(line, i == m_straights.listed ? overlay_color::selected : IM_COL32(255, 255, 255, 110), i == m_straights.listed ? 4.0f : 2.0f); - } - for (auto const &member : m_straights.set) - draw(member, IM_COL32(255, 150, 30, 255), 4.0f); - draw(m_straights.current, overlay_color::selected, 4.0f); - if (m_straights.tool_placed) - { - auto const &state{m_straights}; - auto const &line{state.tool_line}; - ImVec2 screen; - if (projection.project(state.tool_point, screen)) - drawlist->AddCircleFilled(screen, 7.0f, overlay_color::invalid); - if (projection.project(state.tool_handle, screen)) - drawlist->AddCircleFilled(screen, 7.0f, overlay_color::marked); - if (state.tool == 1) - { - auto const direction{plan_of(state.tool_handle - state.tool_point)}; - if (glm::length(direction) > 1e-3) - { - auto const unit{glm::normalize(direction)}; - auto const rest{std::max(0.0, line.length - state.tool_at)}; - projection.line(drawlist, state.tool_point, state.tool_point + glm::dvec3{unit.x, line.grade, unit.y} * rest, overlay_color::marked, 3.0f); - } - } - else - { - glm::dvec2 const normal{-line.direction.y, line.direction.x}; - auto const offset{plan_of(state.tool_handle - state.tool_point)}; - auto const shift{glm::dot(offset, normal)}; - auto const length{glm::dot(offset, line.direction)}; - auto const shifted{glm::dvec3{normal.x, 0.0, normal.y} * shift}; - auto const along{glm::dvec3{line.direction.x, 0.0, line.direction.y}}; - projection.line(drawlist, state.tool_point, state.tool_point + along * length + shifted, IM_COL32(255, 210, 60, 160), 2.0f); - projection.line(drawlist, state.tool_point + along * length + shifted, line.end + shifted, overlay_color::marked, 3.0f); - } - } - if (m_straights.dragging && m_straights.fitting) - { - auto const &result{m_route.result}; - if (result.length > 0.0 && result.profile.size() >= 2) - { - int const steps{std::clamp(static_cast(result.length / 2.0), 2, 2000)}; - auto previous{alignment::evaluate(result, 0.0).position}; - for (int i = 1; i <= steps; ++i) - { - auto const next{alignment::evaluate(result, result.length * i / steps).position}; - projection.line(drawlist, previous, next, result.valid ? overlay_color::marked : overlay_color::invalid, 3.0f); - previous = next; - } - } - } - else if (m_straights.dragging) - { - for (auto const &member : m_straights.drag_lines) - { - for (auto const *track : member.tracks) - { - auto const &path{track->m_paths.front()}; - projection.line(drawlist, path.points[segment_data::point::start], path.points[segment_data::point::end], overlay_color::marked, 3.0f); - } - } - } - else if (false == m_straights.current.tracks.empty()) - { - auto const &line{m_straights.current}; - glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; - for (int i = 0; i < 3; ++i) - { - ImVec2 screen; - if (false == projection.project(handles[i], screen)) - continue; - if (i < 2) - drawlist->AddRectFilled(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), overlay_color::selected); - else - drawlist->AddCircleFilled(screen, 6.0f, overlay_color::selected, 4); - if (i == m_straights.handle) - drawlist->AddCircle(screen, 12.0f, overlay_color::highlight, 16, 2.5f); - } - } - auto line{m_straights.dragging ? m_straights.drag_line : m_straights.current}; - if (line.tracks.empty()) - return; - glm::dvec3 const offset{m_straights.dragging ? m_straights.preview_start - m_straights.drag_line.start : glm::dvec3{}}; - if (m_straights.dragging) - { - line.start = m_straights.preview_start; - line.end = m_straights.preview_end; - auto const planar{plan_of(line.end - line.start)}; - auto const length{glm::length(planar)}; - if (length > 1e-6) - { - line.direction = planar / length; - line.length = length; - } - } - auto const offer{straight_parallel_offer(m_straights.dragging ? m_straights.drag_line : m_straights.current, offset)}; - glm::dvec2 const normal{-line.direction.y, line.direction.x}; - for (auto const &other : m_straights.neighbours) - { - auto const across = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - line.start), normal); }; - auto const a0{line.along(other.start)}, a1{line.along(other.end)}; - auto const from{std::max(0.0, std::min(a0, a1))}; - auto const to{std::min(line.length, std::max(a0, a1))}; - if (to <= from) - continue; - auto const middle{(from + to) * 0.5}; - auto const fraction{std::abs(a1 - a0) > 1e-6 ? (middle - a0) / (a1 - a0) : 0.0}; - auto const lateral{across(other.start) + (across(other.end) - across(other.start)) * fraction}; - glm::dvec3 const foot{line.start.x + line.direction.x * middle, line.start.y + line.grade * middle, line.start.z + line.direction.y * middle}; - glm::dvec3 const target{foot.x + normal.x * lateral, foot.y, foot.z + normal.y * lateral}; - bool const highlighted{offer.nearby && offer.neighbour == &other}; - auto const tick{highlighted ? (offer.snaps ? IM_COL32(80, 230, 255, 255) : IM_COL32(255, 210, 60, 255)) : IM_COL32(255, 230, 120, 230)}; - if (highlighted) - projection.line(drawlist, other.start, other.end, tick, 3.0f); - projection.line(drawlist, foot, target, tick, highlighted ? 3.0f : 1.5f); - ImVec2 screen; - if (projection.project((foot + target) * 0.5, screen)) - { - auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))}; - auto const text{highlighted ? (offer.snaps ? format(STR_C("parallel %.2f m"), offer.spacing) : format("%.2f → %.2f m %.1f°", std::abs(lateral), offer.spacing, offer.angle)) : - (angle > 0.05 ? format("%.2f m %.1f°", std::abs(lateral), angle) : format("%.2f m", std::abs(lateral)))}; - auto const *label{text.c_str()}; - auto const size{ImGui::CalcTextSize(label)}; - drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 4.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 190), 3.0f); - drawlist->AddText(screen, highlighted ? tick : IM_COL32(255, 230, 120, 255), label); - } - } - if (offer.nearby && false == offer.snaps && std::abs(offer.correction) > 0.02) - { - auto const shift{glm::dvec3{normal.x, 0.0, normal.y} * offer.correction}; - projection.line(drawlist, line.start + shift, line.end + shift, IM_COL32(80, 230, 255, 210), 3.5f); - } -} - -void editor_mode::find_neighbour_straights() -{ - auto &state{m_straights}; - state.neighbours.clear(); - auto const &line{state.current}; - if (line.tracks.empty()) - return; - glm::dvec2 const normal{-line.direction.y, line.direction.x}; - auto const middle{(line.start + line.end) * 0.5}; - std::vector visited(line.tracks.begin(), line.tracks.end()); - auto const sections{simulation::Region->sections(middle, static_cast(line.length * 0.5 + 60.0))}; - for (auto *section : sections) - { - for (auto const &cell : section->m_cells) - { - for (auto *track : cell.m_paths) - { - if (std::find(visited.begin(), visited.end(), track) != visited.end() || false == editor_track::is_straight(*track, state.tolerance)) - continue; - auto other{editor_track::find_straight(*track, state.tolerance)}; - visited.insert(visited.end(), other.tracks.begin(), other.tracks.end()); - if (other.tracks.empty() || std::abs(line.direction.x * other.direction.y - line.direction.y * other.direction.x) > std::sin(glm::radians(kParallelFindAngle))) - continue; - auto const offset{glm::dvec2{(other.start + other.end).x * 0.5 - line.start.x, (other.start + other.end).z * 0.5 - line.start.z}}; - if (std::abs(glm::dot(offset, normal)) > 60.0) - continue; - auto const a0{line.along(other.start)}; - auto const a1{line.along(other.end)}; - if (std::max(a0, a1) <= 0.0 || std::min(a0, a1) >= line.length) - continue; - state.neighbours.push_back(std::move(other)); - } - } - } -} - -editor_mode::parallel_offer editor_mode::straight_parallel_offer(editor_track::straight const &Line, glm::dvec3 const &Offset) const -{ - parallel_offer offer; - glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; - auto const middle{(Line.start + Line.end) * 0.5 + Offset}; - double best{kParallelHintWindow}; - for (auto const &other : m_straights.neighbours) - { - auto const sine{std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x)}; - if (sine > std::sin(glm::radians(kParallelFindAngle))) - continue; - auto const distance{glm::dot(plan_of(other.start - middle), normal)}; - auto const angle{glm::degrees(std::abs(std::asin(std::clamp(sine, 0.0, 1.0))))}; - for (auto const spacing : m_straights.spacings) - { - auto const error{std::abs(std::abs(distance) - spacing)}; - if (error < best) - { - best = error; - offer.nearby = true; - offer.snaps = error < kParallelSnapWindow && angle <= kParallelSnapAngle; - offer.spacing = spacing; - offer.distance = std::abs(distance); - offer.angle = angle; - offer.correction = distance > 0.0 ? std::abs(distance) - spacing : spacing - std::abs(distance); - offer.neighbour = &other; - } - } - } - return offer; -} - -glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const -{ - auto const offer{straight_parallel_offer(Line, Offset)}; - if (false == offer.snaps) - return Offset; - glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; - return Offset + glm::dvec3{normal.x, 0.0, normal.y} * offer.correction; -} - -glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const -{ - glm::dvec2 const offset{Moved.x - Pivot.x, Moved.z - Pivot.z}; - auto const length{glm::length(offset)}; - if (length < 1e-3) - return Moved; - auto const direction{offset / length}; - for (auto const &other : m_straights.neighbours) - { - auto const sine{direction.x * other.direction.y - direction.y * other.direction.x}; - if (std::abs(sine) > std::sin(glm::radians(kParallelSnapAngle))) - continue; - auto const aligned{other.direction * (glm::dot(direction, other.direction) < 0.0 ? -1.0 : 1.0)}; - return {Pivot.x + aligned.x * length, Moved.y, Pivot.z + aligned.y * length}; - } - return Moved; -} - -void editor_mode::render_straights_ui() -{ - auto &state{m_straights}; - ImGui::TextDisabled(STR_C("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to")); - - ImGui::PushItemWidth(100.0f); - double angle{glm::degrees(state.tolerance.angle)}; - if (ImGui::InputDouble(STR_C("Angle tolerance (deg)"), &angle, 0.0, 0.0, "%.4f")) - state.tolerance.angle = glm::radians(std::max(0.0, angle)); - item_tooltip("Largest change of the direction between the paths of one straight"); - ImGui::InputDouble(STR_C("Offset tolerance (m)"), &state.tolerance.offset, 0.0, 0.0, "%.3f"); - item_tooltip("Largest distance of the paths from the common axis of the straight"); - ImGui::InputDouble(STR_C("Treat as straight above R (m)"), &state.tolerance.radius, 0.0, 0.0, "%.0f"); - item_tooltip("Curves of a larger radius count as straight"); - ImGui::InputDouble(STR_C("Minimum length (m)"), &state.minimum_length, 0.0, 0.0, "%.0f"); - item_tooltip("Shorter straights aren't listed"); - ImGui::PopItemWidth(); - if (ImGui::Button(STR_C("Recognize straights in the scenery"))) - { - state.found = editor_track::find_straights(state.minimum_length, state.tolerance); - state.listed = -1; - state.current_for = nullptr; - } - if (false == state.found.empty()) - { - ImGui::Text(STR_C("%zu straights"), state.found.size()); - ImGui::SameLine(); - ImGui::PushItemWidth(150.0f); - ImGui::InputTextWithHint("##straightfilter", STR_C("path name"), state.filter, sizeof(state.filter)); - ImGui::PopItemWidth(); - ImGui::BeginChild("##straights", ImVec2(0.0f, 160.0f), true); - for (int i = 0; i < static_cast(state.found.size()); ++i) - { - auto const &line{state.found[i]}; - if (state.filter[0] != '\0' && std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return Track->name().find(state.filter) != std::string::npos; })) - continue; - auto const label{std::to_string(i + 1) + ". " + describe(line) + "##straight" + std::to_string(i)}; - if (ImGui::Selectable(label.c_str(), state.listed == i)) - { - state.listed = i; - m_node = line.tracks[line.tracks.size() / 2]; - ui()->set_node(m_node); - start_focus(m_node); - } - } - ImGui::EndChild(); - } - -} - -void editor_mode::render_straight_ui() -{ - auto &state{m_straights}; - auto const &line{current_straight()}; - if (line.tracks.empty()) - { - if (false == state.status.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); - return; - } - ImGui::TextUnformatted(describe(line).c_str()); - if (false == m_straights.neighbours.empty()) - ImGui::TextDisabled("%s", STR_C("Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m")); - - auto const toolbutton = [&](char const *Label, int const Tool, char const *Tooltip) { - bool const active{state.tool == Tool}; - if (active) - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - if (ImGui::Button(Label)) - { - state.tool = active ? 0 : Tool; - state.tool_placed = false; - state.handle = -1; - } - if (active) - ImGui::PopStyleColor(); - item_tooltip(Tooltip); - }; - toolbutton(STR_C("Break"), 1, STR_C("Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)")); - ImGui::SameLine(); - toolbutton(STR_C("S-curve"), 2, STR_C("Shifts a piece of the straight sideways around an obstacle and back: four clicks on the straight (Shift+click in the view does the same)")); - ImGui::SameLine(); - bool const member{in_straight_set(line)}; - if (ImGui::Button(member ? STR_C("Leave the group") : STR_C("Move with others"))) - { - if (member) - state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return std::find(Member.tracks.begin(), Member.tracks.end(), Track) != Member.tracks.end(); }); }), state.set.end()); - else - state.set.push_back(line); - } - item_tooltip("Straights of a group move together and stay parallel; Alt+click in the view adds another one"); - if (state.tool != 0) - { - ImGui::PushItemWidth(100.0f); - ImGui::InputDouble(STR_C("Curve R (m, 0: automatic)"), &state.curve_radius, 0.0, 0.0, "%.0f"); - item_tooltip("Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line"); - ImGui::Checkbox(STR_C("Transitions from the line speed"), &state.auto_transitions); - item_tooltip("Lengths of the transition curves from the speed of the paths and the limits of the curve design"); - if (false == state.auto_transitions) - { - ImGui::SameLine(); - ImGui::InputDouble(STR_C("Transition (m)"), &state.transition, 0.0, 0.0, "%.1f"); - } - ImGui::PopItemWidth(); - } - if (false == state.status.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); - if (false == state.set.empty()) - { - ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.15f, 1.0f), STR_C("Group: %zu straights move together"), state.set.size()); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Clear"))) - state.set.clear(); - } - - if (false == ImGui::IsAnyItemActive()) - { - state.edit_start = line.start; - state.edit_end = line.end; - state.edit_length = line.length; - state.edit_azimuth = line.azimuth; - } - if (ImGui::CollapsingHeader(STR_C("Exact values"))) - { - ImGui::PushItemWidth(-110.0f); - ImGui::InputScalarN(STR_C("Start (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_start), 3, nullptr, nullptr, "%.3f"); - ImGui::InputScalarN(STR_C("End (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_end), 3, nullptr, nullptr, "%.3f"); - ImGui::PopItemWidth(); - if (ImGui::Button(STR_C("Apply ends"))) - straight_apply(line, state.edit_start, state.edit_end); - ImGui::PushItemWidth(120.0f); - ImGui::InputDouble(STR_C("Length (m)"), &state.edit_length, 0.0, 0.0, "%.3f"); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Set from the start##length"))) - { - glm::dvec3 const direction{line.direction.x, line.grade, line.direction.y}; - straight_apply(line, line.start, line.start + direction * std::max(0.1, state.edit_length)); - } - ImGui::InputDouble(STR_C("Azimuth (deg)"), &state.edit_azimuth, 0.0, 0.0, "%.5f"); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Turn around the start"))) - { - auto const azimuth{glm::radians(state.edit_azimuth)}; - glm::dvec3 const direction{std::sin(azimuth), line.grade, std::cos(azimuth)}; - straight_apply(line, line.start, line.start + direction * line.length); - } - ImGui::PopItemWidth(); - } -} - -bool editor_mode::straights_active() -{ - return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights && false == current_straight().tracks.empty(); -} - -int editor_mode::straight_handle_hit() -{ - if (false == straights_active()) - return -1; - auto const &line{current_straight()}; - glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; - screen_projection const projection; - float best = kHandleRadius * kHandleRadius; - int hit{-1}; - for (int i = 0; i < 3; ++i) - { - if (auto const distance{projection.mouse_distance2(handles[i])}; distance < best) - { - best = distance; - hit = i; - } - } - return hit; -} - -void editor_mode::straight_refresh() -{ - auto &state{m_straights}; - state.current_for = selected_track(); - state.current = state.current_for != nullptr ? editor_track::find_straight(*state.current_for, state.tolerance) : editor_track::straight{}; - find_neighbour_straights(); - for (auto &line : state.found) - if (false == line.tracks.empty()) - line = editor_track::find_straight(*line.tracks.front(), state.tolerance); - for (auto &line : state.set) - if (false == line.tracks.empty()) - line = editor_track::find_straight(*line.tracks.front(), state.tolerance); -} - -bool editor_mode::in_straight_set(editor_track::straight const &Line) const -{ - for (auto const &member : m_straights.set) - for (auto const *track : Line.tracks) - if (std::find(member.tracks.begin(), member.tracks.end(), track) != member.tracks.end()) - return true; - return false; -} - -void editor_mode::straight_apply(editor_track::straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End) -{ - straights_apply({Line}, {{Start, End}}); -} - -void editor_mode::straights_apply(std::vector const &Lines, std::vector> const &Ends) -{ - auto &state{m_straights}; - state.status.clear(); - auto const tracks{editor_track::straight_affected(Lines)}; - if (false == tracks_editable(tracks, state.status)) - return; - std::vector> states; - for (auto *track : tracks) - states.emplace_back(track, editor_track::capture(*track)); - push_track_snapshot(std::move(states)); - editor_track::move_straights(Lines, Ends); - editor_track::commit(tracks); - straight_refresh(); -} - -void editor_mode::render_straight_gizmo() -{ - auto &state{m_straights}; - if (state.tool_mouse) - { - glm::dvec3 const ground{cursor_ground()}; - state.tool_handle = {ground.x, state.tool_handle.y, ground.z}; - typed_turn(); - return; - } - auto const &line{state.dragging ? state.drag_line : current_straight()}; - if (state.handle < 0 || line.tracks.empty()) - { - state.dragging = false; - return; - } - - glm::dvec3 const anchors[] = {line.start, line.end, (line.start + line.end) * 0.5}; - glm::dvec3 handled{anchors[state.handle]}; - bool const held{handle_drag_step(handled)}; - if (false == held && false == state.dragging) - return; - - if (held) - { - if (false == state.dragging) - { - std::vector lines; - if (in_straight_set(line)) - { - for (auto const &member : state.set) - if (false == member.tracks.empty()) - lines.push_back(editor_track::find_straight(*member.tracks.front(), state.tolerance)); - } - else - { - lines.push_back(line); - } - state.fitting = lines.size() == 1 && start_curve_fit(line, state.handle); - auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; - if (false == tracks_editable(tracks, state.status)) - { - state.fitting = false; - return; - } - state.dragging = true; - state.drag_line = line; - state.drag_lines = lines; - state.drag_states.clear(); - for (auto *track : tracks) - state.drag_states.emplace_back(track, editor_track::capture(*track)); - if (false == state.fitting) - { - push_track_snapshot(state.drag_states); - m_track_drag = tracks; - } - } - auto const &grabbed{state.drag_line}; - glm::dvec3 moved{handled}; - std::function transform; - if (state.handle == 2) - { - auto offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)}; - if (auto const values{typed_values()}; false == values.empty()) - offset = glm::dvec3{-grabbed.direction.y, 0.0, grabbed.direction.x} * values[0]; - transform = [offset](glm::dvec3 const &Point) { return Point + offset; }; - } - else - { - auto const pivot{state.handle == 0 ? grabbed.end : grabbed.start}; - auto const original{state.handle == 0 ? grabbed.start : grabbed.end}; - moved = snap_straight_direction(pivot, moved); - if (auto const values{typed_values()}; false == values.empty() && values[0] > 0.1) - { - glm::dvec2 heading{moved.x - pivot.x, moved.z - pivot.z}; - if (glm::length(heading) < 1e-6) - heading = {original.x - pivot.x, original.z - pivot.z}; - heading = glm::normalize(heading) * values[0]; - moved.x = pivot.x + heading.x; - moved.z = pivot.z + heading.y; - } - glm::dvec2 const before{original.x - pivot.x, original.z - pivot.z}; - glm::dvec2 const after{moved.x - pivot.x, moved.z - pivot.z}; - auto const lengthbefore{glm::length(before)}; - auto const lengthafter{std::max(0.1, glm::length(after))}; - auto const axis{before / std::max(1e-9, lengthbefore)}; - auto const newaxis{after / lengthafter}; - auto const scale{lengthafter / std::max(1e-9, lengthbefore)}; - auto const rise{moved.y - original.y}; - transform = [=](glm::dvec3 const &Point) { - glm::dvec2 const offset{Point.x - pivot.x, Point.z - pivot.z}; - auto const along{glm::dot(offset, axis)}; - auto const across{axis.x * offset.y - axis.y * offset.x}; - auto const planar{plan_of(pivot) + newaxis * (along * scale) + glm::dvec2{-newaxis.y, newaxis.x} * across}; - return glm::dvec3{planar.x, Point.y + rise * along / std::max(1e-9, lengthbefore), planar.y}; - }; - } - std::vector> ends; - for (auto const &member : state.drag_lines) - ends.emplace_back(transform(member.start), transform(member.end)); - state.preview_start = transform(grabbed.start); - state.preview_end = transform(grabbed.end); - if (state.fitting) - { - update_curve_fit(state.preview_start, state.preview_end); - return; - } - for (auto const &entry : state.drag_states) - editor_track::apply(*entry.first, entry.second); - editor_track::move_straights(state.drag_lines, ends); - m_track_dirty = true; - commit_track_drag(false); - } - else if (state.dragging) - { - if (state.fitting) - { - state.fitting = false; - state.dragging = false; - state.drag_lines.clear(); - if (m_route.result.valid) - route_apply(); - straight_refresh(); - return; - } - commit_track_drag(true); - state.dragging = false; - m_track_drag.clear(); - state.drag_states.clear(); - state.drag_lines.clear(); - straight_refresh(); - } -} - -void editor_mode::cancel_track_tools() -{ - auto &state{m_straights}; - if (state.dragging) - { - // the geometry goes back to what it was before the drag - if (false == state.fitting) - { - for (auto const &entry : state.drag_states) - editor_track::apply(*entry.first, entry.second); - m_track_dirty = true; - commit_track_drag(true); - m_track_drag.clear(); - } - state.dragging = false; - state.fitting = false; - state.drag_states.clear(); - state.drag_lines.clear(); - straight_refresh(); - } - m_extend.active = false; - m_turntable.placing = false; - m_signal.placing = false; - m_signal.moving = false; - m_handle_drag = {}; - if (m_point_drag.active) - { - m_track_snap = {}; - point_drag_finish(); - } - if (m_switch.placing) - { - m_switch.placing = false; - m_switch.status = STR_C("Placing of the switch cancelled"); - } - else if (m_switch.armed >= 0) - { - m_switch.armed = -1; - m_switch.status.clear(); - } - m_track_point = {}; - m_route.vertex = -1; - m_route.grip = -1; - state.handle = -1; - state.tool_placed = false; - state.detour.clear(); -} - -void editor_mode::straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius) -{ - auto &state{m_straights}; - state.status.clear(); - struct span - { - TTrack *track; - int path; - double from; - double to; - bool forward; - }; - auto line{Line}; - auto const point = [&](double const Along) { - auto const planar{plan_of(line.start) + line.direction * Along}; - return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; - }; - std::vector spans; - auto const collect = [&]() { - spans.clear(); - for (std::size_t i = 0; i < line.tracks.size(); ++i) - { - auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; - auto const a{line.along(path.points[segment_data::point::start])}; - auto const b{line.along(path.points[segment_data::point::end])}; - spans.push_back({line.tracks[i], line.paths[i], std::min(a, b), std::max(a, b), a < b}); - } - std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); - }; - int first{-1}; - int last{-1}; - auto const select = [&](double const End) { - first = -1; - last = -1; - for (int i = 0; i < static_cast(spans.size()); ++i) - { - if (spans[i].to > From && spans[i].from < End) - { - if (first < 0) - first = i; - last = i; - } - } - }; - auto const keep_regular = [&]() { - if (first < 0) - return; - auto const centre{(std::max(From, 0.0) + std::min(To, line.length)) * 0.5}; - int middle{first}; - for (int i = first; i <= last; ++i) - if (spans[i].from <= centre && spans[i].to >= centre) - middle = i; - if (spans[middle].track->eType != tt_Normal) - { - int best{-1}; - for (int i = first; i <= last; ++i) - if (spans[i].track->eType == tt_Normal && (best < 0 || std::abs(i - middle) < std::abs(best - middle))) - best = i; - if (best < 0) - { - first = -1; - last = -1; - return; - } - middle = best; - } - int low{middle}; - int high{middle}; - while (low > first && spans[low - 1].track->eType == tt_Normal) - --low; - while (high < last && spans[high + 1].track->eType == tt_Normal) - ++high; - first = low; - last = high; - }; - collect(); - select(To); - keep_regular(); - if (first < 0) - return; - - std::vector> splitstates; - std::vector splitcreated; - auto end{To}; - if (last + 1 >= static_cast(spans.size())) - { - auto const final{spans.back()}; - if (final.track->eType != tt_Normal) - return; - auto const length{final.to - final.from}; - auto const cut{To < final.to - 1.0 ? std::max(To, final.from + std::min(1.0, length * 0.5)) : final.from + length * 0.7}; - splitstates.emplace_back(final.track, editor_track::capture(*final.track)); - editor_track::chain split; - split.tracks = {final.track}; - split.forward = {final.forward}; - std::vector pieces(2); - pieces[0].points[segment_data::point::start] = point(final.from); - pieces[0].points[segment_data::point::end] = point(cut); - pieces[1].points[segment_data::point::start] = point(cut); - pieces[1].points[segment_data::point::end] = point(final.to); - splitcreated = editor_track::relay(split, pieces); - line = editor_track::find_straight(*line.tracks.front(), state.tolerance); - collect(); - end = std::min(To, cut); - select(end); - keep_regular(); - if (first < 0 || last + 1 >= static_cast(spans.size())) - { - for (auto *track : splitcreated) - editor_track::retire(*track); - editor_track::apply(*splitstates.front().first, splitstates.front().second); - editor_track::commit({splitstates.front().first}); - return; - } - } - auto const undo_split = [&]() { - for (auto *track : splitcreated) - editor_track::retire(*track); - for (auto const &entry : splitstates) - { - editor_track::apply(*entry.first, entry.second); - editor_track::commit({entry.first}); - } - }; - - editor_track::straight tail; - tail.direction = line.direction; - for (int i = last + 1; i < static_cast(spans.size()); ++i) - { - tail.tracks.push_back(spans[i].track); - tail.paths.push_back(spans[i].path); - } - tail.start = point(spans[last + 1].from); - tail.end = line.end; - tail.length = line.length - spans[last + 1].from; - tail.grade = line.grade; - - std::vector chainpaths; - for (int i = first; i <= last; ++i) - chainpaths.push_back(spans[i].track); - auto tracks{editor_track::straight_affected({tail})}; - for (auto *track : chainpaths) - if (std::find(tracks.begin(), tracks.end(), track) == tracks.end()) - tracks.push_back(track); - if (false == tracks_editable(tracks, m_straights.status)) - { - undo_split(); - return; - } - std::vector> states; - for (auto *track : tracks) - states.emplace_back(track, editor_track::capture(*track)); - - editor_track::move_straights({tail}, {{Tail(tail.start), Tail(tail.end)}}); - - m_route.from = spans[first].track; - m_route.to = spans[last].track; - route_reset(); - if (m_route.chain.tracks.empty() || false == m_route.chain.switches.empty()) - { - if (false == m_route.chain.switches.empty()) - m_route.error = STR_C("The curve would run through a switch"); - for (auto const &entry : states) - editor_track::apply(*entry.first, entry.second); - editor_track::commit(tracks); - undo_split(); - state.status = m_route.error; - return; - } - m_route.design.start_reserve = 0.0; - m_route.design.end_reserve = 0.0; - for (auto &vertex : m_route.design.vertices) - { - vertex.radius = Radius; - route_recommend(vertex); - if (false == state.auto_transitions) - { - vertex.transition_in = state.transition; - vertex.transition_out = state.transition; - } - } - route_update(); - if (false == m_route.result.valid) - { - for (auto const &entry : states) - editor_track::apply(*entry.first, entry.second); - editor_track::commit(tracks); - undo_split(); - state.status = m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front(); - return; - } - auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; - editor_track::keep_heights(m_route.chain, pieces); - auto created{editor_track::relay(m_route.chain, pieces, &m_route.to)}; - editor_track::commit(tracks); - for (auto const &entry : splitstates) - { - auto const found{std::find_if(states.begin(), states.end(), [&](auto const &State) { return State.first == entry.first; })}; - if (found != states.end()) - found->second = entry.second; - else - states.push_back(entry); - } - created.insert(created.begin(), splitcreated.begin(), splitcreated.end()); - { - auto changed{created}; - for (auto const &entry : states) - changed.push_back(entry.first); - digitize_drawn(changed); - } - push_track_snapshot(std::move(states), std::move(created)); - std::string error; - editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true); - straight_refresh(); - state.status = STR_C("Done, select a curve to adjust it in the Curve mode"); -} - -double editor_mode::straight_tool_radius(editor_track::straight const &Line) const -{ - if (m_straights.curve_radius > 0.0) - return m_straights.curve_radius; - double speed{-1.0}; - for (auto const *track : Line.tracks) - speed = std::max(speed, editor_track::velocity(*track)); - if (speed <= 0.0) - speed = 100.0; - auto const recommended{alignment::recommend(speed, 1000.0, m_route.design.norms)}; - return std::max(150.0, std::ceil(recommended.radius_min * 1.5 / 50.0) * 50.0); -} - -bool editor_mode::place_straight_tool() -{ - auto &state{m_straights}; - if (ui()->mode() != nodebank_panel::TRACK || m_track_tab != track_tab::straights || state.tool == 0) - return false; - if (state.tool == 2) - { - add_detour_point(); - return true; - } - auto const &line{current_straight()}; - if (line.tracks.empty()) - return false; - glm::dvec3 const ground{cursor_ground()}; - auto const along{std::clamp(line.along(ground), 0.0, line.length)}; - glm::dvec3 const axis{line.direction.x, line.grade, line.direction.y}; - state.tool_line = line; - state.tool_at = along; - state.tool_point = line.start + axis * along; - auto const reach{std::min(50.0, std::max(1.0, line.length - along))}; - state.tool_handle = state.tool_point + axis * reach; - state.tool_placed = true; - state.handle = -1; - return true; -} - -void editor_mode::apply_straight_tool() -{ - auto &state{m_straights}; - auto const line{state.tool_line}; - auto const offset{plan_of(state.tool_handle - state.tool_point)}; - auto const radius{straight_tool_radius(line)}; - if (state.tool == 1) - { - if (glm::length(offset) < 1e-3) - return; - auto const unit{glm::normalize(offset)}; - auto const angle{std::atan2(line.direction.x * unit.y - line.direction.y * unit.x, glm::dot(line.direction, unit))}; - if (std::abs(angle) < 1e-5) - return; - glm::dvec2 const pivot{state.tool_point.x, state.tool_point.z}; - double speed{-1.0}; - for (auto const *track : line.tracks) - speed = std::max(speed, editor_track::velocity(*track)); - auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; - auto const reach{radius * std::tan(std::min(std::abs(angle), 3.0) * 0.5) + transition * 0.5 + 10.0}; - straight_reshape(line, state.tool_at - reach, state.tool_at + reach, - [pivot, angle](glm::dvec3 const &Point) { - glm::dvec2 const relative{Point.x - pivot.x, Point.z - pivot.y}; - auto const c{std::cos(angle)}; - auto const s{std::sin(angle)}; - return glm::dvec3{pivot.x + relative.x * c - relative.y * s, Point.y, pivot.y + relative.x * s + relative.y * c}; - }, - radius); - } - else - { - glm::dvec2 const normal{-line.direction.y, line.direction.x}; - auto const shift{glm::dot(offset, normal)}; - auto const length{glm::dot(offset, line.direction)}; - if (std::abs(shift) < 1e-3 || length < 1.0) - return; - auto const translation{normal * shift}; - straight_reshape(line, state.tool_at, state.tool_at + length, - [translation](glm::dvec3 const &Point) { return glm::dvec3{Point.x + translation.x, Point.y, Point.z + translation.y}; }, - radius); - } - state.tool_placed = false; -} - -void editor_mode::start_straight_gesture(int const Tool) -{ - auto &state{m_straights}; - state.tool_before = state.tool; - state.tool = Tool; - if (false == place_straight_tool()) - { - state.tool = state.tool_before; - return; - } - state.tool_mouse = true; -} - -void editor_mode::finish_straight_gesture() -{ - auto &state{m_straights}; - state.tool_mouse = false; - apply_straight_tool(); - state.tool = state.tool_before; - state.tool_placed = false; -} - -void editor_mode::toggle_straight_set(TTrack &Track) -{ - auto &state{m_straights}; - auto const line{editor_track::find_straight(Track, state.tolerance)}; - if (line.tracks.empty()) - return; - if (in_straight_set(line)) - state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Other) { return std::find(Member.tracks.begin(), Member.tracks.end(), Other) != Member.tracks.end(); }); }), state.set.end()); - else - state.set.push_back(line); - m_node = &Track; - ui()->set_node(m_node); - m_track_tab = track_tab::straights; -} void editor_mode::delete_selected_track() { @@ -4080,1500 +1704,6 @@ void editor_mode::draw_track_hints() foreground->AddText(at, IM_COL32(255, 255, 255, 255), readout.c_str()); } -void editor_mode::update_build_tools() -{ - glm::dvec3 const ground{cursor_ground()}; - auto const &io{ImGui::GetIO()}; - glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; - m_straights.detour_mouse = ground; - auto const joining = [&](editor_track::snap_target const &Target) { - if (Target.track == m_join_track && Target.point == m_join_point) - return; - m_join_track = Target.track; - m_join_point = Target.point; - m_join_radius = 0.0; - m_join_variant = 0; - }; - bool const pointing{m_cursor_override.has_value() || ImGui::IsMousePosValid()}; - if (m_extend.active) - { - if (pointing) - m_extend.mouse = cursor_terrain(m_extend.point.y); - m_extend.snap = m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; - if (m_extend.snap.track == nullptr) - m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); - joining(m_extend.snap); - } - if (m_switch.placing) - m_switch.mouse = cursor_level(m_switch.point.y); - auto &lay{m_lay}; - lay.preview.clear(); - lay.preview_error.clear(); - lay.preview_length = 0.0; - if (false == lay.active) - return; - // geometric ray, same as extend: Mouse_Position() is a delayed depth pick and lags the cursor - double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : ground.y}; - if (pointing) - lay.mouse = typed_lay(cursor_terrain(height)); - if (pointing && ImGui::GetIO().KeyAlt && m_typed.empty() && false == lay.points.empty()) - lay.mouse = lay_angle_snap(lay.mouse); - lay.mouse_snap = m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); - if (lay.mouse_snap.track == nullptr) - lay.mouse = lay_bed(lay.mouse); - if (false == m_extend.active) - joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); - if (lay.points.empty()) - return; - auto points{lay.points}; - auto end{lay.mouse_snap}; - if (end.track != nullptr && glm::distance(end.position, points.front()) > 1.0) - points.push_back(end.position); - else - { - end = {}; - if (plan_distance(lay.mouse, points.back()) > 1.0) - points.push_back(lay.mouse); - } - lay.preview = lay_pieces(points, end, lay.preview_length, lay.preview_error); -} - -bool editor_mode::free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool) -{ - editor_track::point_ref const point{Path, Atend ? editor_track::point_kind::end : editor_track::point_kind::start}; - if (Path < 0 || Path >= static_cast(Track.m_paths.size()) || editor_track::is_connected(Track, point)) - return false; - auto const &path{Track.m_paths[Path]}; - auto const &start{path.points[segment_data::point::start]}; - auto const &end{path.points[segment_data::point::end]}; - auto const &control1{path.points[segment_data::point::control1]}; - auto const &control2{path.points[segment_data::point::control2]}; - glm::dvec3 tangent; - if (Atend) - tangent = control2 != glm::dvec3{} ? -control2 : end - (start + control1); - else - tangent = -(control1 != glm::dvec3{} ? control1 : (end + control2) - start); - glm::dvec2 const planar{tangent.x, tangent.z}; - if (glm::length(planar) < 1e-6) - return false; - Tool.path = Path; - Tool.atend = Atend; - Tool.track = &Track; - Tool.point = editor_track::point_position(Track, point); - Tool.direction = glm::normalize(planar); - Tool.grade = tangent.y / glm::length(planar); - Tool.mouse = Tool.point; - return true; -} - -bool editor_mode::chosen_free_end(extend_tool &Tool) -{ - auto *track{selected_track()}; - if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) - return false; - if (m_track_point.valid() && editor_track::is_end(m_track_point.kind) && free_end(*track, m_track_point.path, m_track_point.kind == editor_track::point_kind::end, Tool)) - return true; - for (int i = 0; i < static_cast(track->m_paths.size()); ++i) - for (auto const atend : {true, false}) - if (free_end(*track, i, atend, Tool)) - return true; - return false; -} - -bool editor_mode::start_extend(editor_track::snap_target const &End) -{ - auto *track{End.track}; - if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) - return false; - extend_tool tool; - if (false == free_end(*track, End.point.path, End.point.kind == editor_track::point_kind::end, tool)) - return false; - auto const &io{ImGui::GetIO()}; - m_extend = tool; - m_extend.origin = End; - m_extend.pressed = {io.MousePos.x, io.MousePos.y}; - m_extend.active = true; - if (track->eType == tt_Switch && End.point.path == 1 && End.point.kind == editor_track::point_kind::end) - m_extend.line = editor_track::find_straight(*track, m_straights.tolerance).tracks; - return true; -} - -bool editor_mode::extend_snap(editor_track::snap_target &Target) const -{ - auto const &tool{m_extend}; - if (false == tool.active || tool.track == nullptr) - return false; - Target = tool.snap; - return Target.track != nullptr && glm::distance(Target.position, tool.point) > 0.5; -} - -glm::dvec3 editor_mode::cursor_ground() const -{ - glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; - if (GfxRenderer->Mouse_Hit()) - return ground; - screen_projection const projection; - glm::dvec3 level; - if (projection.on_level(ImGui::GetIO().MousePos, 0.0, level)) - return level; - return {ground.x, 0.0, ground.z}; -} - -glm::dvec3 editor_mode::cursor_level(double const Height) const -{ - if (m_cursor_override.has_value()) - return glm::dvec3{m_cursor_override->x, Height, m_cursor_override->z}; - glm::dvec3 const ground{cursor_ground()}; - screen_projection const projection; - glm::dvec3 level; - if (false == projection.on_level(ImGui::GetIO().MousePos, Height, level) || (false == Global.editor_tracks && glm::distance(level, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) - return ground; - return level; -} - -editor_track::snap_target editor_mode::snap_free_end(glm::dvec3 const &Near, glm::vec2 const &Screen, TTrack const *Self, int const Category, std::vector const &Exclude) const -{ - screen_projection const projection; - auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; - editor_track::snap_target result; - auto best{kSnapPixels * kSnapPixels}; - for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) - { - ImVec2 screen; - if (false == projection.project(candidate.position, screen)) - continue; - auto const distance{(screen.x - Screen.x) * (screen.x - Screen.x) + (screen.y - Screen.y) * (screen.y - Screen.y)}; - if (distance < best) - { - best = distance; - result = candidate; - } - } - if (result.track == nullptr && m_track_snap_radius > 0.0f) - result = editor_track::find_free_end(Self, Category, Near, m_track_snap_radius, Exclude); - return result; -} - -void editor_mode::draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const -{ - screen_projection const projection; - auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; - auto const &display{ImGui::GetIO().DisplaySize}; - auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; - for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) - { - ImVec2 screen; - if (projection.project(candidate.position, screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) - drawlist->AddCircle(screen, 7.0f, IM_COL32(255, 60, 255, 170), 12, 1.5f); - } -} - -bool editor_mode::switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point) -{ - auto *selected{selected_track()}; - if (m_track_tab != track_tab::turnout || m_switch.armed < 0 || selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) - return false; - auto const &line{current_straight()}; - if (false == line.tracks.empty()) - { - auto const along{std::clamp(line.along(Ground), 0.0, line.length)}; - Point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along; - } - else - { - Point = editor_track::point_at(*selected, editor_track::nearest_parameter(*selected, Ground)); - } - return plan_distance(Point, Ground) <= kSwitchReach; -} - -void editor_mode::handle_drag_start(glm::dvec3 const &Anchor) -{ - auto const &io{ImGui::GetIO()}; - m_handle_drag = {}; - m_handle_drag.active = true; - m_handle_drag.from = {io.MousePos.x, io.MousePos.y}; - m_handle_drag.height = Anchor.y; - m_handle_drag.position = Anchor; - screen_projection const projection; - glm::dvec3 level; - if (projection.on_level(io.MousePos, Anchor.y, level)) - m_handle_drag.offset = {Anchor.x - level.x, 0.0, Anchor.z - level.z}; -} - -bool editor_mode::handle_drag_step(glm::dvec3 &Position) -{ - auto &drag{m_handle_drag}; - auto const &io{ImGui::GetIO()}; - if (false == drag.active) - return false; - if (false == io.MouseDown[0]) - { - drag = {}; - return false; - } - glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; - if (false == drag.moved && glm::length(mouse - drag.from) < kDragPixels) - return false; - drag.moved = true; - screen_projection const projection; - glm::dvec3 level; - if (projection.on_level(io.MousePos, drag.height, level) && (Global.editor_tracks || glm::distance(level, glm::dvec3{Global.pCamera.Pos}) <= static_cast(kMaxPlacementDistance))) - drag.position = level + drag.offset; - Position = drag.position; - return true; -} - -void editor_mode::point_drag_start(editor_track::point_ref const &Point) -{ - auto *track{selected_track()}; - m_track_point = Point; - m_point_drag = {}; - if (track == nullptr || false == Point.valid() || Point.path >= static_cast(track->m_paths.size())) - return; - auto const &io{ImGui::GetIO()}; - m_point_drag.active = true; - m_point_drag.from = {io.MousePos.x, io.MousePos.y}; - m_point_drag.height = editor_track::point_position(*track, Point).y; -} - -void editor_mode::point_drag_update() -{ - auto &drag{m_point_drag}; - if (false == drag.active) - return; - auto const &io{ImGui::GetIO()}; - if (false == io.MouseDown[0]) - { - point_drag_finish(); - return; - } - auto *track{selected_track()}; - if (track == nullptr || false == m_track_point.valid() || m_track_point.path >= static_cast(track->m_paths.size())) - { - drag = {}; - return; - } - glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; - if (false == drag.moved) - { - if (glm::length(mouse - drag.from) < kDragPixels) - return; - std::string reason; - if (false == editor_track::can_edit_geometry(*track, reason)) - { - ui()->set_status(reason, true); - drag = {}; - return; - } - drag.moved = true; - m_track_drag = {track}; - m_track_drag_points.clear(); - m_track_joints.clear(); - m_track_snap = {}; - if (editor_track::is_end(m_track_point.kind) && m_track_drag_connected) - { - for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) - { - std::string neighbourreason; - if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason)) - continue; - m_track_drag_points.emplace_back(connection); - if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) - m_track_drag.emplace_back(connection.first); - } - } - std::vector> states; - for (auto *dragged : m_track_drag) - states.emplace_back(dragged, editor_track::capture(*dragged)); - push_track_snapshot(std::move(states)); - records_watch_begin(); - } - screen_projection const projection; - glm::dvec3 position; - if (false == projection.on_level(io.MousePos, drag.height, position) || (false == Global.editor_tracks && glm::distance(position, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) - return; - m_track_snap = {}; - if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) - { - std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); - m_track_snap = snap_free_end(position, mouse, track, track->iCategoryFlag & 15, exclude); - if (m_track_snap.track != nullptr) - position = m_track_snap.position; - } - editor_track::move_point(*track, m_track_point, position); - for (auto const &connection : m_track_drag_points) - editor_track::move_point(*connection.first, connection.second, position); - m_track_dirty = true; - commit_track_drag(false); -} - -void editor_mode::point_drag_finish() -{ - auto const drag{m_point_drag}; - m_point_drag = {}; - if (false == drag.active || false == drag.moved) - return; - if (auto *track{selected_track()}; track != nullptr && m_track_snap.track != nullptr && m_track_point.valid()) - { - editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent); - m_track_dirty = true; - } - m_track_snap = {}; - commit_track_drag(true); - records_watch_end(); - m_track_drag.clear(); - m_track_drag_points.clear(); - m_track_joints.clear(); -} - -std::vector editor_mode::extend_pieces() const -{ - std::vector result; - auto const &tool{m_extend}; - editor_track::snap_target target; - if (extend_snap(target)) - { - editor_track::branch_link link; - std::string reason; - if (plan_branch_link(link, reason)) - { - m_join_last = {}; - return link.pieces; - } - double length; - std::string error; - return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error); - } - m_join_last = {}; - if (m_track_layout != track_layout::straight && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) - { - auto const &diverging{tool.track->m_paths[1]}; - bezier const curve{diverging}; - auto const begin{plan_of(curve.first(0.0))}; - auto const finish{plan_of(curve.first(1.0))}; - auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; - if (auto const radius{editor_track::branch_radius(diverging)}; radius > 0.0) - { - int const side{turn > 0.0 ? 1 : -1}; - auto const &direction{tool.direction}; - auto const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * radius)}; - auto const angle{geometry::signed_angle(plan_of(tool.point) - centre, plan_of(tool.mouse) - centre) * side}; - if (angle * radius < 0.5) - return result; - auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side)}; - for (auto &piece : pieces) - piece.rolls = {diverging.rolls[1], diverging.rolls[1]}; - follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); - return pieces; - } - } - auto radius{m_lay.radius}; - if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) - radius = std::abs(values[0]); - auto pieces{editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; - follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); - return pieces; -} - -double editor_mode::design_speed(TTrack const *From, TTrack const *To) const -{ - double speed{0.0}; - for (auto const *track : {From, To}) - { - if (track == nullptr) - continue; - auto const velocity{editor_track::velocity(*track)}; - if (velocity > 0.0) - speed = speed > 0.0 ? std::min(speed, velocity) : velocity; - } - if (speed <= 0.0) - speed = m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; - return speed; -} - -std::vector editor_mode::join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, - std::string &Error) const -{ - alignment::join_request request; - request.start.position = Start; - request.start.direction = Direction; - request.start.roll = From != nullptr ? -editor_track::joint_roll(*From, Frompoint) : 0.0; - request.end.position = End.position; - request.end.direction = plan_of(End.direction); - request.end.roll = End.track != nullptr ? editor_track::joint_roll(*End.track, End.point) : 0.0; - request.radius = m_join_radius; - request.variant = m_join_variant; - request.transitions = m_lay.transitions; - request.speed = design_speed(From, End.track); - request.kink = m_joints.angle * 0.5; - request.shape = m_route.design.shape; - request.norms = laying_norms(); - auto result{alignment::join(request)}; - if (m_lay.transitions && result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) - { - auto const grade{(End.position.y - Start.y) / std::max(1.0, plan_distance(Start, End.position))}; - auto const in{From != nullptr && From->eType == tt_Normal ? editor_track::ease_out_of(*From, Start, grade, request.speed, request.norms) : editor_track::easing{}}; - auto const out{End.track != nullptr && End.track->eType == tt_Normal ? editor_track::ease_out_of(*End.track, End.position, -grade, request.speed, request.norms) : editor_track::easing{}}; - if (auto eased{eased_join(request, in, out)}) - result = std::move(*eased); - } - Length = result.length; - std::vector notes; - for (auto const &warning : result.warnings) - if (m_join_radius > 0.0 || warning.find("R reduced") == std::string::npos) - notes.push_back(warning); - if (result.kind == alignment::join_kind::bezier) - notes.insert(notes.begin(), STR_C("No curve fits between the ends, they are joined with a free curve")); - Error = lines_of(notes); - m_join_last = result; - return result.pieces; -} - -void editor_mode::finish_extend() -{ - auto const &io{ImGui::GetIO()}; - if (glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - m_extend.pressed) < kDragPixels) - { - m_extend.active = false; - if (m_lay.active && m_lay.points.empty() && m_extend.origin.track != nullptr) - { - m_lay.status.clear(); - m_lay.start = m_extend.origin; - m_lay.points.push_back(m_extend.origin.position); - } - else if (false == m_lay.active && m_extend.origin.track != nullptr && m_extend.origin.track == selected_track()) - m_track_point = m_extend.origin.point; - return; - } - std::string reason; - if (editor_track::branch_link link; plan_branch_link(link, reason)) - { - finish_branch_link(link); - return; - } - if (false == reason.empty()) - ui()->set_status(reason, true); - crossover_plan plan; - editor_track::snap_target target; - if (false == extend_snap(target) && plan_crossover(plan)) - { - auto *origin{m_extend.track}; - m_extend.active = false; - std::vector> states; - std::vector created; - std::vector removed; - if (false == place_switch_on_straight(plan.target, plan.shape, origin, plan.along, plan.direction, plan.side, false, states, created, removed, switch_name_for_new())) - return; - auto *placed{*std::find_if(created.begin(), created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; - if (glm::distance(plan.insert.points[segment_data::point::start], plan.insert.points[segment_data::point::end]) > 0.01) - { - auto *insert{editor_track::create_path(*origin, plan.insert)}; - editor_track::commit({insert}); - created.push_back(insert); - } - push_track_snapshot(std::move(states), std::move(created), std::move(removed)); - straight_refresh(); - if (place_switch_drive(*placed)) - join_last_steps(); - advance_switch_name(); - return; - } - auto const pieces{extend_pieces()}; - auto *style{m_extend.track}; - m_extend.active = false; - if (pieces.empty() || style == nullptr) - return; - std::vector created; - for (auto const &piece : pieces) - created.push_back(editor_track::create_path(*style, piece)); - editor_track::commit(created); - digitize_drawn(created); - push_track_snapshot({}, created); - first_step_done(first_step::laid); - if (target.track != nullptr) - first_step_done(first_step::join); - m_node = created.back(); - ui()->set_node(m_node); - straight_refresh(); -} - -void editor_mode::extend_add(int const Side) -{ - extend_tool tool; - if (false == chosen_free_end(tool)) - return; - auto const length{std::max(0.5, m_extend_length)}; - std::vector pieces; - if (Side == 0) - { - segment_data path; - path.points[segment_data::point::start] = tool.point; - path.points[segment_data::point::end] = {tool.point.x + tool.direction.x * length, tool.point.y + tool.grade * length, tool.point.z + tool.direction.y * length}; - pieces.push_back(path); - } - else - { - auto const radius{std::max(10.0, m_extend_radius)}; - pieces = arc_pieces(tool.point, tool.direction, tool.grade, radius, std::min(length / radius, glm::radians(180.0)), Side); - } - std::vector created; - for (auto const &piece : pieces) - created.push_back(editor_track::create_path(*tool.track, piece)); - editor_track::commit(created); - digitize_drawn(created); - push_track_snapshot({}, created); - m_node = created.back(); - ui()->set_node(m_node); - m_track_point = {}; - straight_refresh(); -} - -void editor_mode::render_extend_ui() -{ - extend_tool tool; - if (false == chosen_free_end(tool)) - return; - if (false == ImGui::CollapsingHeader(STR_C("Continue from the free end"), ImGuiTreeNodeFlags_DefaultOpen)) - return; - ImGui::PushItemWidth(100.0f); - ImGui::DragScalar(STR_C("Length (m)##extend"), ImGuiDataType_Double, &m_extend_length, 0.5f, nullptr, nullptr, "%.1f"); - item_tooltip("Length of the next piece, along the curve for the curves"); - ImGui::SameLine(); - ImGui::DragScalar("R (m)##extend", ImGuiDataType_Double, &m_extend_radius, 5.0f, nullptr, nullptr, "%.0f"); - item_tooltip("Radius of the curves added"); - ImGui::PopItemWidth(); - m_extend_length = std::clamp(m_extend_length, 0.5, 5000.0); - m_extend_radius = std::clamp(m_extend_radius, 10.0, 100000.0); - if (ImGui::Button(STR_C("Straight##extend"), ImVec2(100.0f, 0.0f))) - extend_add(0); - item_tooltip("Adds a straight at the free end; the new piece gets selected, so the next click goes on from it"); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Curve left##extend"), ImVec2(100.0f, 0.0f))) - extend_add(1); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) - extend_add(-1); -} - -void editor_mode::render_layout_combo() -{ - char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; - auto current{static_cast(m_track_layout)}; - ImGui::PushItemWidth(200.0f); - if (ImGui::BeginCombo(STR_C("Shape from a free end"), STR_C(names[current]))) - { - for (int i = 0; i < 4; ++i) - if (ImGui::Selectable(STR_C(names[i]), i == current)) - m_track_layout = static_cast(i); - ImGui::EndCombo(); - } - ImGui::PopItemWidth(); - item_tooltip("What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius"); -} - -bool editor_mode::track_wheel(double const Offset) -{ - auto const &io{ImGui::GetIO()}; - if (m_switch.placing && m_switch.connection >= 0 && Offset != 0.0 && (io.KeyCtrl || io.KeyShift || io.KeyAlt)) - { - auto &tool{m_switch}; - int const step{Offset > 0.0 ? 1 : -1}; - if (io.KeyShift) - tool.connection = (tool.connection + step + connection::kind_count) % connection::kind_count; - else if (io.KeyCtrl) - { - static double const radii[]{0.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0}; - int index{0}; - for (int i = 0; i < static_cast(std::size(radii)); ++i) - if (radii[i] <= tool.connection_radius + 1e-6) - index = i; - tool.connection_radius = radii[std::clamp(index + step, 0, static_cast(std::size(radii)) - 1)]; - } - else - tool.connection_together = false == tool.connection_together; - return true; - } - if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0) - return false; - bool const laying{m_lay.active && false == m_lay.points.empty()}; - if (false == m_extend.active && false == laying) - return false; - int const step{Offset > 0.0 ? 1 : -1}; - editor_track::snap_target target; - bool const joined{m_extend.active ? extend_snap(target) : m_lay.mouse_snap.track != nullptr && m_lay.start.track != nullptr}; - static double const radii[]{100.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0, 4000.0, 6000.0}; - auto const next_radius = [&](double const Current) { - if (step > 0) - { - for (auto const radius : radii) - if (radius > Current * 1.001) - return radius; - return std::end(radii)[-1]; - } - for (auto radius{std::rbegin(radii)}; radius != std::rend(radii); ++radius) - if (*radius < Current * 0.999) - return *radius; - return radii[0]; - }; - first_step_done(io.KeyCtrl ? first_step::wheel : first_step::shape); - if (io.KeyCtrl) - { - if (false == joined) - m_lay.radius = next_radius(m_lay.radius); - else if (m_join_radius <= 0.0) - m_join_radius = next_radius(m_join_last.radius > 0.0 ? m_join_last.radius : 500.0); - else if (step > 0 && m_join_radius >= std::end(radii)[-1]) - m_join_radius = 0.0; - else - m_join_radius = next_radius(m_join_radius); - return true; - } - if (joined) - m_join_variant += step; - else - m_track_layout = static_cast((static_cast(m_track_layout) + 4 - step) % 4); - return true; -} - -std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const -{ - auto const &tool{m_lay}; - Length = 0.0; - if (Points.size() < 2) - return {}; - if (Points.size() == 2) - { - auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; - if (glm::length(leaving) < 1e-6) - { - Error = STR_C("Two points lie on one another"); - return {}; - } - if (End.track != nullptr && tool.start.track != nullptr) - return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); - m_join_last = {}; - std::vector pieces; - if (tool.start.track != nullptr) - { - auto const grade{-grade_of(tool.start.direction)}; - pieces = editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); - Error = narrowed(pieces, m_track_layout, tool.radius); - follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN(), tool.start.track); - } - else if (End.track == nullptr) - { - auto const length{glm::length(leaving)}; - pieces = straight_piece(Points[0], glm::normalize(leaving), (Points[1].y - Points[0].y) / length, length); - follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), Points[1].y, std::numeric_limits::quiet_NaN(), tool.copy_selected ? selected_track() : nullptr); - } - else if (glm::length(plan_of(End.direction)) > 1e-6) - { - auto const back{editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; - for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) - pieces.push_back(reversed(*piece)); - Error = narrowed(pieces, m_track_layout, tool.radius); - follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); - } - for (auto const &piece : pieces) - Length += bezier{piece}.plan_length(); - if (pieces.empty()) - Error = STR_C("The track can't be laid through these points"); - return pieces; - } - auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; - auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; - if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) - { - Error = STR_C("Two points lie on one another"); - return {}; - } - alignment::design design; - auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)}; - if (false == shape.valid) - { - Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front(); - return {}; - } - Error = lines_of(shape.warnings); - auto const count = [&](alignment::result const &Shape, alignment::design const &Design) { - return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); - }; - // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them - 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 editor_track::easing{}; - auto result{editor_track::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{tool.transitions ? eased(tool.start, 0.0, 1.0) : editor_track::easing{}}; - auto const out{tool.transitions ? eased(End, shape.length, -1.0) : editor_track::easing{}}; - if ((false == in.pieces.empty() || false == out.pieces.empty()) && editor_track::eases_room(in, out, Points.front(), design.start_direction, Points.back(), design.end_direction)) - { - auto points{Points}; - auto starting{design.start_direction}; - auto ending{design.end_direction}; - 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) - { - Length = editor_track::eased_length(in, easedshape.length, out); - Error = lines_of(easedshape.warnings); - return editor_track::eased_pieces(in, alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign)), out); - } - 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 = laying_norms(); - design.shape = m_route.design.shape; - design.speed = design_speed(tool.start.track, nullptr); - design.start = Points.front(); - design.end = Points.back(); - design.start_direction = Startdirection; - design.end_direction = Enddirection; - - alignment::vertex corner; - corner.radius = std::max(1.0, tool.radius); - if (Points.size() == 2) - { - if (false == alignment::collinear(design)) - { - glm::dvec2 intersection; - if (alignment::tangent_intersection(design, intersection)) - { - design.vertices.push_back(corner); - } - else - { - corner.offset = plan_distance(design.start, design.end) / 3.0; - design.vertices = {corner, corner}; - } - } - } - else - { - for (std::size_t i = 1; i + 1 < Points.size(); ++i) - { - corner.position = plan_of(Points[i]); - design.vertices.push_back(corner); - } - if (design.vertices.size() >= 2) - { - design.vertices.front().offset = std::max(1.0, glm::dot(plan_of(Points[1] - Points.front()), design.start_direction)); - design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); - } - } - return alignment::compute_recommended(design, tool.transitions); -} - -void editor_mode::lay_click() -{ - auto &tool{m_lay}; - tool.status.clear(); - auto const snap{tool.mouse_snap}; - if (snap.track != nullptr && false == tool.points.empty() && glm::distance(snap.position, tool.points.front()) > 1.0) - { - // reaching a free end finishes the track there, in line with the path it joins - tool.points.push_back(snap.position); - lay_finish(snap); - return; - } - if (tool.points.empty()) - { - tool.chain.clear(); - tool.start = snap; - tool.points.push_back(snap.track != nullptr ? snap.position : tool.mouse); - return; - } - auto const &point{tool.mouse}; - if (plan_distance(point, tool.points.back()) < 1.0) - return; - m_typed.clear(); - if (tool.points.size() == 1) - { - lay_tool::step const previous{tool.start, tool.points.front()}; - tool.points.push_back(tool.mouse); - lay_finish({}); - if (false == tool.points.empty()) - { - tool.points.pop_back(); - return; - } - auto *last{dynamic_cast(m_node)}; - if (last == nullptr || last->m_paths.empty()) - return; - auto const &path{last->m_paths.front()}; - editor_track::snap_target next; - next.track = last; - next.point = {0, editor_track::point_kind::end}; - next.position = path.points[segment_data::point::end]; - auto const &control{path.points[segment_data::point::control2]}; - next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position); - tool.chain.push_back(previous); - tool.start = next; - tool.points = {next.position}; - return; - } - tool.points.push_back(point); -} - -void editor_mode::lay_finish(editor_track::snap_target const &End) -{ - auto &tool{m_lay}; - double length; - std::string error; - auto const pieces{lay_pieces(tool.points, End, length, error)}; - tool.status_time = ImGui::GetTime(); - if (pieces.empty()) - { - tool.status = error.empty() ? std::string{STR_C("Click at least two points")} : error; - if (End.track != nullptr && false == tool.points.empty()) - tool.points.pop_back(); - return; - } - auto const *style{tool.start.track != nullptr ? tool.start.track : End.track}; - if (style == nullptr && tool.copy_selected) - style = selected_track(); - std::vector created; - for (auto const &piece : pieces) - created.push_back(style != nullptr ? editor_track::create_path(*style, piece) : editor_track::create_path(tool.style, piece)); - editor_track::commit(created); - digitize_drawn(created); - push_track_snapshot({}, created); - first_step_done(first_step::laid); - if (End.track != nullptr) - first_step_done(first_step::join); - 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); - tool.points.clear(); - tool.start = {}; - m_node = created.back(); - ui()->set_node(m_node); - straight_refresh(); -} - -void editor_mode::lay_enter() -{ - auto &tool{m_lay}; - if (false == m_typed.empty()) - { - lay_click(); - return; - } - if (tool.points.empty()) - return; - auto const end{tool.mouse_snap}; - if (end.track != nullptr && glm::distance(end.position, tool.points.front()) > 1.0) - { - tool.points.push_back(end.position); - lay_finish(end); - return; - } - auto const point{placement_on_ground(tool.mouse)}; - bool const appended{plan_distance(point, tool.points.back()) > 1.0}; - if (appended) - tool.points.push_back(point); - auto const count{tool.points.size()}; - lay_finish({}); - if (appended && tool.points.size() == count) - tool.points.pop_back(); -} - -void editor_mode::lay_cancel() -{ - auto &tool{m_lay}; - tool.points.clear(); - tool.start = {}; - tool.chain.clear(); - tool.preview.clear(); - tool.status.clear(); -} - -void editor_mode::lay_after_history(bool const Undo) -{ - auto &tool{m_lay}; - if (tool.points.empty()) - return; - if (false == Undo) - { - lay_cancel(); - return; - } - while (tool.start.track != nullptr && tool.start.track->m_editorremoved) - { - if (tool.chain.empty()) - { - lay_cancel(); - return; - } - tool.start = tool.chain.back().start; - tool.points = {tool.chain.back().point}; - tool.chain.pop_back(); - } -} - -void editor_mode::lay_back() -{ - auto &tool{m_lay}; - if (false == tool.chain.empty()) - { - undo_last(); - tool.status = STR_C("The last piece was taken back"); - tool.status_time = ImGui::GetTime(); - return; - } - if (false == tool.points.empty()) - tool.points.pop_back(); - if (tool.points.empty()) - tool.start = {}; -} - -glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse) const -{ - auto const &last{m_lay.points.back()}; - glm::dvec2 const offset{Mouse.x - last.x, Mouse.z - last.z}; - auto const length{glm::length(offset)}; - if (length < 1.0) - return Mouse; - auto const leaving{m_lay.start.track != nullptr ? -plan_of(m_lay.start.direction) : glm::dvec2{0.0}}; - glm::dvec2 const reference{glm::length(leaving) > 1e-6 ? glm::normalize(leaving) : glm::dvec2{0.0, 1.0}}; - auto const step{glm::radians(15.0)}; - auto const heading{turned(reference, std::round(signed_angle(reference, offset / length) / step) * step) * length}; - return {last.x + heading.x, Mouse.y, last.z + heading.y}; -} - -editor_mode::track_hover editor_mode::split_spot() const -{ - auto const *selected{selected_track()}; - if (selected == nullptr || ui()->mode() != nodebank_panel::TRACK || ImGui::GetIO().WantCaptureMouse) - return {}; - return track_under_cursor(selected, 20.0f); -} - -alignment::limits editor_mode::laying_norms() const -{ - auto norms{m_route.design.norms}; - norms.checked = false == m_norms_free; - return norms; -} - -bool editor_mode::render_norms_choice() -{ - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(STR_C("Norms:")); - auto changed{false}; - char const *names[]{STR_C("free"), STR_C("PKP PLK")}; - for (int i = 0; i < 2; ++i) - { - ImGui::SameLine(); - auto const chosen{m_norms_free == (i == 0)}; - if (chosen) - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - if (ImGui::Button(names[i], ImVec2(90.0f, 0.0f)) && false == chosen) - { - m_norms_free = i == 0; - m_lay.transitions = false == m_norms_free; - changed = true; - } - if (chosen) - ImGui::PopStyleColor(); - item_tooltip(i == 0 ? "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked" - : "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); - } - if (changed && false == m_route.chain.tracks.empty()) - route_update(); - return changed; -} - -void editor_mode::render_lay_ui() -{ - auto &tool{m_lay}; - render_norms_choice(); - ImGui::Separator(); - ImGui::PushItemWidth(120.0f); - ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); - item_tooltip("Drag to change, Ctrl+click to type the value. While laying: Ctrl+wheel"); - tool.radius = std::max(10.0, tool.radius); - ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f"); - tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); - item_tooltip(m_norms_free ? "Speed limit of the new paths; the speed of the track joined comes first" - : "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows"); - ImGui::PopItemWidth(); - render_layout_combo(); - if (auto *track{selected_track()}; track != nullptr) - { - std::string reason; - if (editor_track::can_edit_geometry(*track, reason)) - render_extend_ui(); - } - if (false == ImGui::TreeNode(STR_C("More settings"))) - { - render_lay_status(); - return; - } - ImGui::PushItemWidth(120.0f); - ImGui::DragScalar(STR_C("Longest path (m)"), ImGuiDataType_Double, &tool.piece_length, 1.0f, nullptr, nullptr, "%.0f"); - tool.piece_length = std::clamp(tool.piece_length, 5.0, 1000.0); - ImGui::PopItemWidth(); - ImGui::Checkbox(STR_C("Parameters of the selected track"), &tool.copy_selected); - item_tooltip("Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins"); - if (false == tool.copy_selected) - { - auto const text_field = [](char const *Label, std::string &Value, char const *Tooltip) { - char buffer[256]; - std::snprintf(buffer, sizeof(buffer), "%s", Value.c_str()); - if (ImGui::InputText(Label, buffer, sizeof(buffer))) - Value = buffer; - item_tooltip(Tooltip); - }; - ImGui::PushItemWidth(-1.0f); - text_field("##layrail", tool.style.rail, STR_C("Texture of the rails")); - text_field("##layballast", tool.style.ballast, STR_C("Texture of the ballast")); - ImGui::PopItemWidth(); - } - ImGui::TreePop(); - render_lay_status(); -} - -void editor_mode::render_lay_status() -{ - auto const &tool{m_lay}; - if (scene::Layers.active() == null_handle) - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The scenery isn't opened for editing: new track won't be saved")); - if (tool.active && false == tool.preview_error.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", tool.preview_error.c_str()); - if (false == tool.status.empty()) - ImGui::TextWrapped("%s", tool.status.c_str()); -} - -std::vector editor_mode::switch_preview() const -{ - auto const &tool{m_switch}; - if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) - return {}; - if (tool.curved) - { - auto const sample{sample_at(tool.frame, tool.along)}; - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; - auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; - glm::dvec2 const direction{sample.tangent * static_cast(forward)}; - auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; - return curved_switch_paths(tool.templates[tool.armed], tool.frame, tool.along, forward, side); - } - if (switch_link plan; plan_switch_link(plan)) - { - plan.paths.insert(plan.paths.end(), plan.link.pieces.begin(), plan.link.pieces.end()); - return plan.paths; - } - auto const &line{tool.line}; - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; - auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1}; - glm::dvec2 const direction{line.direction * static_cast(forward)}; - auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; - return editor_track::place_switch(tool.templates[tool.armed], tool.point, direction, side, line.grade * forward); -} - -bool editor_mode::start_switch_placement() -{ - auto &tool{m_switch}; - if (tool.armed < 0 || m_track_tab != track_tab::turnout) - return false; - // with no plain track selected, or a press away from it, the click selects - auto *selected{selected_track()}; - if (selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) - return false; - glm::dvec3 const ground{cursor_level(selected->m_paths.front().points[segment_data::point::start].y)}; - tool.curved = false; - tool.status.clear(); - if (current_straight().tracks.empty()) - { - auto *track{selected}; - if (tool.templates[tool.armed].double_slip) - { - tool.status = STR_C("A double slip goes only into a straight crossing another one"); - return false; - } - editor_track::curve curve; - editor_track::chain chain; - std::string error; - if (false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) - { - tool.status = STR_C("The selected track is neither a recognized straight nor a curve"); - return false; - } - if (false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) - { - tool.status = error.empty() ? std::string{STR_C("The curve can't be followed")} : error; - return false; - } - for (auto const &part : editor_track::chain_parts(chain)) - if (std::find(part.tracks.begin(), part.tracks.end(), track) != part.tracks.end()) - { - chain = part; - break; - } - tool.frame = sample_chain(chain); - tool.frame_tracks = chain.tracks; - if (tool.frame.size() < 2) - return false; - tool.curved = true; - tool.along = nearest_station(tool.frame, ground); - tool.point = sample_at(tool.frame, tool.along).position; - if (plan_distance(tool.point, ground) > kSwitchReach) - return false; - tool.mouse = ground; - tool.placing = true; - return true; - } - auto const &line{current_straight()}; - auto const along{std::clamp(line.along(ground), 0.0, line.length)}; - auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along}; - if (plan_distance(point, ground) > kSwitchReach) - return false; - if (tool.templates[tool.armed].double_slip) - { - insert_double_slip(line, ground, tool.templates[tool.armed]); - return true; - } - tool.along = along; - tool.point = point; - tool.mouse = ground; - tool.line = line; - tool.placing = true; - return true; -} - -void editor_mode::finish_switch_placement() -{ - auto &tool{m_switch}; - if (tool.connection >= 0) - { - if (false == tool.curved) - { - insert_connection(); - return; - } - tool.placing = false; - tool.status = STR_C("A connection is laid between straights"); - ui()->set_status(tool.status, true); - return; - } - switch_link plan; - auto const linked{plan_switch_link(plan)}; - tool.placing = false; - if (tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) - return; - if (linked) - { - insert_linked_switch(plan); - return; - } - if (tool.curved) - { - auto const sample{sample_at(tool.frame, tool.along)}; - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; - auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; - glm::dvec2 const direction{sample.tangent * static_cast(forward)}; - auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; - insert_curved_switch(forward, side); - return; - } - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; - auto const forward{glm::dot(offset, tool.line.direction) >= 0.0 ? 1 : -1}; - glm::dvec2 const direction{tool.line.direction * static_cast(forward)}; - auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; - insert_switch(tool.line, tool.along, forward, side); - if (false == plan.reason.empty()) - ui()->set_status(plan.reason, true); -} - -void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side) -{ - std::vector> states; - std::vector created; - std::vector removed; - if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed, switch_name_for_new())) - return; - auto const placed{created}; - push_track_snapshot(std::move(states), std::move(created), std::move(removed)); - straight_refresh(); - switch_placed(placed); -} - - -void editor_mode::switch_placed(std::vector const &Created) -{ - auto const found{std::find_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; - if (found == Created.end()) - return; - auto const slip{std::count_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; }) > 1}; - m_switch.placing = false; - first_step_done(first_step::turnout); - m_track_point = {}; - m_node = *found; - ui()->set_node(m_node); - if (slip) - { - auto const known{switch_store::named(**found)}; - if (false == known.has_value() || (known->kind != "double" && known->kind != "slip")) - return; - std::vector driven; - for (auto *track : Created) - if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track)) - { - join_last_steps(); - driven.push_back(track->name()); - } - // a slip has a drive at each of its switches - if (known->kind == "slip") - { - if (false == driven.empty()) - { - std::string names; - for (auto const &name : driven) - names += (names.empty() ? "" : ", ") + name; - m_switch.status = format(STR_C("Slip with drives at its switches %s: the events of the name of a switch with + and - switch it, also in the map"), names.c_str()); - ui()->set_status(m_switch.status, false); - } - return; - } - if (driven.size() == 2) - { - m_switch.status = format(STR_C("Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map"), driven[0].c_str(), driven[1].c_str(), driven[0].c_str(), driven[0].c_str(), driven[1].c_str(), driven[1].c_str()); - ui()->set_status(m_switch.status, false); - } - advance_switch_name(); - return; - } - if (place_switch_drive(**found)) - join_last_steps(); - advance_switch_name(); -} - -void editor_mode::advance_switch_name() -{ - std::string name{m_switch.name}; - auto digits{name.size()}; - while (digits > 0 && std::isdigit(static_cast(name[digits - 1]))) - --digits; - if (digits < name.size() && name.size() - digits < 9) - name = name.substr(0, digits) + std::to_string(std::stoi(name.substr(digits)) + 1); - else - name.clear(); - std::snprintf(m_switch.name, sizeof(m_switch.name), "%s", name.c_str()); -} - -std::string editor_mode::switch_name_for_new() const -{ - if (m_switch.name[0] != '\0') - return m_switch.name; - for (int i = 1;; ++i) - { - auto const name{"z" + std::to_string(i)}; - if (simulation::Paths.find(name) == nullptr && simulation::Events.FindEvent(name + "+") == nullptr) - return name; - } -} - -void editor_mode::join_last_steps() -{ - if (m_history.size() < 2) - return; - auto last{std::move(m_history.back())}; - m_history.pop_back(); - auto &previous{m_history.back()}; - previous.instances.insert(previous.instances.end(), last.instances.begin(), last.instances.end()); -} - -bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style) -{ - auto const from{From}; - auto const to{To}; - std::string reason; - if (false == tracks_editable(Line.tracks, reason)) - { - m_switch.status = reason; - ui()->set_status(reason, true); - return false; - } - struct span - { - TTrack *track; - int path; - double from; - double to; - bool forward; - }; - std::vector spans; - for (std::size_t i = 0; i < Line.tracks.size(); ++i) - { - auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; - auto const a{Line.along(path.points[segment_data::point::start])}; - auto const b{Line.along(path.points[segment_data::point::end])}; - spans.push_back({Line.tracks[i], Line.paths[i], std::min(a, b), std::max(a, b), a < b}); - } - std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); - int first{-1}; - int last{-1}; - for (int i = 0; i < static_cast(spans.size()); ++i) - { - if (spans[i].to > from + 1e-3 && spans[i].from < to - 1e-3) - { - if (first < 0) - first = i; - last = i; - } - } - if (first < 0) - return false; - editor_track::chain chain; - for (int i = first; i <= last; ++i) - { - if (spans[i].track->eType != tt_Normal) - return false; - chain.tracks.push_back(spans[i].track); - chain.forward.push_back(spans[i].forward); - } - if (Style != nullptr) - *Style = spans[first].track; - auto const before{std::max(0.0, from - spans[first].from)}; - auto const after{std::max(0.0, spans[last].to - to)}; - bool const hasbefore{before > 0.01}; - bool const hasafter{after > 0.01}; - auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; - if (false == hasbefore && false == hasafter) - { - for (auto *track : chain.tracks) - editor_track::retire(*track); - Removed.insert(Removed.end(), chain.tracks.begin(), chain.tracks.end()); - return true; - } - auto const count{std::max(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))}; - std::size_t countbefore{0}; - if (hasbefore && hasafter) - countbefore = std::clamp(static_cast(std::round(count * before / (before + after))), 1, count - 1); - else if (hasbefore) - countbefore = count; - std::vector pieces; - auto const straight = [&](double const Start, double const End, std::size_t const Count) { - for (std::size_t i = 0; i < Count; ++i) - { - segment_data path; - path.points[segment_data::point::start] = point(Start + (End - Start) * i / Count); - path.points[segment_data::point::end] = point(Start + (End - Start) * (i + 1) / Count); - pieces.push_back(path); - } - }; - if (hasbefore) - straight(spans[first].from, from, countbefore); - if (hasafter) - straight(to, spans[last].to, count - countbefore); - for (auto *track : chain.tracks) - States.emplace_back(track, editor_track::capture(*track)); - auto const relaid{editor_track::relay(chain, pieces)}; - Created.insert(Created.end(), relaid.begin(), relaid.end()); - return true; -} - -bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector> &States, std::vector &Created, std::vector &Removed, std::string const &Name) -{ - auto const &shape{Shape}; - if (shape.length > Line.length - 0.02) - { - m_switch.status = format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length); - return false; - } - auto const from{switch_from(Line, shape.length, Along, Direction, Snap)}; - auto const to{from + shape.length}; - auto const origin{Direction > 0 ? from : to}; - TTrack *first{nullptr}; - if (false == cut_straight(Line, from, to, States, Created, Removed, &first)) - { - m_switch.status = STR_C("The switch would overlap another switch, move it onto plain track"); - return false; - } - auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; - glm::dvec2 const direction{Line.direction * static_cast(Direction)}; - auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; - auto const made{create_switch_set(shape, paths, Style != nullptr ? *Style : *first, Name)}; - Created.insert(Created.end(), made.begin(), made.end()); - return true; -} - -std::vector editor_mode::create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name) -{ - if (Shape.twofold() && Paths.size() >= 5) - { - auto firstshape{Shape}; - firstshape.length = glm::distance(Paths[0].points[segment_data::point::start], Paths[0].points[segment_data::point::end]); - auto secondshape{Shape}; - secondshape.length = Shape.length - Shape.stagger; - auto *one{editor_track::create_switch(firstshape, Paths, Style, Name)}; - auto *two{one != nullptr ? editor_track::create_switch(secondshape, {Paths[2], Paths[3]}, Style, Name + "b") : nullptr}; - auto *link{two != nullptr ? editor_track::create_path(Style, Paths[4]) : nullptr}; - if (link == nullptr) - { - m_switch.status = STR_C("The switch couldn't be created from this template"); - return {}; - } - editor_track::commit({one, two, link}); - record_double(*one, *two, Shape); - m_switch.status = format(STR_C("Double switch %s with its second switch %s inserted"), one->name().c_str(), two->name().c_str()); - return {one, two, link}; - } - auto *track{editor_track::create_switch(Shape, Paths, Style, Name)}; - if (track == nullptr) - { - m_switch.status = STR_C("The switch couldn't be created from this template"); - return {}; - } - editor_track::commit({track}); - record_switch(*track, Shape); - m_switch.status = STR_C("Switch ") + track->name() + STR_C(" inserted"); - return {track}; -} - void editor_mode::draw_build_overlay() const { screen_projection const projection; @@ -5788,1119 +1918,3 @@ void editor_mode::draw_build_overlay() const if (auto const *track{selected_track()}; track != nullptr) draw_free_ends(track, track->iCategoryFlag & 15, std::vector(m_track_drag.begin(), m_track_drag.end())); } - -std::vector editor_mode::detour_outline() const -{ - auto const &state{m_straights}; - std::vector result; - if (state.detour.empty()) - return result; - auto const &line{state.detour_line}; - glm::dvec2 const normal{-line.direction.y, line.direction.x}; - auto points{state.detour}; - if (points.size() < 4) - points.push_back(state.detour_mouse); - auto const offset{points.size() >= 2 ? glm::dot(plan_of(points[1] - line.start), normal) : 0.0}; - auto const place = [&](double const Along, double const Lateral) { - auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; - return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; - }; - for (std::size_t i = 0; i < points.size(); ++i) - result.push_back(place(line.along(points[i]), (i == 1 || i == 2) ? offset : 0.0)); - return result; -} - -void editor_mode::add_detour_point() -{ - auto &state{m_straights}; - if (state.detour.empty()) - { - auto const &line{current_straight()}; - if (line.tracks.empty()) - return; - state.detour_line = line; - } - state.detour.push_back(cursor_ground()); - if (state.detour.size() >= 4) - { - apply_detour(); - state.detour.clear(); - } -} - -void editor_mode::apply_detour() -{ - auto &state{m_straights}; - auto line{editor_track::find_straight(*state.detour_line.tracks.front(), state.tolerance)}; - if (line.tracks.empty()) - return; - glm::dvec2 const normal{-line.direction.y, line.direction.x}; - auto const offset{glm::dot(plan_of(state.detour[1] - line.start), normal)}; - std::array stations{line.along(state.detour[0]), line.along(state.detour[1]), line.along(state.detour[2]), line.along(state.detour[3])}; - if (stations[0] > stations[3]) - { - std::swap(stations[0], stations[3]); - std::swap(stations[1], stations[2]); - } - for (auto &station : stations) - station = std::clamp(station, 0.0, line.length); - stations[1] = std::clamp(stations[1], stations[0] + 1.0, stations[3] - 1.0); - stations[2] = std::clamp(stations[2], stations[1], stations[3] - 1.0); - if (std::abs(offset) < 0.01 || stations[3] - stations[0] < 3.0) - return; - - auto const radius{straight_tool_radius(line)}; - auto const slope{std::atan(std::abs(offset) / std::max(1.0, std::min(stations[1] - stations[0], stations[3] - stations[2])))}; - double speed{-1.0}; - for (auto const *track : line.tracks) - speed = std::max(speed, editor_track::velocity(*track)); - auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; - auto const reach{radius * std::tan(slope * 0.5) + transition * 0.5 + 5.0}; - auto const from{std::max(0.0, stations[0] - reach)}; - auto const to{std::min(line.length, stations[3] + reach)}; - - struct span - { - TTrack *track; - double from; - double to; - }; - std::vector spans; - for (std::size_t i = 0; i < line.tracks.size(); ++i) - { - auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; - auto const a{line.along(path.points[segment_data::point::start])}; - auto const b{line.along(path.points[segment_data::point::end])}; - spans.push_back({line.tracks[i], std::min(a, b), std::max(a, b)}); - } - std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); - int first{-1}; - int last{-1}; - for (int i = 0; i < static_cast(spans.size()); ++i) - { - if (spans[i].to > from && spans[i].from < to && spans[i].track->eType == tt_Normal) - { - if (first < 0) - first = i; - last = i; - } - else if (first >= 0 && spans[i].from < to) - { - if (spans[i].from > stations[3]) - break; - first = -1; - last = -1; - } - } - if (first < 0) - return; - - m_route.from = spans[first].track; - m_route.to = spans[last].track; - route_reset(); - if (m_route.chain.tracks.empty()) - return; - auto &design{m_route.design}; - auto const startalong{line.along(design.start)}; - auto const endalong{line.along(design.end)}; - auto const place = [&](double const Along, double const Lateral) { - auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; - return planar; - }; - alignment::vertex vertex; - vertex.radius = radius; - route_recommend(vertex); - if (false == state.auto_transitions) - { - vertex.transition_out = state.transition; - vertex.transition_in = vertex.transition_out; - } - design.vertices.clear(); - auto first_vertex{vertex}; - first_vertex.offset = std::max(0.1, stations[0] - startalong); - design.vertices.push_back(first_vertex); - if (stations[2] - stations[1] < 1.0) - { - auto middle{vertex}; - middle.position = place((stations[1] + stations[2]) * 0.5, offset); - design.vertices.push_back(middle); - } - else - { - auto second{vertex}; - second.position = place(stations[1], offset); - auto third{vertex}; - third.position = place(stations[2], offset); - design.vertices.push_back(second); - design.vertices.push_back(third); - } - auto last_vertex{vertex}; - last_vertex.offset = std::max(0.1, endalong - stations[3]); - design.vertices.push_back(last_vertex); - route_update(); - if (false == m_route.result.valid) - return; - route_apply(); - straight_refresh(); -} - -bool editor_mode::start_curve_fit(editor_track::straight const &Line, int const Handle) -{ - auto &state{m_straights}; - auto *before{editor_track::outside_neighbour(Line, false)}; - auto *after{editor_track::outside_neighbour(Line, true)}; - auto const curve_at = [&](TTrack *Neighbour, editor_track::curve &Curve) { - Curve = {}; - return Neighbour != nullptr && Neighbour->eType == tt_Normal && editor_track::find_curve(*Neighbour, state.tolerance, m_route.design.norms.gauge, Curve) && Curve.reversals == 0 && std::abs(Curve.turn) > 1e-4; - }; - state.fit_has_before = curve_at(before, state.fit_before); - state.fit_has_after = curve_at(after, state.fit_after); - if (false == state.fit_has_before && false == state.fit_has_after) - return false; - auto const side_follows = [&](bool const Curve, TTrack const *Neighbour, bool const Pivot) { return Curve || Neighbour == nullptr || Pivot; }; - if (false == side_follows(state.fit_has_before, before, Handle == 1) || false == side_follows(state.fit_has_after, after, Handle == 0)) - return false; - auto const own_end = [&](glm::dvec3 const &Point) -> TTrack * { - for (auto *track : Line.tracks) - if (editor_track::touches(*track, Point)) - return track; - return Line.tracks.empty() ? nullptr : Line.tracks.front(); - }; - auto const other_end = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; - m_route.from = state.fit_has_before ? other_end(state.fit_before, Line.start) : own_end(Line.start); - m_route.to = state.fit_has_after ? other_end(state.fit_after, Line.end) : own_end(Line.end); - route_reset(); - if (m_route.chain.tracks.empty()) - return false; - return true; -} - -void editor_mode::update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &End) -{ - auto &state{m_straights}; - auto &design{m_route.design}; - glm::dvec2 const point{Start.x, Start.z}; - glm::dvec2 const direction{glm::normalize(plan_of(End - Start))}; - glm::dvec2 const start{design.start.x, design.start.z}; - glm::dvec2 const end{design.end.x, design.end.z}; - auto const startdirection{glm::normalize(design.start_direction)}; - auto const enddirection{glm::normalize(design.end_direction)}; - auto const first{cross(point - start, direction) / cross(startdirection, direction)}; - auto const last{cross(end - point, direction) / cross(enddirection, direction)}; - auto const make = [&](editor_track::curve const &Curve, double const Offset) { - alignment::vertex vertex; - vertex.radius = Curve.radius > 0.0 ? std::round(Curve.radius) : 1000.0; - route_recommend(vertex); - if (Curve.transition_in > 0.0) - vertex.transition_in = std::round(Curve.transition_in); - if (Curve.transition_out > 0.0) - vertex.transition_out = std::round(Curve.transition_out); - if (Curve.cant > 0.0) - vertex.cant = std::round(Curve.cant); - compound_from(Curve, vertex); - vertex.offset = std::max(0.1, Offset); - return vertex; - }; - design.vertices.clear(); - if (state.fit_has_before) - design.vertices.push_back(make(state.fit_before, first)); - else - { - design.start = Start; - design.start_direction = direction; - design.start_reserve = 0.0; - } - if (state.fit_has_after) - design.vertices.push_back(make(state.fit_after, last)); - else - { - design.end = End; - design.end_direction = direction; - design.end_reserve = 0.0; - } - route_update(); -} - -bool editor_mode::plan_crossover(crossover_plan &Plan) const -{ - auto const &tool{m_extend}; - if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || m_track_layout == track_layout::straight) - return false; - auto const &own{*tool.track}; - auto const &main{own.m_paths[0]}; - auto const &diverging{own.m_paths[1]}; - auto const origin{main.points[segment_data::point::start]}; - auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))}; - glm::dvec2 const perpendicular{-axis.y, axis.x}; - auto const &frog{diverging.points[segment_data::point::end]}; - auto const lateral{glm::dot(plan_of(frog - origin), perpendicular)}; - if (std::abs(lateral) < 0.01) - return false; - auto const side{lateral > 0.0 ? 1.0 : -1.0}; - auto const &control{(diverging.pieces.empty() ? diverging : diverging.pieces.back()).points[segment_data::point::control2]}; - auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan_of(control) : plan_of(frog - origin))}; - auto const angle{std::atan2(std::abs(cross(axis, tangent)), glm::dot(axis, tangent))}; - if (angle < 1e-4) - return false; - - auto const ground{tool.mouse}; - TTrack *target{nullptr}; - double best{8.0}; - auto const sections{simulation::Region->sections(ground, 40.f)}; - for (auto *section : sections) - { - for (auto const &cell : section->m_cells) - { - for (auto *track : cell.m_paths) - { - if (track == &own || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) - continue; - auto const &path{track->m_paths.front()}; - auto const a{plan_of(path.points[segment_data::point::start])}; - auto const b{plan_of(path.points[segment_data::point::end])}; - auto const segment{b - a}; - auto const t{std::clamp(glm::dot(plan_of(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; - auto const distance{glm::distance(plan_of(ground), a + segment * t)}; - if (distance < best) - { - best = distance; - target = track; - } - } - } - } - if (target == nullptr) - return false; - Plan.target = editor_track::find_straight(*target, m_straights.tolerance); - auto const &line{Plan.target}; - if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0))) - return false; - editor_track::switch_template shape; - shape.source = tool.track; - shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis); - for (int i = 0; i < 2; ++i) - { - auto const &path{own.m_paths[i]}; - auto &local{shape.local[i]}; - local = path; - auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), perpendicular) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; }; - local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal); - if (side < 0.0) - { - local.rolls = {-local.rolls[0], -local.rolls[1]}; - for (auto &piece : local.pieces) - piece.rolls = {-piece.rolls[0], -piece.rolls[1]}; - } - } - Plan.shape = shape; - auto const faced{connection::face(frog, tangent, connection::of(line), 1, shape)}; - if (false == faced.valid) - return false; - Plan.along = faced.partner.along; - Plan.direction = faced.partner.forward; - Plan.side = faced.partner.side; - Plan.paths = faced.partner.paths; - Plan.insert = faced.insert; - return true; -} - -std::string editor_mode::link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const -{ - auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}}; - if (false == Link.valid) - return Link.shortfall > 0.0 ? format(STR_C("The branch can't lead into %s: the R %.0f m curve needs %.2f m more room"), name.c_str(), Link.radius, Link.shortfall) : - format(STR_C("The branch can't lead into %s"), name.c_str()); - if (Link.trim > 0.0 && (false == stretchable(Target) || geometry::chord_length(Target.track->m_paths[Target.point.path]) < Link.trim + 1.0)) - return format(STR_C("%s would have to be shortened by %.2f m, it's too short or not straight"), name.c_str(), Link.trim); - return {}; -} - -bool editor_mode::stretchable(editor_track::snap_target const &Target) const -{ - return Target.track != nullptr && Target.track->eType == tt_Normal && Target.track->m_paths.size() == 1 && editor_track::is_straight(*Target.track, m_straights.tolerance); -} - -bool editor_mode::plan_switch_link(switch_link &Plan) const -{ - auto const &tool{m_switch}; - if (false == tool.placing || tool.curved || tool.connection >= 0 || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size()) || tool.line.tracks.empty() || tool.templates[tool.armed].double_slip) - return false; - auto const &shape{tool.templates[tool.armed]}; - auto const &line{tool.line}; - std::vector const exclude(line.tracks.begin(), line.tracks.end()); - auto const category{line.tracks.front()->iCategoryFlag & 15}; - auto const orient = [&](glm::dvec3 const &Toward) { - glm::dvec2 const offset{plan_of(Toward - tool.point)}; - Plan.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1; - Plan.side = cross(line.direction * static_cast(Plan.forward), offset) >= 0.0 ? 1 : -1; - }; - auto const place = [&](double const Along) { - auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Along}; - return editor_track::place_switch(shape, point, line.direction * static_cast(Plan.forward), Plan.side, line.grade * Plan.forward); - }; - auto const &io{ImGui::GetIO()}; - if (false == m_cursor_override.has_value()) - Plan.target = snap_free_end(tool.mouse, {io.MousePos.x, io.MousePos.y}, nullptr, category, exclude); - if (Plan.target.track == nullptr) - Plan.target = editor_track::find_free_end(nullptr, category, tool.mouse, kLaySnapRadius, exclude); - if (Plan.target.track != nullptr) - orient(Plan.target.position); - else - { - orient(tool.mouse); - auto const paths{place(tool.along)}; - if (paths.size() < 2) - return false; - Plan.target = editor_track::find_free_end(nullptr, category, paths[1].points[segment_data::point::end], kLaySnapRadius, exclude); - if (Plan.target.track == nullptr) - return false; - } - if (shape.length > line.length - 0.02) - return false; - auto const kink{glm::radians(m_joints.angle * 0.5)}; - auto const plan = [&](double const Along) { - Plan.along = Along; - Plan.paths = place(Along); - if (Plan.paths.size() < 2) - return false; - auto const radius{editor_track::branch_radius(Plan.paths[1])}; - Plan.link = editor_track::link_branch(branch_start(Plan.paths[1]), radius, branch_target(Plan.target), kink, - editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(nullptr, Plan.target.track))), stretchable(Plan.target)); - Plan.reason = link_problem(Plan.target, Plan.link); - return Plan.reason.empty(); - }; - auto const from{switch_from(line, shape.length, tool.along, Plan.forward, true)}; - auto const snapped{Plan.forward > 0 ? from : from + shape.length}; - auto const paths{place(snapped)}; - if (paths.size() < 2) - return false; - if (auto const shift{editor_track::branch_shift(branch_start(paths[1]), line.direction * static_cast(Plan.forward), editor_track::branch_radius(paths[1]), branch_target(Plan.target), kink)}) - { - auto const along{snapped + *shift * Plan.forward}; - auto const begin{Plan.forward > 0 ? along : along - shape.length}; - Plan.moved = std::abs(*shift); - if (begin >= -1e-6 && begin + shape.length <= line.length + 1e-6 && plan(along) && false == Plan.link.reverse) - return true; - } - Plan.moved = 0.0; - return plan(snapped); -} - -void editor_mode::insert_linked_switch(switch_link const &Plan) -{ - std::vector> states; - std::vector created; - std::vector removed; - if (false == place_switch_on_straight(m_switch.line, m_switch.templates[m_switch.armed], nullptr, Plan.along, Plan.forward, Plan.side, false, states, created, removed, switch_name_for_new())) - return; - auto const placed{created}; - link_tracks(Plan.target, Plan.link, Plan.target.track->eType == tt_Normal ? *Plan.target.track : *m_switch.line.tracks.front(), states, created); - push_track_snapshot(std::move(states), std::move(created), std::move(removed)); - straight_refresh(); - m_switch.status.clear(); - switch_placed(placed); - auto summary{link_summary(Plan.target, Plan.link, editor_track::branch_radius(Plan.paths[1]))}; - if (Plan.moved >= 0.005) - summary += format(STR_C(", the switch moved %.2f m"), Plan.moved); - m_switch.status = summary + (m_switch.status.empty() ? std::string{} : "\n" + m_switch.status); - ui()->set_status(m_switch.status, false); -} - -bool editor_mode::plan_branch_link(editor_track::branch_link &Link, std::string &Reason) const -{ - auto const &tool{m_extend}; - editor_track::snap_target target; - Reason.clear(); - if (false == extend_snap(target) || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() != 2 || tool.path != 1 || false == tool.atend) - return false; - if (std::find(tool.line.begin(), tool.line.end(), target.track) != tool.line.end()) - return false; - auto const &diverging{tool.track->m_paths[1]}; - auto const radius{editor_track::branch_radius(diverging)}; - Link = editor_track::link_branch(branch_start(diverging), radius, branch_target(target), glm::radians(m_joints.angle * 0.5), - editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(tool.track, target.track))), stretchable(target)); - Reason = link_problem(target, Link); - return Reason.empty(); -} - -void editor_mode::finish_branch_link(editor_track::branch_link const &Link) -{ - auto *own{m_extend.track}; - auto const target{m_extend.snap}; - m_extend.active = false; - std::vector> states; - std::vector created; - auto const &style{target.track->eType == tt_Normal || m_extend.line.empty() ? *target.track : *m_extend.line.front()}; - auto const linked{link_tracks(target, Link, style, states, created)}; - push_track_snapshot(std::move(states), std::move(created)); - m_node = linked.empty() ? own : linked.back(); - ui()->set_node(m_node); - straight_refresh(); - ui()->set_status(link_summary(target, Link, editor_track::branch_radius(own->m_paths[1])), false); -} - -std::vector editor_mode::link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector> &States, std::vector &Created) -{ - auto *target{Target.track}; - if (Link.trim != 0.0) - { - States.emplace_back(target, editor_track::capture(*target)); - editor_track::move_point(*target, Target.point, Link.landing); - auto &path{target->m_paths[Target.point.path]}; - path.points[segment_data::point::control1] = glm::dvec3{0.0}; - path.points[segment_data::point::control2] = glm::dvec3{0.0}; - editor_track::commit({target}); - } - std::vector pieces; - for (auto const &piece : Link.pieces) - pieces.push_back(editor_track::create_path(Style, piece)); - editor_track::commit(pieces); - digitize_drawn(pieces); - Created.insert(Created.end(), pieces.begin(), pieces.end()); - return pieces; -} - -std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type) -{ - auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; }; - auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) { - segment_data path; - path.points[segment_data::point::start] = Start; - path.points[segment_data::point::end] = End; - return path; - }; - // the ends of the two diagonals at the crossing are kept apart further than the simulation joins the ends, 2 cm - auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))}; - auto const gap{std::max(0.25, 0.06 / halfsine)}; - auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0}; - auto const whole{Curve.length()}; - // the part of the slip curve from Start along Heading, turning by Turn to Finish, between the distances From and To along it; - // where it changes the radius made of pieces, with one curve from end to end for the versions which know no pieces - auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) { - auto const place = [&](double const Along) { - auto const local{Curve.point(Along)}; - auto const planar{plan_of(Start) + Heading * local.x + glm::dvec2{-Heading.y, Heading.x} * (Turn * local.y)}; - return glm::dvec3{planar.x, Start.y + (Finish.y - Start.y) * Along / whole, planar.y}; - }; - auto const bend = [&](double const Begin, double const End, double const Radius, double const Handle0, double const Handle1) { - auto const d0{turned(Heading, Turn * Curve.heading(Begin))}; - auto const d3{turned(Heading, Turn * Curve.heading(End))}; - segment_data path; - path.points[segment_data::point::start] = place(Begin); - path.points[segment_data::point::end] = place(End); - if (Radius > 0.0) - { - path.points[segment_data::point::control1] = {d0.x * Handle0, 0.0, d0.y * Handle0}; - path.points[segment_data::point::control2] = {-d3.x * Handle1, 0.0, -d3.y * Handle1}; - } - path.radius = static_cast(Radius); - return path; - }; - std::vector> zones; - for (auto const &zone : {std::pair{Curve.lead, 0.0}, {Curve.lead + Curve.ends, Curve.radius}, {Curve.lead + Curve.ends + Curve.between, Curve.middle}, {whole - Curve.lead, Curve.radius}, {whole, 0.0}}) - { - if (zone.first <= (zones.empty() ? 0.0 : zones.back().first) + 1e-9) - continue; - if (false == zones.empty() && zone.second == zones.back().second) - zones.back().first = zone.first; - else - zones.push_back(zone); - } - // broken at the tips of the blades of the tables, so the model of the switch finds them there - if (Curve.tip > 0.0) - for (auto const cut : {Curve.tip, whole - Curve.tip}) - for (std::size_t i = 0; i < zones.size(); ++i) - if (cut > (i > 0 ? zones[i - 1].first : 0.0) + 0.05 && cut < zones[i].first - 0.05) - { - zones.insert(zones.begin() + i, {cut, zones[i].second}); - break; - } - std::vector pieces; - auto from{From}; - for (auto const &[to, radius] : zones) - { - auto const upto{std::min(To, to)}; - if (upto <= from + 1e-6) - continue; - auto const handle{radius > 0.0 ? 4.0 / 3.0 * std::tan((upto - from) / radius / 4.0) * radius : 0.0}; - pieces.push_back(bend(from, upto, radius, handle, handle)); - from = upto; - } - if (pieces.size() == 1) - return pieces.front(); - // the handles of the curve from end to end reach 2/3 of the way to the crossing of the tangents - auto const chord{plan_of(place(To) - place(From))}; - auto const d0{turned(Heading, Turn * Curve.heading(From))}; - auto const d3{turned(Heading, Turn * Curve.heading(To))}; - auto const sine{cross(d0, d3)}; - auto path{bend(From, To, Curve.radius, 2.0 / 3.0 * cross(chord, d3) / sine, 2.0 / 3.0 * cross(d0, chord) / sine)}; - path.pieces = std::move(pieces); - return path; - }; - auto const back = [](segment_data const &Path) { - auto result{reversed(Path)}; - std::reverse(result.pieces.begin(), result.pieces.end()); - for (auto &piece : result.pieces) - piece = reversed(piece); - return result; - }; - auto const middle{whole * 0.5}; - auto const split{gap}; - auto const centreA{at(Crossing - First * gap, Height)}; - auto const centreB{at(Crossing + First * gap, Height)}; - auto const centreC{at(Crossing - Second * gap, Height)}; - auto const centreD{at(Crossing + Second * gap, Height)}; - std::string base; - auto const prefix{Type == "Skrz" ? "editor_skrz" : Type == "Rkp" ? "editor_dkp" : "editor_dks"}; - // a crossing keeps the names of its tracks, only its record holds the name - auto const recorded = [](std::string const &Name) { - auto const &records{switch_store::records()}; - return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; }); - }; - for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || recorded(base); ++i) - base = prefix + std::to_string(i); - std::vector parts; - if (Type == "Skrz") - { - // the two tracks just cross, their ends far from the crossing - parts.push_back(editor_track::create_path(Style, straight(PointA, PointB))); - parts.push_back(editor_track::create_path(Style, straight(PointC, PointD))); - parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); - record_slip(parts, "crossing", Type, 0.0, Angle, base); - return parts; - } - editor_track::switch_template shape; - shape.length = glm::distance(plan_of(PointA), Crossing); - bool const single{Type == "Rkp"}; - parts.push_back(editor_track::create_switch(shape, {straight(PointA, centreA), arc(PointA, First, side, PointD, 0.0, middle - split)}, Style, base + "_a")); - if (single) - parts.push_back(editor_track::create_path(Style, straight(PointB, centreB))); - else - parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), back(arc(PointC, Second, -side, PointB, middle + split, whole))}, Style, base + "_b")); - if (single) - parts.push_back(editor_track::create_path(Style, straight(PointC, centreC))); - else - parts.push_back(editor_track::create_switch(shape, {straight(PointC, centreC), arc(PointC, Second, -side, PointB, 0.0, middle - split)}, Style, base + "_c")); - parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), back(arc(PointA, First, side, PointD, middle + split, whole))}, Style, base + "_d")); - parts.push_back(editor_track::create_path(Style, straight(centreA, centreB))); - parts.push_back(editor_track::create_path(Style, straight(centreC, centreD))); - parts.push_back(editor_track::create_path(Style, arc(PointA, First, side, PointD, middle - split, middle + split))); - if (false == single) - parts.push_back(editor_track::create_path(Style, arc(PointC, Second, -side, PointB, middle - split, middle + split))); - parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); - record_slip(parts, "slip", Type, Curve.radius, Angle, base, Curve.middle); - return parts; -} - -bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape) -{ - auto &state{m_straights}; - state.status.clear(); - { - auto const sections{simulation::Region->sections(Point, 40.f)}; - for (auto *section : sections) - for (auto const &cell : section->m_cells) - for (auto *track : cell.m_paths) - if (Shape.type == "Rkpd" && track->eType == tt_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan_of(track->location()), plan_of(Point)) < 25.0) - return replace_double_slip(*track, Shape); - } - TTrack *other{nullptr}; - glm::dvec2 crossing{0.0}; - double best{40.0}; - auto const sections{simulation::Region->sections(Point, 60.f)}; - for (auto *section : sections) - { - for (auto const &cell : section->m_cells) - { - for (auto *track : cell.m_paths) - { - if (std::find(Line.tracks.begin(), Line.tracks.end(), track) != Line.tracks.end() || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) - continue; - auto const &path{track->m_paths.front()}; - auto const a{plan_of(path.points[segment_data::point::start])}; - auto const b{plan_of(path.points[segment_data::point::end])}; - auto const segment{b - a}; - auto const determinant{cross(Line.direction, segment)}; - if (std::abs(determinant) < 1e-9) - continue; - auto const offset{a - plan_of(Line.start)}; - auto const along{cross(offset, segment) / determinant}; - auto const fraction{cross(offset, Line.direction) / determinant}; - if (fraction < -0.01 || fraction > 1.01 || along < 0.0 || along > Line.length) - continue; - auto const position{plan_of(Line.start) + Line.direction * along}; - auto const distance{glm::distance(position, plan_of(Point))}; - if (distance < best) - { - best = distance; - other = track; - crossing = position; - } - } - } - } - if (other == nullptr) - { - state.status = STR_C("No straight crossing the selected one near the click"); - return false; - } - auto const second{editor_track::find_straight(*other, m_straights.tolerance)}; - if (second.tracks.empty()) - return false; - auto const first{Line.direction}; - auto direction{second.direction}; - if (glm::dot(first, direction) < 0.0) - direction = -direction; - auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; - bool const plain{Shape.type == "Skrz"}; - if (angle < glm::radians(0.5) || angle > glm::radians(plain ? 89.0 : 30.0)) - { - state.status = STR_C("The straights cross at an angle unsuitable for a double slip"); - return false; - } - auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)}; - auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)}; - auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5}; - auto const *gauged{Line.tracks.front()}; - auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; - auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)}; - curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); - auto half{plain ? Shape.a : curve.tangent()}; - if (plain && half > available) - { - half = available; - if (half < 2.0) - { - state.status = STR_C("Not enough straight track around the crossing"); - return false; - } - } - else if (half > available) - { - curve = curve.scaled(available / half); - half = available; - if (curve.radius < 50.0) - { - state.status = STR_C("Not enough straight track around the crossing"); - return false; - } - state.status = STR_C("R reduced to ") + std::to_string(static_cast(curve.radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast(available)) + STR_C(" m from the crossing"); - } - - auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; - auto const heightsecond = [&](double const Along) { return second.start.y + second.grade * Along; }; - auto const sign{glm::dot(second.direction, direction) > 0.0 ? 1.0 : -1.0}; - auto const at = [&](glm::dvec2 const &Planar, double const Height) { return glm::dvec3{Planar.x, Height, Planar.y}; }; - auto const pointA{at(crossing - first * half, heightfirst(alongfirst - half))}; - auto const pointB{at(crossing + first * half, heightfirst(alongfirst + half))}; - auto const pointC{at(crossing - direction * half, heightsecond(alongsecond - sign * half))}; - auto const pointD{at(crossing + direction * half, heightsecond(alongsecond + sign * half))}; - - std::vector> states; - std::vector created; - std::vector removed; - TTrack *style{nullptr}; - if (false == cut_straight(Line, alongfirst - half, alongfirst + half, states, created, removed, &style)) - return false; - auto const secondline{editor_track::find_straight(*other, m_straights.tolerance)}; - auto const secondalong{glm::dot(crossing - plan_of(secondline.start), secondline.direction)}; - if (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr)) - { - for (auto *track : created) - editor_track::retire(*track); - for (auto const &entry : states) - { - editor_track::apply(*entry.first, entry.second); - editor_track::commit({entry.first}); - } - for (auto *track : removed) - { - track->m_editorremoved = false; - editor_track::commit({track}); - } - return false; - } - auto &styled{style != nullptr ? *style : *Line.tracks.front()}; - // a single slip joins the pair of the tracks on the side of the click - auto const nearer{glm::distance((plan_of(pointA) + plan_of(pointD)) * 0.5, plan_of(Point)) <= glm::distance((plan_of(pointC) + plan_of(pointB)) * 0.5, plan_of(Point))}; - auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, curve, styled, Shape.type) - : build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, curve, styled, Shape.type)}; - editor_track::commit(parts); - created.insert(created.end(), parts.begin(), parts.end()); - push_track_snapshot(std::move(states), std::move(created), std::move(removed)); - straight_refresh(); - switch_placed(parts); - return true; -} - -bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape) -{ - auto &state{m_straights}; - auto const base{Part.name().substr(0, Part.name().size() - 2)}; - std::array switches{}; - for (int i = 0; i < 4; ++i) - { - switches[i] = simulation::Paths.find(base + "_" + static_cast('a' + i)); - if (switches[i] == nullptr || switches[i]->m_editorremoved || switches[i]->eType != tt_Switch || switches[i]->m_paths.size() < 2) - { - state.status = STR_C("The double slip isn't complete, it can't be replaced"); - return false; - } - } - struct end - { - TTrack *track; - glm::dvec3 point; - glm::dvec2 inward; - TTrack *outer; - }; - std::vector ends; - std::vector parts(switches.begin(), switches.end()); - for (auto *track : switches) - { - segment_data const *straight{nullptr}; - for (auto const &path : track->m_paths) - if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) - straight = &path; - if (straight == nullptr) - straight = &track->m_paths.front(); - auto const start{straight->points[segment_data::point::start]}; - ends.push_back({track, start, glm::normalize(plan_of(straight->points[segment_data::point::end] - start)), track->SwitchExtension->pPrevs[0]}); - for (int i = 0; i < 2; ++i) - { - auto *inner{track->SwitchExtension->pNexts[i]}; - if (inner != nullptr && inner->eType == tt_Normal && std::find(parts.begin(), parts.end(), inner) == parts.end() && inner->Length() < 5.0) - parts.push_back(inner); - } - } - int partner{-1}; - for (int i = 1; i < 4; ++i) - if (glm::dot(ends[0].inward, ends[i].inward) < -0.999) - partner = i; - if (partner < 0) - { - state.status = STR_C("The double slip geometry isn't recognized"); - return false; - } - auto const &a{ends[0]}; - auto const &b{ends[partner]}; - std::vector others; - for (int i = 1; i < 4; ++i) - if (i != partner) - others.push_back(i); - if (glm::dot(ends[0].inward, ends[others[0]].inward) < 0.0) - std::swap(others[0], others[1]); - auto const &c{ends[others[0]]}; - auto const &d{ends[others[1]]}; - auto const first{a.inward}; - auto direction{c.inward}; - auto const determinant{cross(first, direction)}; - if (std::abs(determinant) < 1e-9) - return false; - auto const t{cross(plan_of(c.point) - plan_of(a.point), direction) / determinant}; - glm::dvec2 const crossing{plan_of(a.point) + first * t}; - auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; - auto const oldhalf{glm::distance(plan_of(a.point), crossing)}; - auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; - auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(switches[0]->fTrackWidth) + switches[0]->rail_dimensions().head + 0.002, blade.first, blade.second)}; - curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); - auto const full{curve.tangent()}; - auto radius{curve.radius}; - auto half{full}; - auto const height{(a.point.y + b.point.y) * 0.5}; - - std::vector> states; - std::vector created; - std::vector removed; - if (half > oldhalf) - { - for (auto const *e : {&a, &b, &c, &d}) - { - if (e->outer == nullptr || false == editor_track::is_straight(*e->outer, m_straights.tolerance)) - { - half = oldhalf; - radius = curve.radius * half / full; - state.status = STR_C("R limited to ") + std::to_string(static_cast(radius)) + STR_C(" m, a neighbouring path isn't straight"); - break; - } - auto const line{editor_track::find_straight(*e->outer, m_straights.tolerance)}; - auto const reach{std::abs(glm::dot(plan_of(line.end) - crossing, -e->inward))}; - auto const reachstart{std::abs(glm::dot(plan_of(line.start) - crossing, -e->inward))}; - if (std::max(reach, reachstart) - 0.5 < half) - { - half = std::min(half, std::max(reach, reachstart) - 0.5); - radius = curve.radius * half / full; - state.status = STR_C("R reduced to ") + std::to_string(static_cast(radius)) + STR_C(" m to fit the straights"); - } - } - } - for (auto *track : parts) - editor_track::retire(*track); - removed.insert(removed.end(), parts.begin(), parts.end()); - TTrack *style{a.outer != nullptr && a.outer->eType == tt_Normal ? a.outer : switches[0]}; - std::array points; - std::array order{&a, &b, &c, &d}; - for (int i = 0; i < 4; ++i) - { - auto const &e{*order[i]}; - auto const outward{-e.inward}; - auto const target{crossing + outward * half}; - points[i] = {target.x, e.point.y, target.y}; - if (half < oldhalf - 0.01) - { - auto *filler{editor_track::create_path(*style, [&] { - segment_data path; - path.points[segment_data::point::start] = points[i]; - path.points[segment_data::point::end] = e.point; - return path; - }())}; - created.push_back(filler); - } - else if (half > oldhalf + 0.01 && e.outer != nullptr) - { - auto const line{editor_track::find_straight(*e.outer, m_straights.tolerance)}; - auto const alongold{glm::dot(plan_of(e.point) - plan_of(line.start), line.direction)}; - auto const alongnew{glm::dot(target - plan_of(line.start), line.direction)}; - cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr); - } - } - auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, curve.scaled(half / full), *style)}; - std::vector commitlist(parts2.begin(), parts2.end()); - for (auto *track : created) - if (std::find(commitlist.begin(), commitlist.end(), track) == commitlist.end()) - commitlist.push_back(track); - editor_track::commit(commitlist); - created.insert(created.end(), parts2.begin(), parts2.end()); - push_track_snapshot(std::move(states), std::move(created), std::move(removed)); - straight_refresh(); - // the drives of the switches replaced go with them, the new ones get theirs - std::vector drives; - for (auto const *track : switches) - if (auto const drive{find_switch_drive(*track)}) - drives.push_back(drive->standing.instance); - if (false == drives.empty() && remove_templates(drives) > 0) - join_last_steps(); - for (auto *track : parts2) - if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track)) - join_last_steps(); - return true; -} - -std::vector editor_mode::sample_chain(editor_track::chain const &Chain) -{ - std::vector result; - double station{0.0}; - for (std::size_t i = 0; i < Chain.tracks.size(); ++i) - { - auto const &path{Chain.tracks[i]->m_paths.front()}; - auto const forward{Chain.forward[i]}; - bezier const curve{path}; - auto const length{glm::distance(curve.p0, curve.p3)}; - int const steps{std::max(8, static_cast(std::ceil(length / 0.5)))}; - for (int k = (result.empty() ? 0 : 1); k <= steps; ++k) - { - auto const t{forward ? static_cast(k) / steps : 1.0 - static_cast(k) / steps}; - auto const position{curve.point(t)}; - auto const derivative{forward ? curve.first(t) : -curve.first(t)}; - if (false == result.empty()) - station += plan_distance(position, result.back().position); - curve_sample sample; - sample.station = station; - sample.position = position; - sample.tangent = glm::normalize(plan_of(derivative)); - auto const roll{path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t}; - sample.roll = static_cast(forward ? roll : -roll); - sample.radius = path.radius; - result.push_back(sample); - } - } - return result; -} - -editor_mode::curve_sample editor_mode::sample_at(std::vector const &Frame, double const Station) -{ - if (Frame.empty()) - return {}; - if (Station <= Frame.front().station) - return Frame.front(); - if (Station >= Frame.back().station) - return Frame.back(); - auto const next{std::lower_bound(Frame.begin(), Frame.end(), Station, [](curve_sample const &Sample, double const Value) { return Sample.station < Value; })}; - auto const previous{std::prev(next)}; - auto const span{std::max(1e-9, next->station - previous->station)}; - auto const f{(Station - previous->station) / span}; - curve_sample result; - result.station = Station; - result.position = glm::mix(previous->position, next->position, f); - result.tangent = glm::normalize(glm::mix(previous->tangent, next->tangent, f)); - result.roll = static_cast(previous->roll + (next->roll - previous->roll) * f); - result.radius = previous->radius; - return result; -} - -double editor_mode::nearest_station(std::vector const &Frame, glm::dvec3 const &Point) -{ - double best{std::numeric_limits::max()}; - double station{0.0}; - for (std::size_t i = 0; i + 1 < Frame.size(); ++i) - { - glm::dvec2 const a{Frame[i].position.x, Frame[i].position.z}; - glm::dvec2 const b{Frame[i + 1].position.x, Frame[i + 1].position.z}; - auto const segment{b - a}; - auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)}; - auto const distance{glm::distance(plan_of(Point), a + segment * t)}; - if (distance < best) - { - best = distance; - station = Frame[i].station + (Frame[i + 1].station - Frame[i].station) * t; - } - } - return station; -} - -std::vector editor_mode::curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side) -{ - auto const map = [&](glm::dvec3 const &Local, glm::dvec2 &Tangent, glm::dvec2 const &Localtangent) { - auto const sample{sample_at(Frame, Station + Direction * Local.z)}; - auto const along{sample.tangent * static_cast(Direction)}; - glm::dvec2 const across{glm::dvec2{-along.y, along.x} * static_cast(Side)}; - Tangent = glm::normalize(along * Localtangent.y + across * Localtangent.x); - auto const planar{plan_of(sample.position) + across * Local.x}; - auto const cant{glm::radians(static_cast(sample.roll) * Direction)}; - auto const cone{-Local.x * Side * std::tan(cant)}; - return glm::dvec3{planar.x, sample.position.y + Local.y + cone, planar.y}; - }; - auto const hermite = [](glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection) { - auto const handle{plan_distance(Start, End) / 3.0}; - segment_data path; - path.points[segment_data::point::start] = Start; - path.points[segment_data::point::end] = End; - path.points[segment_data::point::control1] = {Startdirection.x * handle, (End.y - Start.y) / 3.0, Startdirection.y * handle}; - path.points[segment_data::point::control2] = {-Enddirection.x * handle, -(End.y - Start.y) / 3.0, -Enddirection.y * handle}; - return path; - }; - auto const bend = [&](segment_data const &Local) { - auto const &start{Local.points[segment_data::point::start]}; - auto const &end{Local.points[segment_data::point::end]}; - auto const &control1{Local.points[segment_data::point::control1]}; - auto const &control2{Local.points[segment_data::point::control2]}; - glm::dvec2 const chord{end.x - start.x, end.z - start.z}; - auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)}; - auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; - glm::dvec2 startdirection; - glm::dvec2 enddirection; - auto const p0{map(start, startdirection, localstart)}; - auto const p3{map(end, enddirection, localend)}; - auto path{hermite(p0, startdirection, p3, enddirection)}; - { - auto const cone = [&](double const T) { - auto const point{bezier{path}.point(T)}; - auto const sample{sample_at(Frame, nearest_station(Frame, point))}; - auto const lateral{glm::dot(plan_of(point) - plan_of(sample.position), glm::dvec2{-sample.tangent.y, sample.tangent.x})}; - return sample.position.y - lateral * std::tan(glm::radians(static_cast(sample.roll))); - }; - auto const h1{cone(1.0 / 3.0)}; - auto const h2{cone(2.0 / 3.0)}; - auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * p0.y + p3.y / 3.0}; - auto const y2{3.0 * h2 - 1.5 * h1 + p0.y / 3.0 - 5.0 / 6.0 * p3.y}; - path.points[segment_data::point::control1].y = y1 - p0.y; - path.points[segment_data::point::control2].y = y2 - p3.y; - } - auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction}; - auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction}; - path.rolls = {static_cast(rollstart), static_cast(rollend)}; - path.radius = Local.radius; - { - bezier const curve{path}; - auto const begin{plan_of(curve.first(0.0))}; - auto const finish{plan_of(curve.first(1.0))}; - auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; - if (Local.radius != 0.f && std::abs(turn) > 1e-6) - path.radius = static_cast(std::copysign(curve.plan_length() / std::abs(turn), static_cast(Local.radius))); - } - return path; - }; - std::vector locals{Shape.local.begin(), Shape.local.end()}; - if (Shape.twofold()) - { - locals.insert(locals.end(), Shape.second.begin(), Shape.second.end()); - locals.push_back(Shape.link); - } - std::vector result; - for (std::size_t i = 0; i < locals.size(); ++i) - { - auto const &local{locals[i]}; - auto path{bend(local)}; - for (auto const &piece : local.pieces) - path.pieces.push_back(bend(piece)); - // the main tracks follow the curve they are laid into - if (i % 2 == 0) - path.radius = sample_at(Frame, Station + Direction * 0.5 * (local.points[segment_data::point::start].z + local.points[segment_data::point::end].z)).radius; - result.push_back(path); - } - return result; -} - -void editor_mode::insert_curved_switch(int const Direction, int const Side) -{ - auto &tool{m_switch}; - auto const &shape{tool.templates[tool.armed]}; - auto const total{tool.frame.back().station}; - if (shape.length > total - 0.02) - { - tool.status = format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length); - return; - } - auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; - auto const from{Direction > 0 ? station : station - shape.length}; - auto const to{from + shape.length}; - std::vector> states; - std::vector created; - std::vector removed; - std::vector candidates{tool.frame_tracks}; - for (auto const cut : {from, to}) - { - auto const point{sample_at(tool.frame, cut).position}; - for (auto *candidate : std::vector(candidates)) - { - if (candidate->m_editorremoved || candidate->eType != tt_Normal) - continue; - auto const t{editor_track::nearest_parameter(*candidate, point)}; - auto const found{editor_track::point_at(*candidate, t)}; - if (plan_distance(found, point) > 0.05) - continue; - if (t > 0.002 && t < 0.998) - { - if (std::find(created.begin(), created.end(), candidate) == created.end() && std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == candidate; })) - states.emplace_back(candidate, editor_track::capture(*candidate)); - if (auto *second{editor_track::split_path(*candidate, t)}) - { - created.push_back(second); - candidates.push_back(second); - } - } - break; - } - } - for (auto *candidate : candidates) - { - if (candidate->m_editorremoved) - continue; - auto const middle{editor_track::point_at(*candidate, 0.5)}; - auto const where{nearest_station(tool.frame, middle)}; - if (where > from + 0.01 && where < to - 0.01) - { - editor_track::retire(*candidate); - removed.push_back(candidate); - } - } - auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; - auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; - created.insert(created.end(), made.begin(), made.end()); - if (made.size() == 1) - tool.status = STR_C("Switch ") + made.front()->name() + STR_C(" inserted into the curve"); - auto const placed{created}; - push_track_snapshot(std::move(states), std::move(created), std::move(removed)); - straight_refresh(); - switch_placed(placed); -} diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h new file mode 100644 index 00000000..183a9664 --- /dev/null +++ b/application/editormodetrackshared.h @@ -0,0 +1,233 @@ +/* +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 + +// what the files of the track mode of the editor share: the sizes of the handles and reaches, and small helpers + +#include "editor/editorAlignment.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorTrack.hpp" +#include "utilities/translation.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include +#include +#include + +namespace track_mode +{ + +using geometry::bezier; +using geometry::cross; +using geometry::grade_of; +using geometry::plan_distance; +using geometry::plan_of; +using geometry::signed_angle; +using geometry::turned; +using geometry::arc_pieces; +using geometry::arc_through; +using geometry::has_controls; +using geometry::level; +using geometry::reversed; +using geometry::straight_piece; + +double constexpr kParallelFindAngle{15.0}; // deg, neighbours counted as beside the straight +double constexpr kParallelSnapAngle{5.0}; // deg, close enough to snap parallel +double constexpr kParallelSnapWindow{0.45}; // m from 4.00 / 4.50 / 4.75 m +double constexpr kParallelHintWindow{1.25}; // m, hint before the snap takes + +inline double piece_plan_radius(segment_data const &Path) +{ + if (std::abs(Path.radius) > 0.5f) + return std::abs(Path.radius); + bezier const curve{Path}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + if (glm::length(begin) < 1e-9 || glm::length(finish) < 1e-9) + return 0.0; + auto const turn{std::abs(signed_angle(begin, finish))}; + if (turn < 1e-4) + return 0.0; + return curve.plan_length() / turn; +} + +inline std::string lines_of(std::vector const &Texts) +{ + std::string text; + for (auto const &line : Texts) + text += (text.empty() ? "" : "\n") + line; + return text; +} + +inline std::string narrowed(std::vector const &Pieces, editor_track::layout const Layout, double const Radius) +{ + if (Layout != editor_track::layout::arc_straight && Layout != editor_track::layout::straight_arc) + return {}; + double sharpest{0.0}; + for (auto const &piece : Pieces) + if (auto const radius{piece_plan_radius(piece)}; radius >= 0.5 && (sharpest == 0.0 || radius < sharpest)) + sharpest = radius; + if (sharpest == 0.0 || sharpest >= Radius * 0.99) + return {}; + return format(STR_C("R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m"), Radius, sharpest); +} + +inline std::string describe(editor_track::straight const &Line) +{ + return format("L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); +} + +inline char const *point_label(editor_track::point_kind const Kind) +{ + switch (Kind) + { + case editor_track::point_kind::start: return STR_C("Start"); + case editor_track::point_kind::control1: return STR_C("Control 1"); + case editor_track::point_kind::control2: return STR_C("Control 2"); + default: return STR_C("End"); + } +} + +editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, editor_track::point_kind::control1, editor_track::point_kind::control2, editor_track::point_kind::end}; + +float const kHandleRadius{10.0f}; +double const kLaySnapRadius{5.0}; +float const kSnapPixels{16.0f}; +double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch +double const kLineReach{2000.0}; // how far each way the Curve tab follows the line +double const kDigitizeFit{0.5}; +double const kDrawnReach{100000.0}; +double const kJointRange{400.0}; // the ends of the paths are drawn for the tracks this close to the camera +ImU32 const kJointColor{IM_COL32(255, 255, 255, 200)}; +int const kRailCategory{1}; + +inline editor_track::branch_end branch_start(segment_data const &Diverging) +{ + auto const tangent{bezier{Diverging}.first(1.0)}; + return {Diverging.points[segment_data::point::end], plan_of(tangent), grade_of(tangent), static_cast(Diverging.rolls[1])}; +} + +inline editor_track::branch_end branch_target(editor_track::snap_target const &Target) +{ + return {Target.position, plan_of(Target.direction), grade_of(Target.direction), Target.track != nullptr ? editor_track::joint_roll(*Target.track, Target.point) : 0.0}; +} + +inline double switch_from(editor_track::straight const &Line, double const Length, double const Along, int const Direction, bool const Snap) +{ + auto from{std::clamp(Direction > 0 ? Along : Along - Length, 0.0, Line.length - Length)}; + if (false == Snap) + return from; + std::vector joints{0.0, Line.length}; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + joints.push_back(Line.along(path.points[index])); + } + auto const nearest = [&](double const Value) { + double best{Value}; + for (auto const joint : joints) + if (std::abs(joint - Value) < std::abs(best - Value) || best == Value) + best = std::abs(joint - Value) < 1.0 ? joint : best; + return best; + }; + auto const snappedstart{nearest(from)}; + auto const snappedend{nearest(from + Length)}; + if (snappedstart != from) + from = snappedstart; + else if (snappedend != from + Length) + from = snappedend - Length; + return std::clamp(from, 0.0, Line.length - Length); +} + +inline std::string link_summary(editor_track::snap_target const &Target, editor_track::branch_link const &Link, double const Switchradius) +{ + auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}}; + auto text{Link.reverse ? format(STR_C("The branch leads into %s with reverse curves R %.0f m"), name.c_str(), Link.radius) : + Link.radius > 0.0 ? format(STR_C("The branch leads into %s with an R %.0f m curve"), name.c_str(), Link.radius) : format(STR_C("The branch leads straight into %s"), name.c_str())}; + if (Link.insert > 0.0) + text += format(STR_C(", a straight of %.1f m between them"), Link.insert); + if (Link.reverse && Link.radius < Switchradius - 0.5) + text += format(STR_C(", sharper than the switch (R %.0f m)"), Switchradius); + if (Link.trim > 0.0) + text += format(STR_C(", %s shortened %.2f m"), name.c_str(), Link.trim); + else if (Link.trim < 0.0) + text += format(STR_C(", %s lengthened %.2f m"), name.c_str(), -Link.trim); + return text; +} + +inline std::optional eased_join(alignment::join_request const &Request, editor_track::easing const &In, editor_track::easing const &Out) +{ + if (In.pieces.empty() && Out.pieces.empty()) + return std::nullopt; + auto middle{Request}; + if (false == In.pieces.empty()) + { + middle.start.position = In.end; + middle.start.direction = In.direction; + middle.start.roll = In.pieces.back().rolls[1]; + } + if (false == Out.pieces.empty()) + { + middle.end.position = Out.end; + middle.end.direction = -Out.direction; + middle.end.roll = -Out.pieces.back().rolls[1]; + } + if (false == editor_track::eases_room(In, Out, Request.start.position, Request.start.direction, Request.end.position, Request.end.direction)) + return std::nullopt; + auto eased{alignment::join(middle)}; + if (eased.kind == alignment::join_kind::none || eased.kind == alignment::join_kind::bezier) + return std::nullopt; + eased.length = editor_track::eased_length(In, eased.length, Out); + eased.pieces = editor_track::eased_pieces(In, eased.pieces, Out); + return eased; +} + +// curvature of the shape at Chainage, positive turning from x towards z +inline 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 +inline void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex) +{ + if (false == Curve.compound || Curve.arcs.size() < 2) + return; + Vertex.compound = true; + Vertex.radius = std::round(Curve.arcs.front().radius); + Vertex.share = Curve.arcs.front().turn; + Vertex.arcs.clear(); + for (std::size_t i = 1; i < Curve.arcs.size(); ++i) + Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); +} + +inline char const *type_name(TTrack const &Track) +{ + switch (Track.eType) + { + case tt_Normal: return "normal"; + case tt_Switch: return "switch"; + case tt_Cross: return "cross"; + case tt_Table: return "turntable"; + case tt_Tributary: return "tributary"; + default: return "unknown"; + } +} + +} // namespace track_mode