/* 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 "utilities/Globals.h" #include "rendering/renderer.h" #include "world/Track.h" #include "scene/scene.h" #include "simulation/simulation.h" #include "utilities/Logs.h" #include "imgui/imgui.h" #include "imgui/ImGuizmo.h" #include #include #include #include #include #include namespace { class screen_projection { public: screen_projection() { ImGuiIO const &io = ImGui::GetIO(); m_size = io.DisplaySize; m_camera = GfxRenderer->Camera_Position(); float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); } glm::vec4 clip(glm::dvec3 const &Point) const { return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); } ImVec2 screen(glm::vec4 const &Clip) const { return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); } bool project(glm::dvec3 const &Point, ImVec2 &Screen) const { auto const c = clip(Point); if (c.w < kNear) return false; Screen = screen(c); return true; } void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const { glm::vec4 a = clip(A); glm::vec4 b = clip(B); if (a.w < kNear && b.w < kNear) return; if (a.w < kNear) a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); else if (b.w < kNear) b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); Drawlist->AddLine(screen(a), screen(b), Color, Thickness); } private: static constexpr float kNear{0.1f}; ImVec2 m_size; glm::dvec3 m_camera; glm::mat4 m_viewprojection; }; glm::dvec3 path_point(segment_data const &Path, double const T) { auto const &p0 = Path.points[segment_data::point::start]; auto const &p3 = Path.points[segment_data::point::end]; auto const &cp1 = Path.points[segment_data::point::control1]; auto const &cp2 = Path.points[segment_data::point::control2]; if (cp1 == glm::dvec3{} && cp2 == glm::dvec3{}) return glm::mix(p0, p3, T); auto const p1 = p0 + cp1; auto const p2 = p3 + cp2; double const u = 1.0 - T; return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; } char const *point_label(editor_track::point_kind const Kind) { switch (Kind) { case editor_track::point_kind::start: return "Start"; case editor_track::point_kind::control1: return "Control 1"; case editor_track::point_kind::control2: return "Control 2"; default: return "End"; } } bool is_end(editor_track::point_kind const Kind) { return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::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}; } TTrack *editor_mode::selected_track() const { return dynamic_cast(m_node); } void editor_mode::select_track(scene::basic_node *Node) { auto *track = dynamic_cast(Node); if (track == nullptr) { m_track_point = {}; return; } glm::dvec3 const camera{Global.pCamera.Pos}; if (glm::distance(glm::dvec3(track->get_nearest_point(camera)), camera) > static_cast(kMaxPlacementDistance)) return; if (track != m_node) m_track_point = {}; m_node = track; ui()->set_node(m_node); if (ui()->mode() != nodebank_panel::TRACK) return; m_track_window_open = true; if (track->eType == tt_Switch) { m_track_tab = track_tab::turnout; } else if (editor_track::is_straight(*track, m_straights.tolerance)) { m_track_tab_request = 0; m_track_tab = track_tab::straights; } else if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end() || route_from_curve(*track)) { m_track_tab_request = 1; m_track_tab = track_tab::route; } } bool editor_mode::pick_track_handle() { auto *track = selected_track(); if (track == nullptr || false == editor_track::is_supported(*track) || m_route_tab) return false; screen_projection const projection; ImVec2 const mouse = ImGui::GetIO().MousePos; float best = kHandleRadius * kHandleRadius; editor_track::point_ref hit; for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { for (auto const kind : kPointKinds) { editor_track::point_ref const point{i, kind}; ImVec2 screen; if (false == projection.project(editor_track::point_position(*track, point), screen)) continue; float const dx = screen.x - mouse.x; float const dy = screen.y - mouse.y; float const distance = (dx * dx + dy * dy) * (is_end(kind) ? 0.5f : 1.0f); if (distance < best) { best = distance; hit = point; } } } if (false == hit.valid()) return false; m_track_point = hit; return true; } void editor_mode::draw_track_overlay() const { auto const *track = selected_track(); if (track == nullptr) return; screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); bool const trackmode = ui()->mode() == nodebank_panel::TRACK; ImU32 const coursecolor = IM_COL32(40, 220, 255, 220); int constexpr samples{32}; for (auto const &path : track->m_paths) { auto previous = path_point(path, 0.0); for (int i = 1; i <= samples; ++i) { auto const next = path_point(path, static_cast(i) / samples); projection.line(drawlist, previous, next, coursecolor, 2.5f); previous = next; } } if (false == trackmode || m_route_tab || false == editor_track::is_supported(*track)) return; ImU32 const vectorcolor = IM_COL32(200, 200, 200, 160); for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::start}), editor_track::point_position(*track, {i, editor_track::point_kind::control1}), vectorcolor, 1.0f); projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::end}), editor_track::point_position(*track, {i, editor_track::point_kind::control2}), vectorcolor, 1.0f); } for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { for (auto const kind : kPointKinds) { editor_track::point_ref const point{i, kind}; ImVec2 screen; if (false == projection.project(editor_track::point_position(*track, point), screen)) continue; bool const selected = (point == m_track_point); if (is_end(kind)) { ImU32 const color = editor_track::is_connected(*track, point) ? IM_COL32(60, 220, 60, 255) : IM_COL32(240, 60, 60, 255); drawlist->AddCircleFilled(screen, 6.0f, color); } else { drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 4.0f), ImVec2(screen.x + 4.0f, screen.y + 4.0f), IM_COL32(230, 230, 230, 255)); } if (selected) drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 220, 40, 255), 16, 2.5f); } } if (m_track_snap.track != nullptr) { ImVec2 screen; if (projection.project(m_track_snap.position, screen)) drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); projection.line(drawlist, m_track_snap.position, m_track_snap.position + m_track_snap.direction * 5.0, IM_COL32(255, 60, 255, 200), 2.0f); } } void editor_mode::render_track_gizmo() { auto *track = selected_track(); std::string reason; if (track == nullptr || false == editor_track::can_edit_geometry(*track, reason)) { m_track_gizmo_using = false; return; } bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size()); ImGuizmo::BeginFrame(); ImGuizmo::SetOrthographic(Global.EditorOrtho); ImGuiIO const &io = ImGui::GetIO(); ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; glm::mat4 const projection = editor_mode::projection_matrix(aspect); ImGuizmo::OPERATION const operation = (false == pointmode && m_gizmo_op == gizmo_operation::rotate) ? ImGuizmo::ROTATE_Y : ImGuizmo::TRANSLATE; if (false == m_track_gizmo_using) { auto const anchor = pointmode ? editor_track::point_position(*track, m_track_point) : editor_track::pivot(*track); m_track_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - camerapos)); } glm::vec3 snapvalue(operation == ImGuizmo::ROTATE_Y ? 5.0f : m_gizmo_snap); float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; glm::mat4 delta(1.0f); ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), operation, ImGuizmo::WORLD, glm::value_ptr(m_track_gizmo), glm::value_ptr(delta), snap); if (ImGuizmo::IsUsing()) { if (false == m_track_gizmo_using) { m_track_gizmo_using = true; m_track_pivot = editor_track::pivot(*track); m_track_drag = {track}; m_track_drag_points.clear(); m_track_snap = {}; if (pointmode && 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)); } if (operation == ImGuizmo::ROTATE_Y) { double const angle = std::atan2(delta[2][0], delta[0][0]); if (std::abs(angle) > 1e-9) { editor_track::rotate_y(*track, m_track_pivot, angle); m_track_dirty = true; } } else if (pointmode) { glm::dvec3 const position = camerapos + glm::dvec3(m_track_gizmo[3]); 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; if (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 = editor_track::find_snap_target(*track, position, m_track_snap_radius, exclude); } } else { glm::dvec3 const offset{delta[3]}; if (glm::length(offset) > 1e-9) { editor_track::translate(*track, offset); m_track_dirty = true; } } commit_track_drag(false); } else if (m_track_gizmo_using) { if (m_track_snap.track != nullptr && pointmode) { 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); m_track_gizmo_using = false; m_track_drag.clear(); m_track_drag_points.clear(); } } void editor_mode::commit_track_drag(bool const Force) { if (false == m_track_dirty || m_track_drag.empty()) return; auto const now = std::chrono::steady_clock::now(); if (false == Force && now - m_track_last_commit < std::chrono::milliseconds(100)) return; editor_track::commit(m_track_drag); m_track_dirty = false; m_track_last_commit = now; } void editor_mode::push_track_snapshot(std::vector> States, std::vector Created) { if (States.empty() && Created.empty()) return; if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); auto *track = States.empty() ? Created.front() : States.front().first; EditorSnapshot snap; snap.action = EditorSnapshot::Action::TrackEdit; snap.node_name = track->name(); snap.node_ptr = track; snap.position = track->location(); snap.uuid = track->uuid; snap.tracks = std::move(States); snap.created = std::move(Created); m_history.push_back(std::move(snap)); g_redo.clear(); } void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo) { if (Snapshot.tracks.empty() && Snapshot.created.empty() && Snapshot.removed.empty()) return; std::vector tracks; for (auto const &entry : Snapshot.tracks) tracks.emplace_back(entry.first); EditorSnapshot current{Snapshot}; current.tracks.clear(); auto involved{tracks}; if (Undo) involved.insert(involved.end(), Snapshot.created.begin(), Snapshot.created.end()); for (auto *track : involved) current.tracks.emplace_back(track, editor_track::capture(*track)); Opposite.push_back(std::move(current)); if (Undo) { for (auto *track : Snapshot.created) editor_track::retire(*track); for (auto *track : Snapshot.removed) { track->m_editorremoved = false; tracks.push_back(track); } } else { for (auto *track : Snapshot.created) track->m_editorremoved = false; for (auto *track : Snapshot.removed) editor_track::retire(*track); } for (auto const &entry : Snapshot.tracks) editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); if (false == m_route.chain.tracks.empty()) { std::string error; if (m_route.from->m_editorremoved || m_route.to->m_editorremoved || false == editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error)) { m_route.chain = {}; m_route.result = {}; m_route.vertex = m_route.grip = -1; } else { route_bind_ends(); route_update(); } } m_track_snap = {}; m_track_dirty = false; m_track_point = {}; m_node = tracks.empty() || tracks.front()->m_editorremoved ? nullptr : tracks.front(); ui()->set_node(m_node); straight_refresh(); } void editor_mode::render_track_ui() { ImGui::TextDisabled("LMB on a path opens its editor: straight, curve or switch"); if (ImGui::Button("Open the editor window")) m_track_window_open = true; if (ImGui::CollapsingHeader("Straights in the scenery")) render_straights_ui(); render_switch_ui(); } void editor_mode::render_track_window() { if (false == m_track_window_open || selected_track() == nullptr) return; char const *title{"Path###trackeditor"}; switch (m_track_tab) { case track_tab::straights: title = "Straight###trackeditor"; break; case track_tab::route: title = "Curve###trackeditor"; break; case track_tab::turnout: title = "Switch###trackeditor"; break; default: break; } ImGui::SetNextWindowSize(ImVec2(460.0f, 520.0f), ImGuiCond_FirstUseEver); if (false == ImGui::Begin(title, &m_track_window_open)) { ImGui::End(); return; } switch (m_track_tab) { case track_tab::straights: m_route_tab = true; render_straight_ui(); break; case track_tab::route: m_route_tab = true; render_route_ui(); break; case track_tab::turnout: m_route_tab = true; render_turnout_ui(); break; default: m_route_tab = false; render_path_ui(); break; } if (m_track_tab != track_tab::path) { ImGui::Separator(); if (ImGui::SmallButton("Edit points and parameters of the single path")) m_track_tab = track_tab::path; } ImGui::End(); } void editor_mode::render_turnout_ui() { auto *track{selected_track()}; if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2) return; auto const &main{track->m_paths[0]}; auto const &diverging{track->m_paths[1]}; auto const origin{main.points[segment_data::point::start]}; glm::dvec2 const axis{glm::normalize(glm::dvec2{main.points[segment_data::point::end].x - origin.x, main.points[segment_data::point::end].z - origin.z})}; glm::dvec2 const normal{-axis.y, axis.x}; auto const length{glm::dot(glm::dvec2{main.points[segment_data::point::end].x - origin.x, main.points[segment_data::point::end].z - origin.z}, axis)}; glm::dvec2 const end{diverging.points[segment_data::point::end].x - origin.x, diverging.points[segment_data::point::end].z - origin.z}; auto const offset{glm::dot(end, normal)}; auto const &control{diverging.points[segment_data::point::control2]}; glm::dvec2 const tangent{glm::normalize(glm::dvec2{-control.x, -control.z})}; auto const angle{std::atan2(std::abs(tangent.x * axis.y - tangent.y * axis.x), glm::dot(tangent, axis))}; ImGui::Text("%s", track->name().empty() ? "(noname)" : track->name().c_str()); ImGui::Text("Length %.3f m, diverging %s, end offset %.3f m", length, offset > 0.0 ? "left" : "right", std::abs(offset)); if (angle > 1e-6) ImGui::Text("Angle 1:%.2f (%.4f deg), R in the entry %.0f m", 1.0 / std::tan(angle), glm::degrees(angle), diverging.radius); ImGui::TextDisabled("Gizmo moves the whole switch (W: turn), drag from a red ring at a free end to add a path"); auto &tool{m_switch}; if (false == tool.collected) { tool.templates = editor_track::standard_switch_templates(); auto const found{editor_track::find_switch_templates()}; tool.templates.insert(tool.templates.end(), found.begin(), found.end()); tool.collected = true; } if (tool.templates.empty()) return; m_turnout_template = std::clamp(m_turnout_template, 0, static_cast(tool.templates.size()) - 1); ImGui::Separator(); ImGui::PushItemWidth(-1.0f); if (ImGui::BeginCombo("##turnouttemplate", tool.templates[m_turnout_template].label.c_str())) { for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const label{(tool.templates[i].source != nullptr ? "Scenery: " : "PLK: ") + tool.templates[i].label}; if (ImGui::Selectable(label.c_str(), i == m_turnout_template)) m_turnout_template = i; } ImGui::EndCombo(); } ImGui::PopItemWidth(); auto const replace = [&](editor_track::switch_template const &Template, int const Side) { auto const grade{length > 0.0 ? (main.points[segment_data::point::end].y - origin.y) / length : 0.0}; auto const paths{editor_track::place_switch(Template, origin, axis, Side, grade)}; push_track_snapshot({{track, editor_track::capture(*track)}}); track->m_paths.assign(paths.begin(), paths.begin() + 2); editor_track::commit({track}); }; int const side{offset > 0.0 ? 1 : -1}; if (ImGui::Button("Replace the geometry with the template")) replace(tool.templates[m_turnout_template], side); ImGui::SameLine(); if (ImGui::Button("Flip the side")) { auto const current{editor_track::find_switch_templates()}; editor_track::switch_template own; own.source = track; for (int i = 0; i < 2; ++i) { auto &local{own.local[i]}; local = track->m_paths[i]; auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(glm::dvec2{Vector.x, Vector.z}, normal) * side, Vector.y, glm::dot(glm::dvec2{Vector.x, Vector.z}, axis)}; }; local.points[segment_data::point::start] = tolocal(track->m_paths[i].points[segment_data::point::start] - origin); local.points[segment_data::point::end] = tolocal(track->m_paths[i].points[segment_data::point::end] - origin); local.points[segment_data::point::control1] = tolocal(track->m_paths[i].points[segment_data::point::control1]); local.points[segment_data::point::control2] = tolocal(track->m_paths[i].points[segment_data::point::control2]); if (side < 0) local.rolls = {-local.rolls[0], -local.rolls[1]}; } replace(own, -side); } ImGui::Separator(); render_path_parameters(*track); } void editor_mode::render_path_parameters(TTrack &Track) { auto *track{&Track}; auto const before = editor_track::capture(*track); enum class rebuild { none, parameters, geometry }; auto const finish_edit = [&](rebuild const Rebuild) { if (ImGui::IsItemActivated()) m_track_field_before = before; if (ImGui::IsItemDeactivatedAfterEdit()) { push_track_snapshot({{track, m_track_field_before}}); switch (Rebuild) { case rebuild::geometry: editor_track::commit({track}); break; case rebuild::parameters: editor_track::commit_parameters(*track); break; default: track->mark_dirty(); break; } } }; auto const apply_now = [&](rebuild const Rebuild) { push_track_snapshot({{track, before}}); if (Rebuild == rebuild::parameters) editor_track::commit_parameters(*track); else track->mark_dirty(); }; if (ImGui::CollapsingHeader("Path parameters", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::PushItemWidth(120.0f); double velocity = editor_track::velocity(*track); if (ImGui::InputDouble("Velocity (km/h, -1: none)", &velocity, 0.0, 0.0, "%.1f")) editor_track::velocity(*track, velocity); finish_edit(rebuild::none); if (ImGui::InputFloat("Friction", &track->fFriction, 0.0f, 0.0f, "%.3f")) track->fFriction = std::max(0.0f, track->fFriction); finish_edit(rebuild::none); ImGui::InputFloat("Sound distance", &track->fSoundDistance, 0.0f, 0.0f, "%.1f"); finish_edit(rebuild::none); ImGui::InputInt("Quality", &track->iQualityFlag, 0); finish_edit(rebuild::none); int damage = track->iDamageFlag; if (ImGui::InputInt("Damage flags", &damage, 0)) editor_track::damage(*track, damage); finish_edit(rebuild::none); char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; int environment = std::clamp(static_cast(track->eEnvironment), 0, 5); if (ImGui::Combo("Environment", &environment, environments, IM_ARRAYSIZE(environments))) { track->eEnvironment = static_cast(environment); apply_now(rebuild::none); } if (ImGui::InputFloat("Width", &track->fTrackWidth, 0.0f, 0.0f, "%.3f")) track->fTrackWidth = std::max(0.01f, track->fTrackWidth); finish_edit(rebuild::parameters); ImGui::PopItemWidth(); if (track->m_visible) { material_handle *materials[] = {&track->m_material1, &track->m_material2, track->SwitchExtension ? &track->SwitchExtension->m_material3 : nullptr}; char const *pathlabels[] = {"Texture 1 (rails/surface)", "Texture 2 (trackbed/side)", "Trackbed (switch)"}; char const *switchlabels[] = {"Rails P1-P2", "Rails P3-P4", "Trackbed"}; auto const &labels{track->eType == tt_Switch ? switchlabels : pathlabels}; if (false == ImGui::IsAnyItemActive()) { for (int i = 0; i < 3; ++i) { auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{}; std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1); m_track_materials[i].back() = '\0'; } } for (int i = 0; i < 3; ++i) { if (materials[i] == nullptr || (i == 2 && track->eType != tt_Switch)) continue; ImGui::PushItemWidth(-160.0f); if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size(), ImGuiInputTextFlags_EnterReturnsTrue)) { *materials[i] = editor_track::fetch_material(m_track_materials[i].data()); apply_now(rebuild::parameters); } ImGui::PopItemWidth(); } ImGui::TextDisabled("Enter applies a texture name, \"none\" removes the texture"); ImGui::PushItemWidth(120.0f); if (ImGui::InputFloat("Texture length", &track->fTexLength, 0.0f, 0.0f, "%.2f")) track->fTexLength = std::max(0.01f, track->fTexLength); finish_edit(rebuild::parameters); float height = editor_track::texture_height(*track); if (ImGui::InputFloat("Trackbed height", &height, 0.0f, 0.0f, "%.3f")) editor_track::texture_height(*track, height); finish_edit(rebuild::parameters); ImGui::InputFloat("Trackbed width", &track->fTexWidth, 0.0f, 0.0f, "%.3f"); finish_edit(rebuild::parameters); ImGui::InputFloat("Trackbed slope", &track->fTexSlope, 0.0f, 0.0f, "%.3f"); finish_edit(rebuild::parameters); ImGui::PopItemWidth(); } } } void editor_mode::render_path_ui() { ImGui::TextDisabled("LMB: select path or point handle Esc: release point\nGizmo moves the selected point, otherwise the whole path (W: turn)"); auto *track = selected_track(); if (track == nullptr) { ImGui::TextDisabled("No path selected"); return; } char const *type = track->eType == tt_Normal ? "normal" : track->eType == tt_Switch ? "switch" : track->eType == tt_Cross ? "cross" : track->eType == tt_Table ? "turntable" : track->eType == tt_Tributary ? "tributary" : "unknown"; char const *category = (track->iCategoryFlag & 1) ? "rail" : (track->iCategoryFlag & 2) ? "road" : (track->iCategoryFlag & 4) ? "river" : "other"; ImGui::Text("%s (%s %s, %.2f m)", track->name().empty() ? "(noname)" : track->name().c_str(), category, type, track->Length()); if (false == editor_track::is_supported(*track)) { ImGui::TextDisabled("Editing of this path type isn't supported"); return; } std::string reason; bool const geometry = editor_track::can_edit_geometry(*track, reason); if (false == geometry) ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "Geometry locked: %s", reason.c_str()); auto const before = editor_track::capture(*track); enum class rebuild { none, parameters, geometry }; auto const finish_edit = [&](rebuild const Rebuild) { if (ImGui::IsItemActivated()) m_track_field_before = before; if (ImGui::IsItemDeactivatedAfterEdit()) { push_track_snapshot({{track, m_track_field_before}}); switch (Rebuild) { case rebuild::geometry: editor_track::commit({track}); break; case rebuild::parameters: editor_track::commit_parameters(*track); break; default: track->mark_dirty(); break; } } }; auto const apply_now = [&](rebuild const Rebuild) { push_track_snapshot({{track, before}}); if (Rebuild == rebuild::parameters) editor_track::commit_parameters(*track); else track->mark_dirty(); }; render_path_parameters(*track); if (ImGui::CollapsingHeader("Path geometry", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::PushItemWidth(120.0f); ImGui::InputFloat("Snap radius (m)", &m_track_snap_radius, 0.0f, 0.0f, "%.2f"); m_track_snap_radius = std::max(0.0f, m_track_snap_radius); ImGui::PopItemWidth(); ImGui::Checkbox("Align with snapped path", &m_track_align_tangent); ImGui::SameLine(); ImGui::Checkbox("Drag connected ends", &m_track_drag_connected); for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { ImGui::PushID(i); if (track->m_paths.size() > 1) ImGui::Text("Path %d", i + 1); for (auto const kind : kPointKinds) { editor_track::point_ref const point{i, kind}; if (kind != editor_track::point_kind::start) ImGui::SameLine(); std::string label = point_label(kind); if (is_end(kind)) label += editor_track::is_connected(*track, point) ? " (+)" : " (-)"; if (ImGui::Selectable(label.c_str(), m_track_point == point, 0, ImVec2(90.0f, 0.0f))) m_track_point = (m_track_point == point ? editor_track::point_ref{} : point); } auto &path = track->m_paths[i]; if (geometry) { ImGui::PushItemWidth(-160.0f); for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) { auto position = path.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end]; if (ImGui::InputScalarN(point_label(kind), ImGuiDataType_Double, glm::value_ptr(position), 3, nullptr, nullptr, "%.3f")) editor_track::move_point(*track, {i, kind}, position); finish_edit(rebuild::geometry); } ImGui::InputScalarN("Control 1 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control1]), 3, nullptr, nullptr, "%.3f"); finish_edit(rebuild::geometry); ImGui::InputScalarN("Control 2 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control2]), 3, nullptr, nullptr, "%.3f"); finish_edit(rebuild::geometry); ImGui::InputFloat2("Roll (deg)", path.rolls.data(), "%.2f"); finish_edit(rebuild::geometry); ImGui::InputFloat("Radius", &path.radius, 0.0f, 0.0f, "%.1f"); finish_edit(rebuild::geometry); ImGui::PopItemWidth(); if (ImGui::Button("Straighten")) { push_track_snapshot({{track, before}}); path.points[segment_data::point::control1] = glm::dvec3{}; path.points[segment_data::point::control2] = glm::dvec3{}; path.radius = 0.0f; editor_track::commit({track}); } } else { auto const &start = path.points[segment_data::point::start]; auto const &end = path.points[segment_data::point::end]; ImGui::TextDisabled("Start (%.2f, %.2f, %.2f) End (%.2f, %.2f, %.2f)", start.x, start.y, start.z, end.x, end.y, end.z); } ImGui::PopID(); } } } 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; Vertex.vertical_radius = 0.0; } void editor_mode::route_bind_ends() { auto const &chain{m_route.chain}; auto &design{m_route.design}; auto const plan = [](glm::dvec3 const &Direction) { return glm::normalize(glm::dvec2{Direction.x, Direction.z}); }; auto const slope = [](glm::dvec3 const &Direction) { return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); }; design.start = chain.start; design.end = chain.end; design.start_direction = plan(chain.start_direction); design.end_direction = plan(chain.end_direction); design.start_grade = slope(chain.start_direction); design.end_grade = slope(chain.end_direction); design.start_radius = chain.start_radius; design.end_radius = chain.end_radius; design.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)) return; 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{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; vertex.offset = distance / 3.0; route.design.vertices.push_back(vertex); route.design.vertices.push_back(vertex); } } route_update(); } 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)) 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}; alignment::vertex vertex; vertex.radius = curve.radius > 0.0 ? std::max(10.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); if (curve.reversals == 0) { vertex.reverse_turn = std::abs(curve.turn) > glm::pi(); if (curve.compound) { vertex.compound = true; vertex.radius2 = std::round(curve.radius2); vertex.transition_middle = std::round(curve.transition_middle); vertex.split = curve.split; } design.vertices = {vertex}; } else { auto const distance{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; vertex.offset = distance / 3.0; design.vertices = {vertex, vertex}; } route_update(); m_route.status = "Curve of " + std::to_string(m_route.chain.tracks.size()) + " paths, turning " + std::to_string(static_cast(std::round(glm::degrees(curve.turn)))) + " deg"; 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)) return; if (glm::distance(chain.start, route.chain.start) > 1e-3 || glm::distance(chain.end, route.chain.end) > 1e-3) { route.error = "The fragment changed since it was selected, select it again"; return; } for (auto *track : chain.tracks) { std::string reason; if (false == editor_track::can_edit_geometry(*track, reason)) { route.error = track->name() + ": " + reason; return; } } std::vector> states; std::vector created; 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); auto const plandistance = [](glm::dvec3 const &A, glm::dvec3 const &B) { return glm::distance(glm::dvec2{A.x, A.z}, glm::dvec2{B.x, B.z}); }; std::string error; if (false == editor_track::find_chain(from, to, chain, error) || plandistance(chain.start, route.result.start) > 5.0 || plandistance(chain.end, route.result.end) > 5.0) { 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}); } route.error = error.empty() ? "The adjoining straights can't be cut at the ends of the curve" : error; 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)); auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; auto relaid{editor_track::relay(chain, pieces)}; 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); 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.status = "Re-laid " + std::to_string(chain.tracks.size()) + " paths" + (added > 0 ? ", added " + std::to_string(added) : std::string{}); if (startextension > 0.0 || endextension > 0.0) { char text[96]; std::snprintf(text, sizeof(text), ", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); route.status += text; } WriteLog("Editor: route design - " + route.status, logtype::generic); } glm::dvec3 editor_mode::route_vertex_position(int const Vertex) const { auto const &design{m_route.design}; auto const &result{m_route.result}; auto const count{static_cast(design.vertices.size())}; glm::dvec2 const start{design.start.x, design.start.z}; glm::dvec2 const end{design.end.x, design.end.z}; glm::dvec2 position; if (Vertex < static_cast(result.vertices.size()) && false == result.vertices.empty()) position = result.vertices[Vertex]; else if (count == 1) { if (false == alignment::tangent_intersection(design, position)) position = (start + end) * 0.5; } else if (Vertex == 0) position = start + glm::normalize(design.start_direction) * design.vertices.front().offset; else if (Vertex == count - 1) position = end - glm::normalize(design.end_direction) * design.vertices.back().offset; else position = design.vertices[Vertex].position; double height; if (Vertex < static_cast(result.vertex_elevations.size())) height = result.vertex_elevations[Vertex]; else if (false == design.vertices[Vertex].auto_elevation) height = design.vertices[Vertex].elevation; else height = design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0); return {position.x, height, position.y}; } bool editor_mode::pick_route_vertex() { if (false == route_active()) return false; screen_projection const projection; ImVec2 const mouse = ImGui::GetIO().MousePos; float best = kHandleRadius * kHandleRadius; int hit{-1}; for (int i = 0; i < static_cast(m_route.design.vertices.size()); ++i) { ImVec2 screen; if (false == projection.project(route_vertex_position(i), screen)) continue; float const dx = screen.x - mouse.x; float const dy = screen.y - mouse.y; if (dx * dx + dy * dy < best) { best = dx * dx + dy * dy; hit = i; } } int griphit{-1}; for (int i = 0; i < static_cast(m_route.result.vertex_chainages.size()) && m_route.result.length > 0.0; ++i) { ImVec2 screen; if (false == projection.project(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position, screen)) continue; float const dx = screen.x - mouse.x; float const dy = screen.y - mouse.y; if (dx * dx + dy * dy < best) { best = dx * dx + dy * dy; griphit = i; hit = -1; } } if (hit < 0 && griphit < 0) return false; m_route.vertex = hit; m_route.grip = griphit; return true; } void editor_mode::draw_route_overlay() const { auto const &route{m_route}; auto const &design{route.design}; auto const &result{route.result}; screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); 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 ? IM_COL32(60, 230, 90, 255) : IM_COL32(255, 140, 30, 255); int const steps = std::clamp(static_cast(e.length / 2.0), 1, 400); auto previous{alignment::evaluate(result, e.chainage).position}; for (int j = 1; j <= steps; ++j) { auto const next{alignment::evaluate(result, e.chainage + e.length * j / steps).position}; projection.line(drawlist, previous, next, color, 3.0f); previous = next; } ImVec2 screen; if (projection.project(alignment::evaluate(result, e.chainage).position, screen)) drawlist->AddCircleFilled(screen, 3.5f, IM_COL32(255, 255, 255, 255)); } } 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, IM_COL32(255, 210, 60, 255)); if (i == route.vertex) drawlist->AddCircle(screen, 11.0f, IM_COL32(255, 255, 255, 255), 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, IM_COL32(60, 230, 90, 255), 4); if (i == route.grip) drawlist->AddCircle(screen, 11.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f); char radius[40]; auto const *report{i < static_cast(result.curves.size()) ? &result.curves[i] : nullptr}; if (report != nullptr && report->radius2 > 0.0) std::snprintf(radius, sizeof(radius), "R %.0f / %.0f", report->radius, report->radius2); else std::snprintf(radius, sizeof(radius), "R %.0f", report != nullptr ? report->radius : design.vertices[i].radius); drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), IM_COL32(60, 230, 90, 255), radius); } char label[8]; std::snprintf(label, sizeof(label), "W%d", i + 1); drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), IM_COL32(255, 210, 60, 255), label); } } void editor_mode::render_route_gizmo() { auto &route{m_route}; auto const count{static_cast(route.design.vertices.size())}; 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)) || false == m_gizmo_enabled) { m_route_gizmo_using = false; return; } ImGuizmo::BeginFrame(); ImGuizmo::SetOrthographic(Global.EditorOrtho); ImGuiIO const &io = ImGui::GetIO(); ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; glm::mat4 const projection = editor_mode::projection_matrix(aspect); auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)}; if (false == m_route_gizmo_using) m_route_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(position - camerapos)); glm::vec3 snapvalue(m_gizmo_snap); float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, glm::value_ptr(m_route_gizmo), nullptr, snap); if (false == ImGuizmo::IsUsing()) { if (m_route_gizmo_using) { m_route_gizmo_using = false; route_apply(); } return; } m_route_gizmo_using = true; glm::dvec3 const moved{camerapos + glm::dvec3(m_route_gizmo[3])}; if (grip) { auto &vertex{route.design.vertices[route.grip]}; auto const corner{route_vertex_position(route.grip)}; auto const distance{glm::distance(glm::dvec2{moved.x, moved.z}, glm::dvec2{corner.x, corner.z})}; 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 - glm::dvec2{route.design.start.x, route.design.start.z}, glm::normalize(route.design.start_direction))); else if (route.vertex == count - 1) vertex.offset = std::max(0.1, glm::dot(glm::dvec2{route.design.end.x, route.design.end.z} - point, glm::normalize(route.design.end_direction))); else vertex.position = point; } if (std::abs(moved.y - position.y) > 1e-3) { vertex.auto_elevation = false; vertex.elevation = moved.y; } route_update(); } void editor_mode::render_route_ui() { auto &route{m_route}; auto &design{route.design}; ImGui::TextDisabled("LMB on a curve: loads the whole curve between the adjoining straights.\n" "The ends of the curve and their directions stay fixed.\n" "LMB on a vertex (W1, W2...): moves it with the gizmo Esc: release the vertex"); auto *track = selected_track(); auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : (Track->name().empty() ? std::string{"(noname)"} : Track->name()); }; ImGui::Text("First path: %s", name(route.from).c_str()); ImGui::SameLine(ImGui::GetWindowContentRegionWidth() - 110.0f); if (ImGui::Button("Use selected##routefrom", ImVec2(110.0f, 0.0f)) && track != nullptr) { route.from = track; if (route.to == nullptr) route.to = track; route_reset(); } ImGui::Text("Last path: %s", name(route.to).c_str()); ImGui::SameLine(ImGui::GetWindowContentRegionWidth() - 110.0f); if (ImGui::Button("Use selected##routeto", ImVec2(110.0f, 0.0f)) && track != nullptr) { route.to = track; if (route.from == nullptr) route.from = track; route_reset(); } if (false == route.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); if (route.chain.tracks.empty()) return; ImGui::Text("Fragment: %zu paths, %.2f m", route.chain.tracks.size(), route.chain.length); ImGui::SameLine(); if (ImGui::SmallButton("Reset design")) { route_reset(); return; } bool changed{false}; ImGui::PushItemWidth(120.0f); changed |= ImGui::InputDouble("Design speed (km/h)", &design.speed, 0.0, 0.0, "%.0f"); design.speed = std::max(1.0, design.speed); int shape = static_cast(design.shape); char const *shapes[] = {"Cubic parabola", "Clothoid"}; if (ImGui::Combo("Transition curve", &shape, shapes, IM_ARRAYSIZE(shapes))) { design.shape = static_cast(shape); changed = true; } if (ImGui::SliderInt("Paths per transition curve", &design.transition_pieces, 1, 4)) route_update(); if (ImGui::IsItemDeactivatedAfterEdit()) route_apply(); ImGui::PopItemWidth(); if (ImGui::TreeNode("Limits")) { auto &norms{design.norms}; ImGui::PushItemWidth(100.0f); changed |= ImGui::InputDouble("Rail axes distance (mm)", &norms.gauge, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Unbalanced acceleration (m/s2)", &norms.unbalanced, 0.0, 0.0, "%.2f"); changed |= ImGui::InputDouble("Maximum cant (mm)", &norms.cant_max, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Cant change rate (mm/s)", &norms.cant_rate, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Cant ramp 1:(k*V), k", &norms.ramp_factor, 0.0, 0.0, "%.1f"); changed |= ImGui::InputDouble("Unbalanced acc. change rate (m/s3)", &norms.jerk, 0.0, 0.0, "%.2f"); changed |= ImGui::InputDouble("Vertical curve R >= k*V^2, k", &norms.vertical_factor, 0.0, 0.0, "%.2f"); changed |= ImGui::InputDouble("Vertical curve R min (m)", &norms.vertical_min, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Straight between curves (s of travel)", &norms.tangent_min_time, 0.0, 0.0, "%.1f"); ImGui::PopItemWidth(); norms.gauge = std::max(100.0, norms.gauge); norms.cant_rate = std::max(1.0, norms.cant_rate); norms.jerk = std::max(0.01, norms.jerk); ImGui::TreePop(); } auto &vertices{design.vertices}; if (ImGui::Button("Add vertex")) { alignment::vertex vertex; vertex.radius = vertices.empty() ? 1000.0 : vertices.back().radius; route_recommend(vertex); auto const distance{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; if (vertices.size() == 1) { vertices.front().offset = distance / 3.0; vertex.offset = distance / 3.0; vertices.push_back(vertex); } else if (vertices.empty()) { vertices.push_back(vertex); } else { auto const index{std::clamp(route.vertex, 0, static_cast(vertices.size()) - 2)}; auto const a{route_vertex_position(index)}; auto const b{route_vertex_position(index + 1)}; vertex.position = glm::dvec2{(a.x + b.x) * 0.5, (a.z + b.z) * 0.5}; for (int i = 0; i < static_cast(vertices.size()); ++i) { auto const p{route_vertex_position(i)}; vertices[i].position = {p.x, p.z}; } vertices.insert(vertices.begin() + index + 1, vertex); } changed = true; } ImGui::SameLine(); if (ImGui::Button("Remove vertex") && false == vertices.empty()) { auto const index{route.vertex >= 0 && route.vertex < static_cast(vertices.size()) ? route.vertex : static_cast(vertices.size()) - 1}; vertices.erase(vertices.begin() + index); route.vertex = -1; changed = true; } auto const count{static_cast(vertices.size())}; for (int i = 0; i < count; ++i) { auto &vertex{vertices[i]}; ImGui::PushID(i); char header[32]; std::snprintf(header, sizeof(header), "Vertex W%d", i + 1); if (ImGui::Selectable(header, route.vertex == i)) route.vertex = (route.vertex == i ? -1 : i); ImGui::Indent(); ImGui::PushItemWidth(110.0f); if (count == 1) ImGui::TextDisabled("At the intersection of the end tangents"); else if (i == 0) changed |= ImGui::InputDouble("Distance from the start (m)", &vertex.offset, 0.0, 0.0, "%.2f"); else if (i == count - 1) changed |= ImGui::InputDouble("Distance from the end (m)", &vertex.offset, 0.0, 0.0, "%.2f"); else { ImGui::PushItemWidth(220.0f); changed |= ImGui::InputScalarN("Position (x, z)", ImGuiDataType_Double, glm::value_ptr(vertex.position), 2, nullptr, nullptr, "%.2f"); ImGui::PopItemWidth(); } changed |= ImGui::InputDouble(vertex.compound ? "Radius R1 (m)" : "Radius R (m)", &vertex.radius, 0.0, 0.0, "%.1f"); changed |= ImGui::Checkbox("Compound curve (two radii)", &vertex.compound); if (vertex.compound) { changed |= ImGui::InputDouble("Radius R2 (m)", &vertex.radius2, 0.0, 0.0, "%.1f"); changed |= ImGui::InputDouble("Transition R1-R2 (m)", &vertex.transition_middle, 0.0, 0.0, "%.1f"); float split{static_cast(vertex.split)}; if (ImGui::SliderFloat("Share of R1", &split, 0.0f, 1.0f, "%.2f")) { vertex.split = split; changed = true; } vertex.radius2 = std::max(1.0, vertex.radius2); vertex.transition_middle = std::max(0.0, vertex.transition_middle); } changed |= ImGui::InputDouble("Transition in (m)", &vertex.transition_in, 0.0, 0.0, "%.1f"); changed |= ImGui::InputDouble("Transition out (m)", &vertex.transition_out, 0.0, 0.0, "%.1f"); changed |= ImGui::InputDouble("Cant (mm)", &vertex.cant, 0.0, 0.0, "%.0f"); changed |= ImGui::Checkbox("Turn the longer way (over 180 deg)", &vertex.reverse_turn); changed |= ImGui::Checkbox("Elevation from the ends", &vertex.auto_elevation); if (false == vertex.auto_elevation) changed |= ImGui::InputDouble("Elevation (m)", &vertex.elevation, 0.0, 0.0, "%.3f"); changed |= ImGui::InputDouble("Vertical curve R (0: min)", &vertex.vertical_radius, 0.0, 0.0, "%.0f"); ImGui::PopItemWidth(); vertex.radius = std::max(1.0, vertex.radius); vertex.transition_in = std::max(0.0, vertex.transition_in); vertex.transition_out = std::max(0.0, vertex.transition_out); vertex.cant = std::max(0.0, vertex.cant); vertex.vertical_radius = std::max(0.0, vertex.vertical_radius); if (ImGui::SmallButton("Apply recommended for V")) { route_recommend(vertex); changed = true; } auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; ImGui::TextDisabled("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m", design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); for (auto const &curve : route.result.curves) { if (curve.vertex != i) continue; ImGui::Text("Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m", glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length); ImGui::Text("Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3", curve.unbalanced, curve.cant_rate_in, curve.cant_rate_out, curve.jerk_in, curve.jerk_out); } ImGui::Unindent(); ImGui::PopID(); } if (changed) route_update(); ImGui::Separator(); auto const &result{route.result}; if (result.length > 0.0) { auto const pieces{alignment::minimum_pieces(result, design)}; ImGui::Text("Length %.2f m (now %.2f m), %zu elements", result.length, route.chain.length, result.elements.size()); ImGui::TextDisabled("Paths: %zu in the fragment, %zu needed%s", route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? " (copies of the neighbours will be added)" : ""); if (result.start_extension > 0.0 || result.end_extension > 0.0) ImGui::Text("Takes over %.2f m of the straight at the start, %.2f m at the end", result.start_extension, result.end_extension); } ImGui::TextDisabled("Straight available beyond the ends: %.2f / %.2f m", design.start_reserve, design.end_reserve); for (auto const &error : result.errors) ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", error.c_str()); for (auto const &warning : result.warnings) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", warning.c_str()); if (ImGui::TreeNode("Elements")) { char const *kinds[] = {"straight", "transition", "curve", "transition"}; for (auto const &e : result.elements) ImGui::Text("%8.2f m %-10s L %8.2f m%s", e.chainage, kinds[static_cast(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast(std::round(e.radius))) + " m").c_str()); ImGui::TreePop(); } if (result.valid) { if (ImGui::Button("Apply to the scenery")) route_apply(); } else { ImGui::TextDisabled("Correct the errors to apply the design"); } if (false == route.status.empty()) { ImGui::SameLine(); ImGui::TextUnformatted(route.status.c_str()); } } 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(); 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(glm::dvec2{offset.x, offset.z}, Line.direction), 0.0, Line.length)}; auto const closest{Line.start + glm::dvec3{Line.direction.x, 0.0, Line.direction.y} * along}; return glm::distance(glm::dvec2{closest.x, closest.z}, glm::dvec2{camera.x, camera.z}) < 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 ? IM_COL32(40, 220, 255, 255) : 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, IM_COL32(40, 220, 255, 255), 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, IM_COL32(255, 60, 60, 255)); if (projection.project(state.tool_handle, screen)) drawlist->AddCircleFilled(screen, 7.0f, IM_COL32(255, 210, 60, 255)); if (state.tool == 1) { auto const direction{glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}}; 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, IM_COL32(255, 210, 60, 255), 3.0f); } } else { glm::dvec2 const normal{-line.direction.y, line.direction.x}; auto const offset{glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}}; 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, IM_COL32(255, 210, 60, 255), 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 ? IM_COL32(255, 210, 60, 255) : IM_COL32(255, 60, 60, 255), 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], IM_COL32(255, 210, 60, 255), 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), IM_COL32(40, 220, 255, 255)); else drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(40, 220, 255, 255), 4); if (i == m_straights.handle) drawlist->AddCircle(screen, 12.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f); } } auto const &line{m_straights.current}; if (line.tracks.empty() || m_straights.dragging) return; glm::dvec2 const normal{-line.direction.y, line.direction.x}; for (auto const &other : m_straights.neighbours) { auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; auto const across = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, normal); }; auto const a0{along(other.start)}, a1{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}; projection.line(drawlist, foot, target, IM_COL32(255, 230, 120, 230), 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))))}; char label[48]; if (angle > 0.001) std::snprintf(label, sizeof(label), "%.3f m %.3f deg", std::abs(lateral), angle); else std::snprintf(label, sizeof(label), "%.3f m", std::abs(lateral)); auto const size{ImGui::CalcTextSize(label)}; drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 3.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 170), 3.0f); drawlist->AddText(screen, IM_COL32(255, 230, 120, 255), label); } } } 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(10.0))) 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{glm::dot(glm::dvec2{other.start.x - line.start.x, other.start.z - line.start.z}, line.direction)}; auto const a1{glm::dot(glm::dvec2{other.end.x - line.start.x, other.end.z - line.start.z}, line.direction)}; if (std::max(a0, a1) <= 0.0 || std::min(a0, a1) >= line.length) continue; state.neighbours.push_back(std::move(other)); } } } } glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const { glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; auto const middle{(Line.start + Line.end) * 0.5 + Offset}; double best{0.2}; double correction{0.0}; for (auto const &other : m_straights.neighbours) { if (std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x) > std::sin(glm::radians(0.5))) continue; auto const distance{glm::dot(glm::dvec2{other.start.x - middle.x, other.start.z - middle.z}, normal)}; for (auto const spacing : m_straights.spacings) { auto const error{std::abs(distance) - spacing}; if (std::abs(error) < best) { best = std::abs(error); correction = distance > 0.0 ? error : -error; } } } return Offset + glm::dvec3{normal.x, 0.0, normal.y} * 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(0.5))) 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("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("Angle tolerance (deg)", &angle, 0.0, 0.0, "%.4f")) state.tolerance.angle = glm::radians(std::max(0.0, angle)); ImGui::InputDouble("Offset tolerance (m)", &state.tolerance.offset, 0.0, 0.0, "%.3f"); ImGui::InputDouble("Treat as straight above R (m)", &state.tolerance.radius, 0.0, 0.0, "%.0f"); ImGui::InputDouble("Minimum length (m)", &state.minimum_length, 0.0, 0.0, "%.0f"); ImGui::PopItemWidth(); if (ImGui::Button("Recognize straights in the scenery")) { state.found = editor_track::find_straights(state.minimum_length, state.tolerance); state.listed = -1; state.current_for = nullptr; } auto const describe = [](editor_track::straight const &Line) { char text[160]; std::snprintf(text, sizeof(text), "L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); return std::string{text}; }; if (false == state.found.empty()) { ImGui::Text("%zu straights", state.found.size()); ImGui::SameLine(); ImGui::PushItemWidth(150.0f); ImGui::InputTextWithHint("##straightfilter", "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()}; auto const describe = [](editor_track::straight const &Line) { char text[160]; std::snprintf(text, sizeof(text), "L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); return std::string{text}; }; if (line.tracks.empty()) { ImGui::TextDisabled(selected_track() != nullptr ? "The selected path isn't straight" : "No path selected"); } else { ImGui::Text("Straight of the selected path"); ImGui::Indent(); ImGui::TextUnformatted(describe(line).c_str()); ImGui::Text("From (%.3f, %.3f, %.3f)", line.start.x, line.start.y, line.start.z); ImGui::Text("To (%.3f, %.3f, %.3f)", line.end.x, line.end.y, line.end.z); ImGui::Text("Paths: %s ... %s", line.tracks.front()->name().c_str(), line.tracks.back()->name().c_str()); ImGui::Unindent(); if (in_straight_set(line)) { if (ImGui::Button("Remove from the set")) 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 if (ImGui::Button("Add to the set")) { state.set.push_back(line); } if (false == ImGui::IsAnyItemActive()) { state.edit_start = line.start; state.edit_end = line.end; state.edit_length = line.length; state.edit_azimuth = line.azimuth; } ImGui::TextDisabled("Drag the ends or the middle of the straight with the gizmo, or enter the values"); ImGui::PushItemWidth(260.0f); ImGui::InputScalarN("Start (x, y, z)", ImGuiDataType_Double, glm::value_ptr(state.edit_start), 3, nullptr, nullptr, "%.3f"); ImGui::InputScalarN("End (x, y, z)", ImGuiDataType_Double, glm::value_ptr(state.edit_end), 3, nullptr, nullptr, "%.3f"); ImGui::PopItemWidth(); if (ImGui::Button("Apply ends")) straight_apply(line, state.edit_start, state.edit_end); ImGui::PushItemWidth(120.0f); ImGui::InputDouble("Length (m)", &state.edit_length, 0.0, 0.0, "%.3f"); ImGui::SameLine(); if (ImGui::Button("Set length from the start")) { 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("Azimuth (deg)", &state.edit_azimuth, 0.0, 0.0, "%.5f"); ImGui::SameLine(); if (ImGui::Button("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(); } if (false == line.tracks.empty()) { ImGui::Separator(); ImGui::Text("Mouse tools"); auto const toolbutton = [&](char const *Label, int const Tool) { 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(); }; toolbutton("Break", 1); ImGui::SameLine(); toolbutton("S-curve", 2); if (state.tool == 1) ImGui::TextDisabled(state.tool_placed ? "Drag the handle to turn the rest of the straight, release to fit the curve" : "LMB on the straight: place the break point"); else if (state.tool == 2) ImGui::TextDisabled(state.tool_placed ? "Drag the handle: sideways sets the shift, along sets its length" : "LMB on the straight: place the start of the shift"); ImGui::PushItemWidth(100.0f); ImGui::InputDouble("Curve R (m, 0: automatic)", &state.curve_radius, 0.0, 0.0, "%.0f"); ImGui::Checkbox("Transitions from the line speed", &state.auto_transitions); if (false == state.auto_transitions) { ImGui::SameLine(); ImGui::InputDouble("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::Text("Set: %zu straights, dragging any of them moves the whole set, keeping it parallel", state.set.size()); ImGui::SameLine(); if (ImGui::SmallButton("Clear the set")) state.set.clear(); } if (false == state.neighbours.empty()) { ImGui::Separator(); ImGui::Text("Neighbouring straights: %zu (distances shown in the view)", state.neighbours.size()); ImGui::TextDisabled("Dragging the middle snaps the distance to: 3.5, 4.0, 4.5, 4.75, 5.0, 5.5, 6.0 m; dragging an end snaps parallel"); } } bool editor_mode::straights_active() { return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights && false == current_straight().tracks.empty(); } bool editor_mode::pick_straight_handle() { if (false == straights_active()) return false; auto const &line{current_straight()}; glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; screen_projection const projection; ImVec2 const mouse = ImGui::GetIO().MousePos; float best = kHandleRadius * kHandleRadius; int hit{-1}; for (int i = 0; i < 3; ++i) { ImVec2 screen; if (false == projection.project(handles[i], screen)) continue; float const dx = screen.x - mouse.x; float const dy = screen.y - mouse.y; if (dx * dx + dy * dy < best) { best = dx * dx + dy * dy; hit = i; } } if (hit < 0) return false; m_straights.handle = hit; return true; } 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)}; 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{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; state.tool_handle = {ground.x, state.tool_handle.y, ground.z}; return; } if (state.tool_placed && m_gizmo_enabled) { render_straight_tool_gizmo(); return; } auto const &line{state.dragging ? state.drag_line : current_straight()}; if (state.handle < 0 || line.tracks.empty() || false == m_gizmo_enabled) { state.dragging = false; return; } ImGuizmo::BeginFrame(); ImGuizmo::SetOrthographic(Global.EditorOrtho); ImGuiIO const &io = ImGui::GetIO(); ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; glm::mat4 const projection = editor_mode::projection_matrix(aspect); glm::dvec3 const anchors[] = {line.start, line.end, (line.start + line.end) * 0.5}; auto const anchor{anchors[state.handle]}; if (false == state.dragging) state.gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - camerapos)); glm::vec3 snapvalue(m_gizmo_snap); float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, glm::value_ptr(state.gizmo), nullptr, snap); if (ImGuizmo::IsUsing()) { 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); auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; 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{camerapos + glm::dvec3(state.gizmo[3])}; std::function transform; if (state.handle == 2) { auto const offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)}; 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); 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{glm::dvec2{pivot.x, pivot.z} + 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::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, to; bool forward; }; auto line{Line}; auto const point = [&](double const Along) { auto const planar{glm::dvec2{line.start.x, line.start.z} + 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 along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; auto const a{along(path.points[segment_data::point::start])}; auto const b{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}, last{-1}; auto const select = [&](double const End) { first = 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 = last = -1; return; } middle = best; } int low{middle}, 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); 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()) { 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 = 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() ? "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())}; auto created{editor_track::relay(m_route.chain, pieces)}; 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()); 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); straight_refresh(); state.status = "Done, the curves can be adjusted in the Route design tab"; } 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{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; auto const along{std::clamp(glm::dot(glm::dvec2{ground.x - line.start.x, ground.z - line.start.z}, line.direction), 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.tool_dragging = false; state.handle = -1; return true; } void editor_mode::render_straight_tool_gizmo() { auto &state{m_straights}; ImGuizmo::BeginFrame(); ImGuizmo::SetOrthographic(Global.EditorOrtho); ImGuiIO const &io = ImGui::GetIO(); ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; glm::mat4 const projection = editor_mode::projection_matrix(aspect); if (false == state.tool_dragging) state.tool_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(state.tool_handle - camerapos)); ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, glm::value_ptr(state.tool_gizmo), nullptr, nullptr); if (ImGuizmo::IsUsing()) { state.tool_dragging = true; auto const moved{camerapos + glm::dvec3(state.tool_gizmo[3])}; state.tool_handle = {moved.x, state.tool_handle.y, moved.z}; } else if (state.tool_dragging) { state.tool_dragging = false; apply_straight_tool(); } } void editor_mode::apply_straight_tool() { auto &state{m_straights}; auto const line{state.tool_line}; auto const offset{glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}}; 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}; auto const previous{state.tool}; state.tool = Tool; if (false == place_straight_tool()) { state.tool = previous; return; } state.tool_mouse = true; } void editor_mode::finish_straight_gesture() { auto &state{m_straights}; state.tool_mouse = false; apply_straight_tool(); state.tool = 0; 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_request = 0; m_track_tab = track_tab::straights; } void editor_mode::delete_selected_track() { auto *track{selected_track()}; if (track == nullptr || false == track->Dynamics.empty()) return; EditorSnapshot snap; snap.action = EditorSnapshot::Action::TrackEdit; snap.node_name = track->name(); snap.position = track->location(); snap.removed = {track}; if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); m_history.push_back(std::move(snap)); g_redo.clear(); editor_track::retire(*track); if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end()) m_route = {}; m_node = nullptr; ui()->set_node(nullptr); m_track_point = {}; straight_refresh(); } void editor_mode::draw_track_hints() { std::string hint; if (false == m_straights.detour.empty()) hint = "Shift+click: point " + std::to_string(m_straights.detour.size() + 1) + " of 4 (1: leave the axis, 2: shifted, 3: start back, 4: back on the axis) Esc: cancel"; else if (m_extend.active) hint = "Drag: curve through the cursor, along the end: straight Ctrl: straight"; else if (m_switch.placing) hint = "Drag along the straight to set the direction of the switch, sideways to set its side"; else if (m_switch.armed >= 0 && m_track_tab == track_tab::straights) hint = "Press on the selected straight where the switch starts and drag"; else if (m_track_tab == track_tab::straights) hint = current_straight().tracks.empty() ? "LMB: select a straight or a curve" : "Drag the ends or the middle Ctrl+drag: break Shift+click x4: shift around Alt+click: add to the set Drag from a red ring: new path Del: delete"; else if (m_track_tab == track_tab::turnout) hint = "Gizmo: move the switch (W: turn) Drag from a red ring: new path Del: delete"; else if (m_track_tab == track_tab::route) hint = m_route.chain.tracks.empty() ? "LMB: select a curve" : "Drag the vertex (yellow) or the radius grip (green), the change is applied on release Ctrl+Z: undo"; else hint = "LMB: select a path or a point handle"; hint += " K: split the path under the cursor O: top view"; ImGuiIO const &io = ImGui::GetIO(); auto *drawlist{ImGui::GetBackgroundDrawList()}; ImVec2 const position{12.0f, io.DisplaySize.y - 28.0f}; auto const size{ImGui::CalcTextSize(hint.c_str())}; drawlist->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 150), 4.0f); drawlist->AddText(position, IM_COL32(255, 255, 255, 230), hint.c_str()); } void editor_mode::update_build_tools() { glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; m_straights.detour_mouse = ground; if (m_extend.active) m_extend.mouse = ground; if (m_switch.placing) m_switch.mouse = ground; } bool editor_mode::start_extend() { auto *track{selected_track()}; if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) return false; screen_projection const projection; ImVec2 const mouse = ImGui::GetIO().MousePos; for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { auto const &path{track->m_paths[i]}; for (auto const atend : {false, true}) { editor_track::point_ref const point{i, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; if (editor_track::is_connected(*track, point)) continue; auto const position{editor_track::point_position(*track, point)}; ImVec2 screen; if (false == projection.project(position, screen)) continue; if ((screen.x - mouse.x) * (screen.x - mouse.x) + (screen.y - mouse.y) * (screen.y - mouse.y) > kHandleRadius * kHandleRadius * 1.5f) continue; 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; m_extend.active = true; m_extend.path = i; m_extend.atend = atend; m_extend.track = track; m_extend.point = position; m_extend.direction = glm::normalize(planar); m_extend.grade = tangent.y / glm::length(planar); m_extend.mouse = position; return true; } } return false; } 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 = editor_track::find_snap_target(*tool.track, tool.mouse, 5.0, {tool.track}); return Target.track != nullptr && glm::distance(Target.position, tool.point) > 0.5; } std::vector editor_mode::extend_pieces() const { std::vector result; auto const &tool{m_extend}; editor_track::snap_target target; if (extend_snap(target)) { alignment::design design; design.norms = m_route.design.norms; design.shape = m_route.design.shape; design.arc_piece_angle = 90.0; design.start = tool.point; design.end = target.position; design.start_direction = tool.direction; design.end_direction = glm::normalize(glm::dvec2{target.direction.x, target.direction.z}); auto const speed{editor_track::velocity(*tool.track)}; design.speed = speed > 0.0 ? speed : 100.0; if (false == alignment::collinear(design)) { alignment::vertex vertex; vertex.radius = 100000.0; auto const recommended{alignment::recommend(design.speed, 1000.0, design.norms)}; vertex.transition_in = vertex.transition_out = recommended.transition; glm::dvec2 intersection; if (alignment::tangent_intersection(design, intersection)) { design.vertices.push_back(vertex); } else { vertex.offset = glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z}) / 3.0; design.vertices = {vertex, vertex}; } } auto const shape{alignment::compute(design)}; if (shape.valid) return alignment::pieces(shape, design, 0); } glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; auto const chord{glm::length(offset)}; if (chord < 0.5) return result; auto const &direction{tool.direction}; auto const along{glm::dot(offset, direction)}; auto const lateral{direction.x * offset.y - direction.y * offset.x}; auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; }; if (ImGui::GetIO().KeyCtrl || std::abs(lateral) < 0.005 * chord + 0.05) { if (along < 0.5) return result; segment_data path; path.points[segment_data::point::start] = tool.point; path.points[segment_data::point::end] = {tool.point.x + direction.x * along, height(along), tool.point.z + direction.y * along}; result.push_back(path); return result; } auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; auto const radius{chord / (2.0 * std::sin(angle * 0.5))}; double const side{lateral > 0.0 ? 1.0 : -1.0}; glm::dvec2 const centre{glm::dvec2{tool.point.x, tool.point.z} + glm::dvec2{-direction.y, direction.x} * (side * radius)}; auto const count{std::max(1, static_cast(std::ceil(angle / glm::radians(90.0) - 1e-9)))}; auto const step{angle / count}; auto const handle{4.0 / 3.0 * std::tan(step / 4.0) * radius}; auto const turn = [&](glm::dvec2 const &Vector, double const Angle) { auto const c{std::cos(Angle)}; auto const s{std::sin(Angle)}; return glm::dvec2{Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; }; glm::dvec2 const radial{glm::dvec2{tool.point.x, tool.point.z} - centre}; for (int i = 0; i < count; ++i) { auto const a0{side * step * i}; auto const a1{side * step * (i + 1)}; auto const p0{centre + turn(radial, a0)}; auto const p3{centre + turn(radial, a1)}; auto const d0{turn(direction, a0)}; auto const d3{turn(direction, a1)}; auto const y0{height(radius * step * i)}; auto const y3{height(radius * step * (i + 1))}; segment_data path; path.points[segment_data::point::start] = {p0.x, y0, p0.y}; path.points[segment_data::point::end] = {p3.x, y3, p3.y}; path.points[segment_data::point::control1] = {d0.x * handle, tool.grade * handle, d0.y * handle}; path.points[segment_data::point::control2] = {-d3.x * handle, -tool.grade * handle, -d3.y * handle}; path.radius = static_cast(radius); result.push_back(path); } return result; } void editor_mode::finish_extend() { 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)) return; 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)); m_history.back().removed = std::move(removed); straight_refresh(); 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); push_track_snapshot({}, created); m_node = created.back(); ui()->set_node(m_node); straight_refresh(); } 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); } 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::straights && m_track_tab != track_tab::route)) return false; glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; tool.curved = false; if (m_track_tab == track_tab::route || current_straight().tracks.empty()) { auto *track{selected_track()}; if (track == nullptr || track->eType != tt_Normal || tool.templates[tool.armed].double_slip) 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) || false == editor_track::find_chain(curve.from, curve.to, chain, error)) return false; 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; tool.mouse = ground; tool.placing = true; return true; } auto const &line{current_straight()}; if (line.tracks.empty()) return false; if (tool.templates[tool.armed].double_slip) { insert_double_slip(line, ground, tool.templates[tool.armed]); return true; } tool.along = std::clamp(glm::dot(glm::dvec2{ground.x - line.start.x, ground.z - line.start.z}, line.direction), 0.0, line.length); tool.point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * tool.along; tool.mouse = ground; tool.line = line; tool.placing = true; return true; } void editor_mode::finish_switch_placement() { auto &tool{m_switch}; tool.placing = false; if (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}; 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); } 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)) return; push_track_snapshot(std::move(states), std::move(created)); m_history.back().removed = std::move(removed); straight_refresh(); } 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}; struct span { TTrack *track; int path; double from, to; bool forward; }; auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - Line.start.x, Point.z - Line.start.z}, Line.direction); }; 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{along(path.points[segment_data::point::start])}; auto const b{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}, 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) { auto const &shape{Shape}; if (shape.length > Line.length - 0.02) return false; auto from{std::clamp(Direction > 0 ? Along : Along - shape.length, 0.0, Line.length - shape.length)}; if (Snap) { 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(glm::dot(glm::dvec2{path.points[index].x - Line.start.x, path.points[index].z - Line.start.z}, Line.direction)); } 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 + shape.length)}; if (snappedstart != from) from = snappedstart; else if (snappedend != from + shape.length) from = snappedend - shape.length; from = std::clamp(from, 0.0, Line.length - shape.length); } 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)) 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 *track{editor_track::create_switch(shape, paths, Style != nullptr ? *Style : *first)}; if (track != nullptr) { editor_track::commit({track}); Created.push_back(track); } return true; } void editor_mode::draw_build_overlay() const { screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); auto const drawpath = [&](segment_data const &Path, ImU32 const Color) { auto previous{path_point(Path, 0.0)}; for (int i = 1; i <= 24; ++i) { auto const next{path_point(Path, i / 24.0)}; projection.line(drawlist, previous, next, Color, 3.0f); previous = next; } }; if (m_extend.active) { crossover_plan plan; editor_track::snap_target target; if (extend_snap(target)) { ImVec2 screen; if (projection.project(target.position, screen)) drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); for (auto const &piece : extend_pieces()) drawpath(piece, IM_COL32(255, 210, 60, 255)); } else if (plan_crossover(plan)) { for (auto const &piece : plan.paths) drawpath(piece, IM_COL32(255, 210, 60, 255)); drawpath(plan.insert, IM_COL32(60, 230, 90, 255)); } else { for (auto const &piece : extend_pieces()) drawpath(piece, IM_COL32(255, 210, 60, 255)); } } if (m_switch.placing) for (auto const &piece : switch_preview()) drawpath(piece, IM_COL32(255, 210, 60, 255)); if (false == m_straights.detour.empty()) { auto const outline{detour_outline()}; for (std::size_t i = 0; i + 1 < outline.size(); ++i) projection.line(drawlist, outline[i], outline[i + 1], IM_COL32(255, 210, 60, 255), 3.0f); for (std::size_t i = 0; i < outline.size() && i < m_straights.detour.size(); ++i) { ImVec2 screen; if (projection.project(outline[i], screen)) drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(255, 60, 60, 255)); } } auto const *track{selected_track()}; if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || m_extend.active) return; for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { for (auto const atend : {false, true}) { editor_track::point_ref const point{i, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; if (editor_track::is_connected(*track, point)) continue; ImVec2 screen; if (projection.project(editor_track::point_position(*track, point), screen)) drawlist->AddCircle(screen, 10.0f, IM_COL32(240, 60, 60, 255), 16, 3.0f); } } } void editor_mode::render_switch_ui() { auto &tool{m_switch}; if (false == ImGui::CollapsingHeader("Switches", ImGuiTreeNodeFlags_DefaultOpen)) return; if (false == tool.collected) { tool.templates = editor_track::standard_switch_templates(); auto const found{editor_track::find_switch_templates()}; tool.templates.insert(tool.templates.end(), found.begin(), found.end()); tool.collected = true; } ImGui::TextDisabled(tool.armed >= 0 ? "Press on the straight where the switch starts and drag: along sets its direction, sideways its side" : "Click a template, then drag on the selected straight"); ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, 150.0f), true); for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const &entry{tool.templates[i]}; auto label{entry.source != nullptr ? "Scenery: " + entry.label + " (" + std::to_string(entry.count) + "x)" : "PLK: " + entry.label}; label += "##switch" + std::to_string(i); if (ImGui::Selectable(label.c_str(), tool.armed == i)) tool.armed = (tool.armed == i ? -1 : i); } ImGui::EndChild(); if (ImGui::SmallButton("Collect again from the scenery")) { tool.collected = false; tool.armed = -1; } } 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 along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; auto const offset{points.size() >= 2 ? glm::dot(glm::dvec2{points[1].x - line.start.x, points[1].z - line.start.z}, normal) : 0.0}; auto const place = [&](double const Along, double const Lateral) { auto const planar{glm::dvec2{line.start.x, line.start.z} + 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(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(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); 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 along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; auto const offset{glm::dot(glm::dvec2{state.detour[1].x - line.start.x, state.detour[1].z - line.start.z}, normal)}; std::array stations{along(state.detour[0]), along(state.detour[1]), along(state.detour[2]), 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, 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{along(path.points[segment_data::point::start])}; auto const b{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}, 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 = 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{along(design.start)}; auto const endalong{along(design.end)}; auto const place = [&](double const Along, double const Lateral) { auto const planar{glm::dvec2{line.start.x, line.start.z} + line.direction * Along + normal * Lateral}; return planar; }; alignment::vertex vertex; vertex.radius = radius; route_recommend(vertex); if (false == state.auto_transitions) vertex.transition_in = vertex.transition_out = state.transition; 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) { auto &state{m_straights}; auto *before{editor_track::outside_neighbour(Line, false)}; auto *after{editor_track::outside_neighbour(Line, true)}; if (before == nullptr || after == nullptr) return false; if (false == editor_track::find_curve(*before, state.tolerance, m_route.design.norms.gauge, state.fit_before) || false == editor_track::find_curve(*after, state.tolerance, m_route.design.norms.gauge, state.fit_after)) return false; if (state.fit_before.reversals != 0 || state.fit_after.reversals != 0) return false; auto const far = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; m_route.from = far(state.fit_before, Line.start); m_route.to = far(state.fit_after, 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(glm::dvec2{End.x - Start.x, End.z - Start.z})}; auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; 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); if (Curve.compound) { vertex.compound = true; vertex.radius2 = std::round(Curve.radius2); vertex.transition_middle = std::round(Curve.transition_middle); vertex.split = Curve.split; } vertex.offset = std::max(0.1, Offset); return vertex; }; auto before{make(state.fit_before, first)}; auto after{make(state.fit_after, last)}; design.vertices = {before, after}; 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) return false; auto const &own{*tool.track}; auto const &main{own.m_paths[0]}; auto const &diverging{own.m_paths[1]}; auto const plan = [](glm::dvec3 const &Point) { return glm::dvec2{Point.x, Point.z}; }; auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; auto const origin{main.points[segment_data::point::start]}; auto const axis{glm::normalize(plan(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(frog - origin), perpendicular)}; if (std::abs(lateral) < 0.01) return false; auto const side{lateral > 0.0 ? 1.0 : -1.0}; auto const normal{perpendicular * side}; auto const offset{std::abs(lateral)}; auto const &control{diverging.points[segment_data::point::control2]}; auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan(control) : plan(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(path.points[segment_data::point::start])}; auto const b{plan(path.points[segment_data::point::end])}; auto const segment{b - a}; auto const t{std::clamp(glm::dot(plan(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; auto const distance{glm::distance(plan(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; auto const distance{glm::dot(plan(line.start) - plan(origin), normal)}; auto const remaining{distance - 2.0 * offset}; if (remaining < 0.01) return false; auto const length{remaining / std::sin(angle)}; glm::dvec2 const crossing{axis * std::cos(angle) + normal * std::sin(angle)}; auto const alongfrog{glm::dot(plan(frog - origin), axis)}; auto const otherfrog{plan(frog) + crossing * length}; auto const otherorigin{otherfrog + axis * alongfrog + normal * offset}; Plan.along = glm::dot(otherorigin - plan(line.start), line.direction); if (Plan.along < 0.0 || Plan.along > line.length) return false; Plan.direction = glm::dot(-axis, line.direction) > 0.0 ? 1 : -1; glm::dvec2 const direction{line.direction * static_cast(Plan.direction)}; Plan.side = glm::dot(glm::dvec2{-direction.y, direction.x}, -normal) > 0.0 ? 1 : -1; editor_track::switch_template shape; shape.source = tool.track; shape.length = glm::dot(plan(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(Vector), perpendicular) * side, Vector.y, glm::dot(plan(Vector), axis)}; }; local.points[segment_data::point::start] = tolocal(path.points[segment_data::point::start] - origin); local.points[segment_data::point::end] = tolocal(path.points[segment_data::point::end] - origin); local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]); local.points[segment_data::point::control2] = tolocal(path.points[segment_data::point::control2]); if (side < 0.0) local.rolls = {-local.rolls[0], -local.rolls[1]}; } Plan.shape = shape; auto const station{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Plan.along}; Plan.paths = editor_track::place_switch(shape, station, direction, Plan.side, line.grade * Plan.direction); Plan.insert = segment_data{}; Plan.insert.points[segment_data::point::start] = frog; Plan.insert.points[segment_data::point::end] = Plan.paths[1].points[segment_data::point::end]; return true; } 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, double const Radius, TTrack &Style) { auto const plan = [](glm::dvec3 const &Vector) { return glm::dvec2{Vector.x, Vector.z}; }; auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; 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; }; double const gap{0.25}; auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0}; auto const turn = [](glm::dvec2 const &Vector, double const Rotation) { auto const c{std::cos(Rotation)}; auto const s{std::sin(Rotation)}; return glm::dvec2{Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; }; auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) { glm::dvec2 const centre{plan(Start) + glm::dvec2{-Heading.y, Heading.x} * (Turn * Radius)}; auto const radial{plan(Start) - centre}; auto const p0{centre + turn(radial, Turn * From)}; auto const p3{centre + turn(radial, Turn * To)}; auto const d0{turn(Heading, Turn * From)}; auto const d3{turn(Heading, Turn * To)}; auto const handle{4.0 / 3.0 * std::tan((To - From) / 4.0) * Radius}; auto const y0{Start.y + (Finish.y - Start.y) * From / Angle}; auto const y3{Start.y + (Finish.y - Start.y) * To / Angle}; segment_data path; path.points[segment_data::point::start] = {p0.x, y0, p0.y}; path.points[segment_data::point::end] = {p3.x, y3, p3.y}; path.points[segment_data::point::control1] = {d0.x * handle, 0.0, d0.y * handle}; path.points[segment_data::point::control2] = {-d3.x * handle, 0.0, -d3.y * handle}; path.radius = static_cast(Radius); return path; }; auto const reversed = [](segment_data const &Path) { segment_data result{Path}; result.points[segment_data::point::start] = Path.points[segment_data::point::end]; result.points[segment_data::point::end] = Path.points[segment_data::point::start]; result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; result.rolls = {-Path.rolls[1], -Path.rolls[0]}; return result; }; auto const middle{Angle * 0.5}; auto const split{gap / Radius}; 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; for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr; ++i) base = "editor_dks" + std::to_string(i); editor_track::switch_template shape; shape.length = glm::distance(plan(PointA), Crossing); std::vector parts; parts.push_back(editor_track::create_switch(shape, {straight(PointA, centreA), arc(PointA, First, side, PointD, 0.0, middle - split)}, Style, base + "_a")); parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), reversed(arc(PointC, Second, -side, PointB, middle + split, Angle))}, Style, base + "_b")); 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), reversed(arc(PointA, First, side, PointD, middle + split, Angle))}, 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))); 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()); 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 plan = [](glm::dvec3 const &Vector) { return glm::dvec2{Vector.x, Vector.z}; }; auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; { 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 (track->eType == tt_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan(track->location()), plan(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(path.points[segment_data::point::start])}; auto const b{plan(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(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(Line.start) + Line.direction * along}; auto const distance{glm::distance(position, plan(Point))}; if (distance < best) { best = distance; other = track; crossing = position; } } } } if (other == nullptr) { state.status = "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))}; if (angle < glm::radians(0.5) || angle > glm::radians(30.0)) { state.status = "The straights cross at an angle unsuitable for a double slip"; return false; } auto const alongfirst{glm::dot(crossing - plan(Line.start), Line.direction)}; auto const alongsecond{glm::dot(crossing - plan(second.start), second.direction)}; auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5}; auto radius{Shape.radius}; auto half{radius * std::tan(angle * 0.5)}; if (half > available) { half = available; radius = half / std::tan(angle * 0.5); if (radius < 50.0) { state.status = "Not enough straight track around the crossing"; return false; } state.status = "R reduced to " + std::to_string(static_cast(radius)) + " m, the straights end " + std::to_string(static_cast(available)) + " 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(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 const parts{build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, radius, style != nullptr ? *style : *Line.tracks.front())}; editor_track::commit(parts); created.insert(created.end(), parts.begin(), parts.end()); push_track_snapshot(std::move(states), std::move(created)); m_history.back().removed = std::move(removed); straight_refresh(); return true; } bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape) { auto &state{m_straights}; auto const plan = [](glm::dvec3 const &Vector) { return glm::dvec2{Vector.x, Vector.z}; }; auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; 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 = "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(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 = "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(c.point) - plan(a.point), direction) / determinant}; glm::dvec2 const crossing{plan(a.point) + first * t}; auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; auto const oldhalf{glm::distance(plan(a.point), crossing)}; auto radius{Shape.radius}; auto half{radius * std::tan(angle * 0.5)}; 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 = half / std::tan(angle * 0.5); state.status = "R limited to " + std::to_string(static_cast(radius)) + " 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(line.end) - crossing, -e->inward))}; auto const reachstart{std::abs(glm::dot(plan(line.start) - crossing, -e->inward))}; if (std::max(reach, reachstart) - 0.5 < half) { half = std::min(half, std::max(reach, reachstart) - 0.5); radius = half / std::tan(angle * 0.5); state.status = "R reduced to " + std::to_string(static_cast(radius)) + " 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(e.point) - plan(line.start), line.direction)}; auto const alongnew{glm::dot(target - plan(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, radius, *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)); m_history.back().removed = std::move(removed); straight_refresh(); 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]}; auto const p0{path.points[segment_data::point::start]}; auto const p3{path.points[segment_data::point::end]}; auto const p1{p0 + path.points[segment_data::point::control1]}; auto const p2{p3 + path.points[segment_data::point::control2]}; bool const straight{path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}}; auto const length{glm::distance(p0, 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 u{1.0 - t}; glm::dvec3 position, derivative; if (straight) { position = glm::mix(p0, p3, t); derivative = p3 - p0; } else { position = u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3; derivative = 3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2); } if (false == forward) derivative = -derivative; if (false == result.empty()) station += glm::length(glm::dvec2{position.x - result.back().position.x, position.z - result.back().position.z}); curve_sample sample; sample.station = station; sample.position = position; sample.tangent = glm::normalize(glm::dvec2{derivative.x, derivative.z}); 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(glm::dvec2{Point.x, Point.z} - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)}; auto const distance{glm::distance(glm::dvec2{Point.x, Point.z}, 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{glm::dvec2{sample.position.x, sample.position.z} + 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{glm::distance(glm::dvec2{Start.x, Start.z}, glm::dvec2{End.x, End.z}) / 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; }; std::vector result; for (auto const &local : Shape.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{} ? glm::dvec2{control1.x, control1.z} : chord)}; auto const localend{glm::normalize(control2 != glm::dvec3{} ? -glm::dvec2{control2.x, control2.z} : chord)}; glm::dvec2 startdirection, enddirection; auto const p0{map(start, startdirection, localstart)}; auto const p3{map(end, enddirection, localend)}; auto path{hermite(p0, startdirection, p3, enddirection)}; 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; result.push_back(path); } if (false == result.empty()) result.front().radius = sample_at(Frame, Station).radius; 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}; auto station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; if (shape.length > total - 0.02) return; 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}; auto const bezier = [](TTrack const &Track, double const T) { auto const &path{Track.m_paths.front()}; auto const p0{path.points[segment_data::point::start]}; auto const p3{path.points[segment_data::point::end]}; auto const p1{p0 + path.points[segment_data::point::control1]}; auto const p2{p3 + path.points[segment_data::point::control2]}; auto const u{1.0 - T}; return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; }; 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{bezier(*candidate, t)}; if (glm::distance(glm::dvec2{found.x, found.z}, glm::dvec2{point.x, point.z}) > 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{bezier(*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 *track{editor_track::create_switch(shape, paths, *tool.frame_tracks.front())}; if (track != nullptr) { editor_track::commit({track}); created.push_back(track); } push_track_snapshot(std::move(states), std::move(created)); m_history.back().removed = std::move(removed); straight_refresh(); }