/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http: */ #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" #include "utilities/Globals.h" #include "rendering/renderer.h" #include "world/Track.h" #include "world/TrackFeatures.h" #include "scene/scene.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" #include "utilities/Logs.h" #include "utilities/translation.h" #include "imgui/imgui.h" #include "imgui/ImGuizmo.h" #include #include #include #include #include #include #include #include #include #include "application/editormodetrackshared.h" using namespace track_mode; void editor_mode::draw_straights_overlay() const { screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); glm::dvec3 const camera{Global.pCamera.Pos}; auto const nearby = [&](editor_track::straight const &Line) { auto const offset{camera - Line.start}; auto const along{std::clamp(glm::dot(plan_of(offset), Line.direction), 0.0, Line.length)}; auto const closest{Line.start + glm::dvec3{Line.direction.x, 0.0, Line.direction.y} * along}; return plan_distance(closest, camera) < 3000.0; }; auto const draw = [&](editor_track::straight const &Line, ImU32 const Color, float const Thickness) { if (Line.tracks.empty()) return; projection.line(drawlist, Line.start, Line.end, Color, Thickness); for (auto const &point : {Line.start, Line.end}) { ImVec2 screen; if (projection.project(point, screen)) drawlist->AddCircleFilled(screen, Thickness + 2.0f, Color); } }; for (int i = 0; i < static_cast(m_straights.found.size()); ++i) { auto const &line{m_straights.found[i]}; if (nearby(line)) draw(line, i == m_straights.listed ? overlay_color::selected : IM_COL32(255, 255, 255, 110), i == m_straights.listed ? 4.0f : 2.0f); } for (auto const &member : m_straights.set) draw(member, IM_COL32(255, 150, 30, 255), 4.0f); draw(m_straights.current, overlay_color::selected, 4.0f); if (m_straights.tool_placed) { auto const &state{m_straights}; auto const &line{state.tool_line}; ImVec2 screen; if (projection.project(state.tool_point, screen)) drawlist->AddCircleFilled(screen, 7.0f, overlay_color::invalid); if (projection.project(state.tool_handle, screen)) drawlist->AddCircleFilled(screen, 7.0f, overlay_color::marked); if (state.tool == 1) { auto const direction{plan_of(state.tool_handle - state.tool_point)}; if (glm::length(direction) > 1e-3) { auto const unit{glm::normalize(direction)}; auto const rest{std::max(0.0, line.length - state.tool_at)}; projection.line(drawlist, state.tool_point, state.tool_point + glm::dvec3{unit.x, line.grade, unit.y} * rest, overlay_color::marked, 3.0f); } } else { glm::dvec2 const normal{-line.direction.y, line.direction.x}; auto const offset{plan_of(state.tool_handle - state.tool_point)}; auto const shift{glm::dot(offset, normal)}; auto const length{glm::dot(offset, line.direction)}; auto const shifted{glm::dvec3{normal.x, 0.0, normal.y} * shift}; auto const along{glm::dvec3{line.direction.x, 0.0, line.direction.y}}; projection.line(drawlist, state.tool_point, state.tool_point + along * length + shifted, IM_COL32(255, 210, 60, 160), 2.0f); projection.line(drawlist, state.tool_point + along * length + shifted, line.end + shifted, overlay_color::marked, 3.0f); } } if (m_straights.dragging && m_straights.fitting) { auto const &result{m_route.result}; if (result.length > 0.0 && result.profile.size() >= 2) { int const steps{std::clamp(static_cast(result.length / 2.0), 2, 2000)}; auto previous{alignment::evaluate(result, 0.0).position}; for (int i = 1; i <= steps; ++i) { auto const next{alignment::evaluate(result, result.length * i / steps).position}; projection.line(drawlist, previous, next, result.valid ? overlay_color::marked : overlay_color::invalid, 3.0f); previous = next; } } } else if (m_straights.dragging) { for (auto const &member : m_straights.drag_lines) { for (auto const *track : member.tracks) { auto const &path{track->m_paths.front()}; projection.line(drawlist, path.points[segment_data::point::start], path.points[segment_data::point::end], overlay_color::marked, 3.0f); } } } else if (false == m_straights.current.tracks.empty()) { auto const &line{m_straights.current}; glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; for (int i = 0; i < 3; ++i) { ImVec2 screen; if (false == projection.project(handles[i], screen)) continue; if (i < 2) drawlist->AddRectFilled(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), overlay_color::selected); else drawlist->AddCircleFilled(screen, 6.0f, overlay_color::selected, 4); if (i == m_straights.handle) drawlist->AddCircle(screen, 12.0f, overlay_color::highlight, 16, 2.5f); } } auto line{m_straights.dragging ? m_straights.drag_line : m_straights.current}; if (line.tracks.empty()) return; glm::dvec3 const offset{m_straights.dragging ? m_straights.preview_start - m_straights.drag_line.start : glm::dvec3{}}; if (m_straights.dragging) { line.start = m_straights.preview_start; line.end = m_straights.preview_end; auto const planar{plan_of(line.end - line.start)}; auto const length{glm::length(planar)}; if (length > 1e-6) { line.direction = planar / length; line.length = length; } } auto const offer{straight_parallel_offer(m_straights.dragging ? m_straights.drag_line : m_straights.current, offset)}; glm::dvec2 const normal{-line.direction.y, line.direction.x}; for (auto const &other : m_straights.neighbours) { auto const across = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - line.start), normal); }; auto const a0{line.along(other.start)}, a1{line.along(other.end)}; auto const from{std::max(0.0, std::min(a0, a1))}; auto const to{std::min(line.length, std::max(a0, a1))}; if (to <= from) continue; auto const middle{(from + to) * 0.5}; auto const fraction{std::abs(a1 - a0) > 1e-6 ? (middle - a0) / (a1 - a0) : 0.0}; auto const lateral{across(other.start) + (across(other.end) - across(other.start)) * fraction}; glm::dvec3 const foot{line.start.x + line.direction.x * middle, line.start.y + line.grade * middle, line.start.z + line.direction.y * middle}; glm::dvec3 const target{foot.x + normal.x * lateral, foot.y, foot.z + normal.y * lateral}; bool const highlighted{offer.nearby && offer.neighbour == &other}; auto const tick{highlighted ? (offer.snaps ? IM_COL32(80, 230, 255, 255) : IM_COL32(255, 210, 60, 255)) : IM_COL32(255, 230, 120, 230)}; if (highlighted) projection.line(drawlist, other.start, other.end, tick, 3.0f); projection.line(drawlist, foot, target, tick, highlighted ? 3.0f : 1.5f); ImVec2 screen; if (projection.project((foot + target) * 0.5, screen)) { auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))}; auto const text{highlighted ? (offer.snaps ? format(STR_C("parallel %.2f m"), offer.spacing) : format("%.2f → %.2f m %.1f°", std::abs(lateral), offer.spacing, offer.angle)) : (angle > 0.05 ? format("%.2f m %.1f°", std::abs(lateral), angle) : format("%.2f m", std::abs(lateral)))}; auto const *label{text.c_str()}; auto const size{ImGui::CalcTextSize(label)}; drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 4.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 190), 3.0f); drawlist->AddText(screen, highlighted ? tick : IM_COL32(255, 230, 120, 255), label); } } if (offer.nearby && false == offer.snaps && std::abs(offer.correction) > 0.02) { auto const shift{glm::dvec3{normal.x, 0.0, normal.y} * offer.correction}; projection.line(drawlist, line.start + shift, line.end + shift, IM_COL32(80, 230, 255, 210), 3.5f); } } void editor_mode::find_neighbour_straights() { auto &state{m_straights}; state.neighbours.clear(); auto const &line{state.current}; if (line.tracks.empty()) return; glm::dvec2 const normal{-line.direction.y, line.direction.x}; auto const middle{(line.start + line.end) * 0.5}; std::vector visited(line.tracks.begin(), line.tracks.end()); auto const sections{simulation::Region->sections(middle, static_cast(line.length * 0.5 + 60.0))}; for (auto *section : sections) { for (auto const &cell : section->m_cells) { for (auto *track : cell.m_paths) { if (std::find(visited.begin(), visited.end(), track) != visited.end() || false == editor_track::is_straight(*track, state.tolerance)) continue; auto other{editor_track::find_straight(*track, state.tolerance)}; visited.insert(visited.end(), other.tracks.begin(), other.tracks.end()); if (other.tracks.empty() || std::abs(line.direction.x * other.direction.y - line.direction.y * other.direction.x) > std::sin(glm::radians(kParallelFindAngle))) continue; auto const offset{glm::dvec2{(other.start + other.end).x * 0.5 - line.start.x, (other.start + other.end).z * 0.5 - line.start.z}}; if (std::abs(glm::dot(offset, normal)) > 60.0) continue; auto const a0{line.along(other.start)}; auto const a1{line.along(other.end)}; if (std::max(a0, a1) <= 0.0 || std::min(a0, a1) >= line.length) continue; state.neighbours.push_back(std::move(other)); } } } } editor_mode::parallel_offer editor_mode::straight_parallel_offer(editor_track::straight const &Line, glm::dvec3 const &Offset) const { parallel_offer offer; glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; auto const middle{(Line.start + Line.end) * 0.5 + Offset}; double best{kParallelHintWindow}; for (auto const &other : m_straights.neighbours) { auto const sine{std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x)}; if (sine > std::sin(glm::radians(kParallelFindAngle))) continue; auto const distance{glm::dot(plan_of(other.start - middle), normal)}; auto const angle{glm::degrees(std::abs(std::asin(std::clamp(sine, 0.0, 1.0))))}; for (auto const spacing : m_straights.spacings) { auto const error{std::abs(std::abs(distance) - spacing)}; if (error < best) { best = error; offer.nearby = true; offer.snaps = error < kParallelSnapWindow && angle <= kParallelSnapAngle; offer.spacing = spacing; offer.distance = std::abs(distance); offer.angle = angle; offer.correction = distance > 0.0 ? std::abs(distance) - spacing : spacing - std::abs(distance); offer.neighbour = &other; } } } return offer; } glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const { auto const offer{straight_parallel_offer(Line, Offset)}; if (false == offer.snaps) return Offset; glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; return Offset + glm::dvec3{normal.x, 0.0, normal.y} * offer.correction; } glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const { glm::dvec2 const offset{Moved.x - Pivot.x, Moved.z - Pivot.z}; auto const length{glm::length(offset)}; if (length < 1e-3) return Moved; auto const direction{offset / length}; for (auto const &other : m_straights.neighbours) { auto const sine{direction.x * other.direction.y - direction.y * other.direction.x}; if (std::abs(sine) > std::sin(glm::radians(kParallelSnapAngle))) continue; auto const aligned{other.direction * (glm::dot(direction, other.direction) < 0.0 ? -1.0 : 1.0)}; return {Pivot.x + aligned.x * length, Moved.y, Pivot.z + aligned.y * length}; } return Moved; } void editor_mode::render_straights_ui() { auto &state{m_straights}; ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", STR_C("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to")); ImGui::PopTextWrapPos(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.42f, 0.24f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.52f, 0.30f, 1.0f)); if (ImGui::Button(STR_C("Recognize straights in the scenery"), ImVec2(-FLT_MIN, ImGui::GetFrameHeight() * 1.4f))) { state.found = editor_track::find_straights(state.minimum_length, state.tolerance); state.listed = -1; state.current_for = nullptr; } ImGui::PopStyleColor(2); if (ImGui::TreeNode(STR_C("What counts as one straight"))) { if (begin_fields("##straighttolerance", 0.55f)) { double angle{glm::degrees(state.tolerance.angle)}; field_row("Angle tolerance (deg)"); if (ImGui::InputDouble("##straightangle", &angle, 0.0, 0.0, "%.4f")) state.tolerance.angle = glm::radians(std::max(0.0, angle)); item_tooltip("Largest change of the direction between the paths of one straight"); field_row("Offset tolerance (m)"); ImGui::InputDouble("##straightoffset", &state.tolerance.offset, 0.0, 0.0, "%.3f"); item_tooltip("Largest distance of the paths from the common axis of the straight"); field_row("Treat as straight above R (m)"); ImGui::InputDouble("##straightradius", &state.tolerance.radius, 0.0, 0.0, "%.0f"); item_tooltip("Curves of a larger radius count as straight"); field_row("Minimum length (m)"); ImGui::InputDouble("##straightminimum", &state.minimum_length, 0.0, 0.0, "%.0f"); item_tooltip("Shorter straights aren't listed"); ImGui::EndTable(); } ImGui::TreePop(); } if (false == state.found.empty()) { ImGui::AlignTextToFramePadding(); ImGui::Text(STR_C("%zu straights"), state.found.size()); ImGui::SameLine(); ImGui::SetNextItemWidth(-FLT_MIN); ImGui::InputTextWithHint("##straightfilter", STR_C("path name"), state.filter, sizeof(state.filter)); ImGui::BeginChild("##straights", ImVec2(0.0f, 160.0f), true); for (int i = 0; i < static_cast(state.found.size()); ++i) { auto const &line{state.found[i]}; if (state.filter[0] != '\0' && std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return Track->name().find(state.filter) != std::string::npos; })) continue; auto const label{std::to_string(i + 1) + ". " + describe(line) + "##straight" + std::to_string(i)}; if (ImGui::Selectable(label.c_str(), state.listed == i)) { state.listed = i; m_node = line.tracks[line.tracks.size() / 2]; ui()->set_node(m_node); start_focus(m_node); } } ImGui::EndChild(); } } void editor_mode::render_straight_ui() { auto &state{m_straights}; auto const &line{current_straight()}; if (line.tracks.empty()) { if (false == state.status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); return; } metric_tiles({{format("%.2f m", line.length), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)}, {format("%.2f\xc2\xb0", line.azimuth), STR_C("azimuth"), ImGui::GetColorU32(ImGuiCol_Text)}, {format("%+.2f \xe2\x80\xb0", line.grade * 1000.0), STR_C("grade"), ImGui::GetColorU32(ImGuiCol_Text)}, {format("%zu", line.tracks.size()), STR_C("paths"), ImGui::GetColorU32(ImGuiCol_TextDisabled)}}); if (false == m_straights.neighbours.empty()) ImGui::TextDisabled("%s", STR_C("Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m")); // what else can be done with the straight, as tiles with a sketch of it auto const gap{std::round(ImGui::GetFontSize() * 0.3f)}; auto const width{std::floor((ImGui::GetContentRegionAvail().x - 2.0f * gap) / 3.0f)}; auto const toolbutton = [&](char const *Label, int const Tool, char const *Tooltip, std::function const &Sketch) { bool const active{state.tool == Tool}; if (sketch_tile(Label, Label, active, width, Sketch)) { state.tool = active ? 0 : Tool; state.tool_placed = false; state.handle = -1; } item_tooltip(Tooltip); }; auto const at = [](ImVec2 const Low, ImVec2 const High, float const X, float const Y) { return ImVec2(Low.x + (High.x - Low.x) * X, Low.y + (High.y - Low.y) * Y); }; toolbutton(STR_C("Break"), 1, STR_C("Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)"), [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32 Made) { List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 1.0f, 0.8f), (Line & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 60), 1.0f); List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 0.4f, 0.8f), Line, 2.0f); List.AddBezierQuadratic(at(Low, High, 0.4f, 0.8f), at(Low, High, 0.62f, 0.8f), at(Low, High, 1.0f, 0.1f), Made, 2.0f); }); ImGui::SameLine(0.0f, gap); toolbutton(STR_C("S-curve"), 2, STR_C("Shifts a piece of the straight sideways around an obstacle and back: four clicks on the straight (Shift+click in the view does the same)"), [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32 Made) { List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 1.0f, 0.8f), (Line & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 60), 1.0f); List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 0.15f, 0.8f), Line, 2.0f); List.AddBezierCubic(at(Low, High, 0.15f, 0.8f), at(Low, High, 0.3f, 0.8f), at(Low, High, 0.25f, 0.2f), at(Low, High, 0.42f, 0.2f), Made, 2.0f); List.AddLine(at(Low, High, 0.42f, 0.2f), at(Low, High, 0.58f, 0.2f), Made, 2.0f); List.AddBezierCubic(at(Low, High, 0.58f, 0.2f), at(Low, High, 0.75f, 0.2f), at(Low, High, 0.7f, 0.8f), at(Low, High, 0.85f, 0.8f), Made, 2.0f); List.AddLine(at(Low, High, 0.85f, 0.8f), at(Low, High, 1.0f, 0.8f), Line, 2.0f); List.AddCircleFilled(at(Low, High, 0.5f, 0.8f), 2.5f, IM_COL32(230, 90, 70, 255)); }); ImGui::SameLine(0.0f, gap); bool const member{in_straight_set(line)}; auto const grouped{sketch_tile("##group", member ? STR_C("Leave the group") : STR_C("Move with others"), member, width, [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32 Made) { List.AddLine(at(Low, High, 0.0f, 0.2f), at(Low, High, 1.0f, 0.2f), Line, 2.0f); List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 1.0f, 0.8f), Line, 2.0f); for (auto const x : {0.3f, 0.7f}) { List.AddLine(at(Low, High, x, 0.32f), at(Low, High, x, 0.68f), Made, 1.5f); List.AddTriangleFilled(at(Low, High, x - 0.08f, 0.38f), at(Low, High, x + 0.08f, 0.38f), at(Low, High, x, 0.26f), Made); List.AddTriangleFilled(at(Low, High, x - 0.08f, 0.62f), at(Low, High, x + 0.08f, 0.62f), at(Low, High, x, 0.74f), Made); } })}; if (grouped) { if (member) state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return std::find(Member.tracks.begin(), Member.tracks.end(), Track) != Member.tracks.end(); }); }), state.set.end()); else state.set.push_back(line); } item_tooltip("Straights of a group move together and stay parallel; Alt+click in the view adds another one"); if (state.tool != 0) { ImGui::TextDisabled("%s", STR_C("Curve R (m, 0: automatic)")); ImGui::SetNextItemWidth(-FLT_MIN); ImGui::InputDouble("##straightcurve", &state.curve_radius, 10.0, 100.0, "%.0f"); state.curve_radius = std::max(0.0, state.curve_radius); item_tooltip("Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line"); value_chips("straightcurves", state.curve_radius, {0.0, 300.0, 600.0, 1000.0, 2000.0}, "%.0f"); ImGui::TextDisabled("%s", STR_C("Transition curves")); int transitions{state.auto_transitions ? 0 : 1}; if (segmented("straighttransitions", transitions, {{0, STR_C("From the line speed"), "Lengths of the transition curves from the speed of the paths and the limits of the curve design"}, {1, STR_C("Given length"), "The same length of the transition curves at every curve"}})) state.auto_transitions = transitions == 0; if (false == state.auto_transitions) { ImGui::SetNextItemWidth(-FLT_MIN); ImGui::InputDouble("##straighttransition", &state.transition, 1.0, 10.0, "%.1f m"); state.transition = std::max(0.0, state.transition); } } if (false == state.status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); if (false == state.set.empty()) { ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.15f, 1.0f), STR_C("Group: %zu straights move together"), state.set.size()); ImGui::SameLine(); if (ImGui::SmallButton(STR_C("Clear"))) state.set.clear(); } if (false == ImGui::IsAnyItemActive()) { state.edit_start = line.start; state.edit_end = line.end; state.edit_length = line.length; state.edit_azimuth = line.azimuth; } if (ImGui::CollapsingHeader(STR_C("Exact values"))) { if (begin_fields("##straightexact", 0.3f)) { field_row("Start (x, y, z)"); ImGui::InputScalarN("##straightstart", ImGuiDataType_Double, glm::value_ptr(state.edit_start), 3, nullptr, nullptr, "%.3f"); field_row("End (x, y, z)"); ImGui::InputScalarN("##straightend", ImGuiDataType_Double, glm::value_ptr(state.edit_end), 3, nullptr, nullptr, "%.3f"); ImGui::EndTable(); } if (ImGui::Button(STR_C("Apply ends"), ImVec2(-FLT_MIN, 0.0f))) straight_apply(line, state.edit_start, state.edit_end); if (begin_fields("##straightmeasures", 0.3f)) { field_row("Length (m)"); ImGui::InputDouble("##straightlength", &state.edit_length, 0.0, 0.0, "%.3f"); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); if (ImGui::Button(STR_C("Set from the start##length"), ImVec2(-FLT_MIN, 0.0f))) { 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)); } field_row("Azimuth (deg)"); ImGui::InputDouble("##straightazimuth", &state.edit_azimuth, 0.0, 0.0, "%.5f"); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); if (ImGui::Button(STR_C("Turn around the start"), ImVec2(-FLT_MIN, 0.0f))) { 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::EndTable(); } } } bool editor_mode::straights_active() { return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights && false == current_straight().tracks.empty(); } int editor_mode::straight_handle_hit() { if (false == straights_active()) return -1; auto const &line{current_straight()}; glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; screen_projection const projection; float best = kHandleRadius * kHandleRadius; int hit{-1}; for (int i = 0; i < 3; ++i) { if (auto const distance{projection.mouse_distance2(handles[i])}; distance < best) { best = distance; hit = i; } } return hit; } void editor_mode::straight_refresh() { auto &state{m_straights}; state.current_for = selected_track(); state.current = state.current_for != nullptr ? editor_track::find_straight(*state.current_for, state.tolerance) : editor_track::straight{}; find_neighbour_straights(); for (auto &line : state.found) if (false == line.tracks.empty()) line = editor_track::find_straight(*line.tracks.front(), state.tolerance); for (auto &line : state.set) if (false == line.tracks.empty()) line = editor_track::find_straight(*line.tracks.front(), state.tolerance); } bool editor_mode::in_straight_set(editor_track::straight const &Line) const { for (auto const &member : m_straights.set) for (auto const *track : Line.tracks) if (std::find(member.tracks.begin(), member.tracks.end(), track) != member.tracks.end()) return true; return false; } void editor_mode::straight_apply(editor_track::straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End) { straights_apply({Line}, {{Start, End}}); } void editor_mode::straights_apply(std::vector const &Lines, std::vector> const &Ends) { auto &state{m_straights}; state.status.clear(); auto const tracks{editor_track::straight_affected(Lines)}; if (false == tracks_editable(tracks, state.status)) return; std::vector> states; for (auto *track : tracks) states.emplace_back(track, editor_track::capture(*track)); push_track_snapshot(std::move(states)); editor_track::move_straights(Lines, Ends); editor_track::commit(tracks); straight_refresh(); } void editor_mode::render_straight_gizmo() { auto &state{m_straights}; if (state.tool_mouse) { glm::dvec3 const ground{cursor_ground()}; state.tool_handle = {ground.x, state.tool_handle.y, ground.z}; typed_turn(); return; } auto const &line{state.dragging ? state.drag_line : current_straight()}; if (state.handle < 0 || line.tracks.empty()) { state.dragging = false; return; } glm::dvec3 const anchors[] = {line.start, line.end, (line.start + line.end) * 0.5}; glm::dvec3 handled{anchors[state.handle]}; bool const held{handle_drag_step(handled)}; if (false == held && false == state.dragging) return; if (held) { if (false == state.dragging) { std::vector lines; if (in_straight_set(line)) { for (auto const &member : state.set) if (false == member.tracks.empty()) lines.push_back(editor_track::find_straight(*member.tracks.front(), state.tolerance)); } else { lines.push_back(line); } state.fitting = lines.size() == 1 && start_curve_fit(line, state.handle); auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; if (false == tracks_editable(tracks, state.status)) { state.fitting = false; return; } state.dragging = true; state.drag_line = line; state.drag_lines = lines; state.drag_states.clear(); for (auto *track : tracks) state.drag_states.emplace_back(track, editor_track::capture(*track)); if (false == state.fitting) { push_track_snapshot(state.drag_states); m_track_drag = tracks; } } auto const &grabbed{state.drag_line}; glm::dvec3 moved{handled}; std::function transform; if (state.handle == 2) { auto offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)}; if (auto const values{typed_values()}; false == values.empty()) offset = glm::dvec3{-grabbed.direction.y, 0.0, grabbed.direction.x} * values[0]; transform = [offset](glm::dvec3 const &Point) { return Point + offset; }; } else { auto const pivot{state.handle == 0 ? grabbed.end : grabbed.start}; auto const original{state.handle == 0 ? grabbed.start : grabbed.end}; moved = snap_straight_direction(pivot, moved); if (auto const values{typed_values()}; false == values.empty() && values[0] > 0.1) { glm::dvec2 heading{moved.x - pivot.x, moved.z - pivot.z}; if (glm::length(heading) < 1e-6) heading = {original.x - pivot.x, original.z - pivot.z}; heading = glm::normalize(heading) * values[0]; moved.x = pivot.x + heading.x; moved.z = pivot.z + heading.y; } glm::dvec2 const before{original.x - pivot.x, original.z - pivot.z}; glm::dvec2 const after{moved.x - pivot.x, moved.z - pivot.z}; auto const lengthbefore{glm::length(before)}; auto const lengthafter{std::max(0.1, glm::length(after))}; auto const axis{before / std::max(1e-9, lengthbefore)}; auto const newaxis{after / lengthafter}; auto const scale{lengthafter / std::max(1e-9, lengthbefore)}; auto const rise{moved.y - original.y}; transform = [=](glm::dvec3 const &Point) { glm::dvec2 const offset{Point.x - pivot.x, Point.z - pivot.z}; auto const along{glm::dot(offset, axis)}; auto const across{axis.x * offset.y - axis.y * offset.x}; auto const planar{plan_of(pivot) + newaxis * (along * scale) + glm::dvec2{-newaxis.y, newaxis.x} * across}; return glm::dvec3{planar.x, Point.y + rise * along / std::max(1e-9, lengthbefore), planar.y}; }; } std::vector> ends; for (auto const &member : state.drag_lines) ends.emplace_back(transform(member.start), transform(member.end)); state.preview_start = transform(grabbed.start); state.preview_end = transform(grabbed.end); if (state.fitting) { update_curve_fit(state.preview_start, state.preview_end); return; } for (auto const &entry : state.drag_states) editor_track::apply(*entry.first, entry.second); editor_track::move_straights(state.drag_lines, ends); m_track_dirty = true; commit_track_drag(false); } else if (state.dragging) { if (state.fitting) { state.fitting = false; state.dragging = false; state.drag_lines.clear(); if (m_route.result.valid) route_apply(); straight_refresh(); return; } commit_track_drag(true); state.dragging = false; m_track_drag.clear(); state.drag_states.clear(); state.drag_lines.clear(); straight_refresh(); } } void editor_mode::cancel_track_tools() { auto &state{m_straights}; if (state.dragging) { // the geometry goes back to what it was before the drag if (false == state.fitting) { for (auto const &entry : state.drag_states) editor_track::apply(*entry.first, entry.second); m_track_dirty = true; commit_track_drag(true); m_track_drag.clear(); } state.dragging = false; state.fitting = false; state.drag_states.clear(); state.drag_lines.clear(); straight_refresh(); } m_extend.active = false; m_turntable.placing = false; m_signal.placing = false; m_signal.moving = false; m_handle_drag = {}; if (m_point_drag.active) { m_track_snap = {}; point_drag_finish(); } if (m_switch.placing) { m_switch.placing = false; m_switch.status = STR_C("Placing of the switch cancelled"); } else if (m_switch.armed >= 0) { m_switch.armed = -1; m_switch.status.clear(); } m_track_point = {}; m_route.vertex = -1; m_route.grip = -1; state.handle = -1; state.tool_placed = false; state.detour.clear(); } void editor_mode::straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius) { auto &state{m_straights}; state.status.clear(); struct span { TTrack *track; int path; double from; double to; bool forward; }; auto line{Line}; auto const point = [&](double const Along) { auto const planar{plan_of(line.start) + line.direction * Along}; return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; }; std::vector spans; auto const collect = [&]() { spans.clear(); for (std::size_t i = 0; i < line.tracks.size(); ++i) { auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; auto const a{line.along(path.points[segment_data::point::start])}; auto const b{line.along(path.points[segment_data::point::end])}; spans.push_back({line.tracks[i], line.paths[i], std::min(a, b), std::max(a, b), a < b}); } std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); }; int first{-1}; int last{-1}; auto const select = [&](double const End) { first = -1; last = -1; for (int i = 0; i < static_cast(spans.size()); ++i) { if (spans[i].to > From && spans[i].from < End) { if (first < 0) first = i; last = i; } } }; auto const keep_regular = [&]() { if (first < 0) return; auto const centre{(std::max(From, 0.0) + std::min(To, line.length)) * 0.5}; int middle{first}; for (int i = first; i <= last; ++i) if (spans[i].from <= centre && spans[i].to >= centre) middle = i; if (spans[middle].track->eType != tt_Normal) { int best{-1}; for (int i = first; i <= last; ++i) if (spans[i].track->eType == tt_Normal && (best < 0 || std::abs(i - middle) < std::abs(best - middle))) best = i; if (best < 0) { first = -1; last = -1; return; } middle = best; } int low{middle}; int high{middle}; while (low > first && spans[low - 1].track->eType == tt_Normal) --low; while (high < last && spans[high + 1].track->eType == tt_Normal) ++high; first = low; last = high; }; collect(); select(To); keep_regular(); if (first < 0) return; std::vector> splitstates; std::vector splitcreated; auto end{To}; if (last + 1 >= static_cast(spans.size())) { auto const final{spans.back()}; if (final.track->eType != tt_Normal) return; auto const length{final.to - final.from}; auto const cut{To < final.to - 1.0 ? std::max(To, final.from + std::min(1.0, length * 0.5)) : final.from + length * 0.7}; splitstates.emplace_back(final.track, editor_track::capture(*final.track)); editor_track::chain split; split.tracks = {final.track}; split.forward = {final.forward}; std::vector pieces(2); pieces[0].points[segment_data::point::start] = point(final.from); pieces[0].points[segment_data::point::end] = point(cut); pieces[1].points[segment_data::point::start] = point(cut); pieces[1].points[segment_data::point::end] = point(final.to); splitcreated = editor_track::relay(split, pieces); line = editor_track::find_straight(*line.tracks.front(), state.tolerance); collect(); end = std::min(To, cut); select(end); keep_regular(); if (first < 0 || last + 1 >= static_cast(spans.size())) { for (auto *track : splitcreated) editor_track::retire(*track); editor_track::apply(*splitstates.front().first, splitstates.front().second); editor_track::commit({splitstates.front().first}); return; } } auto const undo_split = [&]() { for (auto *track : splitcreated) editor_track::retire(*track); for (auto const &entry : splitstates) { editor_track::apply(*entry.first, entry.second); editor_track::commit({entry.first}); } }; editor_track::straight tail; tail.direction = line.direction; for (int i = last + 1; i < static_cast(spans.size()); ++i) { tail.tracks.push_back(spans[i].track); tail.paths.push_back(spans[i].path); } tail.start = point(spans[last + 1].from); tail.end = line.end; tail.length = line.length - spans[last + 1].from; tail.grade = line.grade; std::vector chainpaths; for (int i = first; i <= last; ++i) chainpaths.push_back(spans[i].track); auto tracks{editor_track::straight_affected({tail})}; for (auto *track : chainpaths) if (std::find(tracks.begin(), tracks.end(), track) == tracks.end()) tracks.push_back(track); if (false == tracks_editable(tracks, m_straights.status)) { undo_split(); return; } std::vector> states; for (auto *track : tracks) states.emplace_back(track, editor_track::capture(*track)); editor_track::move_straights({tail}, {{Tail(tail.start), Tail(tail.end)}}); m_route.from = spans[first].track; m_route.to = spans[last].track; route_reset(); if (m_route.chain.tracks.empty() || false == m_route.chain.switches.empty()) { if (false == m_route.chain.switches.empty()) m_route.error = STR_C("The curve would run through a switch"); for (auto const &entry : states) editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); undo_split(); state.status = m_route.error; return; } m_route.design.start_reserve = 0.0; m_route.design.end_reserve = 0.0; for (auto &vertex : m_route.design.vertices) { vertex.radius = Radius; route_recommend(vertex); if (false == state.auto_transitions) { vertex.transition_in = state.transition; vertex.transition_out = state.transition; } } route_update(); if (false == m_route.result.valid) { for (auto const &entry : states) editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); undo_split(); state.status = m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front(); return; } auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; editor_track::keep_heights(m_route.chain, pieces); auto created{editor_track::relay(m_route.chain, pieces, &m_route.to)}; editor_track::commit(tracks); for (auto const &entry : splitstates) { auto const found{std::find_if(states.begin(), states.end(), [&](auto const &State) { return State.first == entry.first; })}; if (found != states.end()) found->second = entry.second; else states.push_back(entry); } created.insert(created.begin(), splitcreated.begin(), splitcreated.end()); { auto changed{created}; for (auto const &entry : states) changed.push_back(entry.first); digitize_drawn(changed); } push_track_snapshot(std::move(states), std::move(created)); std::string error; editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true); straight_refresh(); state.status = STR_C("Done: click a curve to adjust it; Ctrl+Z takes it back"); } double editor_mode::straight_tool_radius(editor_track::straight const &Line) const { if (m_straights.curve_radius > 0.0) return m_straights.curve_radius; double speed{-1.0}; for (auto const *track : Line.tracks) speed = std::max(speed, editor_track::velocity(*track)); if (speed <= 0.0) speed = 100.0; auto const recommended{alignment::recommend(speed, 1000.0, m_route.design.norms)}; return std::max(150.0, std::ceil(recommended.radius_min * 1.5 / 50.0) * 50.0); } bool editor_mode::place_straight_tool() { auto &state{m_straights}; if (ui()->mode() != nodebank_panel::TRACK || m_track_tab != track_tab::straights || state.tool == 0) return false; if (state.tool == 2) { add_detour_point(); return true; } auto const &line{current_straight()}; if (line.tracks.empty()) return false; glm::dvec3 const ground{cursor_ground()}; auto const along{std::clamp(line.along(ground), 0.0, line.length)}; glm::dvec3 const axis{line.direction.x, line.grade, line.direction.y}; state.tool_line = line; state.tool_at = along; state.tool_point = line.start + axis * along; auto const reach{std::min(50.0, std::max(1.0, line.length - along))}; state.tool_handle = state.tool_point + axis * reach; state.tool_placed = true; state.handle = -1; return true; } void editor_mode::apply_straight_tool() { auto &state{m_straights}; auto const line{state.tool_line}; auto const offset{plan_of(state.tool_handle - state.tool_point)}; auto const radius{straight_tool_radius(line)}; if (state.tool == 1) { if (glm::length(offset) < 1e-3) return; auto const unit{glm::normalize(offset)}; auto const angle{std::atan2(line.direction.x * unit.y - line.direction.y * unit.x, glm::dot(line.direction, unit))}; if (std::abs(angle) < 1e-5) return; glm::dvec2 const pivot{state.tool_point.x, state.tool_point.z}; double speed{-1.0}; for (auto const *track : line.tracks) speed = std::max(speed, editor_track::velocity(*track)); auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; auto const reach{radius * std::tan(std::min(std::abs(angle), 3.0) * 0.5) + transition * 0.5 + 10.0}; straight_reshape(line, state.tool_at - reach, state.tool_at + reach, [pivot, angle](glm::dvec3 const &Point) { glm::dvec2 const relative{Point.x - pivot.x, Point.z - pivot.y}; auto const c{std::cos(angle)}; auto const s{std::sin(angle)}; return glm::dvec3{pivot.x + relative.x * c - relative.y * s, Point.y, pivot.y + relative.x * s + relative.y * c}; }, radius); } else { glm::dvec2 const normal{-line.direction.y, line.direction.x}; auto const shift{glm::dot(offset, normal)}; auto const length{glm::dot(offset, line.direction)}; if (std::abs(shift) < 1e-3 || length < 1.0) return; auto const translation{normal * shift}; straight_reshape(line, state.tool_at, state.tool_at + length, [translation](glm::dvec3 const &Point) { return glm::dvec3{Point.x + translation.x, Point.y, Point.z + translation.y}; }, radius); } state.tool_placed = false; } void editor_mode::start_straight_gesture(int const Tool) { auto &state{m_straights}; state.tool_before = state.tool; state.tool = Tool; if (false == place_straight_tool()) { state.tool = state.tool_before; return; } state.tool_mouse = true; } void editor_mode::finish_straight_gesture() { auto &state{m_straights}; state.tool_mouse = false; apply_straight_tool(); state.tool = state.tool_before; state.tool_placed = false; } void editor_mode::toggle_straight_set(TTrack &Track) { auto &state{m_straights}; auto const line{editor_track::find_straight(Track, state.tolerance)}; if (line.tracks.empty()) return; if (in_straight_set(line)) state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Other) { return std::find(Member.tracks.begin(), Member.tracks.end(), Other) != Member.tracks.end(); }); }), state.set.end()); else state.set.push_back(line); m_node = &Track; ui()->set_node(m_node); m_track_tab = track_tab::straights; } std::vector editor_mode::detour_outline() const { auto const &state{m_straights}; std::vector result; if (state.detour.empty()) return result; auto const &line{state.detour_line}; glm::dvec2 const normal{-line.direction.y, line.direction.x}; auto points{state.detour}; if (points.size() < 4) points.push_back(state.detour_mouse); auto const offset{points.size() >= 2 ? glm::dot(plan_of(points[1] - line.start), normal) : 0.0}; auto const place = [&](double const Along, double const Lateral) { auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; }; for (std::size_t i = 0; i < points.size(); ++i) result.push_back(place(line.along(points[i]), (i == 1 || i == 2) ? offset : 0.0)); return result; } void editor_mode::add_detour_point() { auto &state{m_straights}; if (state.detour.empty()) { auto const &line{current_straight()}; if (line.tracks.empty()) return; state.detour_line = line; } state.detour.push_back(cursor_ground()); if (state.detour.size() >= 4) { apply_detour(); state.detour.clear(); } } void editor_mode::apply_detour() { auto &state{m_straights}; auto line{editor_track::find_straight(*state.detour_line.tracks.front(), state.tolerance)}; if (line.tracks.empty()) return; glm::dvec2 const normal{-line.direction.y, line.direction.x}; auto const offset{glm::dot(plan_of(state.detour[1] - line.start), normal)}; std::array stations{line.along(state.detour[0]), line.along(state.detour[1]), line.along(state.detour[2]), line.along(state.detour[3])}; if (stations[0] > stations[3]) { std::swap(stations[0], stations[3]); std::swap(stations[1], stations[2]); } for (auto &station : stations) station = std::clamp(station, 0.0, line.length); stations[1] = std::clamp(stations[1], stations[0] + 1.0, stations[3] - 1.0); stations[2] = std::clamp(stations[2], stations[1], stations[3] - 1.0); if (std::abs(offset) < 0.01 || stations[3] - stations[0] < 3.0) return; auto const radius{straight_tool_radius(line)}; auto const slope{std::atan(std::abs(offset) / std::max(1.0, std::min(stations[1] - stations[0], stations[3] - stations[2])))}; double speed{-1.0}; for (auto const *track : line.tracks) speed = std::max(speed, editor_track::velocity(*track)); auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; auto const reach{radius * std::tan(slope * 0.5) + transition * 0.5 + 5.0}; auto const from{std::max(0.0, stations[0] - reach)}; auto const to{std::min(line.length, stations[3] + reach)}; struct span { TTrack *track; double from; double to; }; std::vector spans; for (std::size_t i = 0; i < line.tracks.size(); ++i) { auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; auto const a{line.along(path.points[segment_data::point::start])}; auto const b{line.along(path.points[segment_data::point::end])}; spans.push_back({line.tracks[i], std::min(a, b), std::max(a, b)}); } std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); int first{-1}; int last{-1}; for (int i = 0; i < static_cast(spans.size()); ++i) { if (spans[i].to > from && spans[i].from < to && spans[i].track->eType == tt_Normal) { if (first < 0) first = i; last = i; } else if (first >= 0 && spans[i].from < to) { if (spans[i].from > stations[3]) break; first = -1; last = -1; } } if (first < 0) return; m_route.from = spans[first].track; m_route.to = spans[last].track; route_reset(); if (m_route.chain.tracks.empty()) return; auto &design{m_route.design}; auto const startalong{line.along(design.start)}; auto const endalong{line.along(design.end)}; auto const place = [&](double const Along, double const Lateral) { auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; return planar; }; alignment::vertex vertex; vertex.radius = radius; route_recommend(vertex); if (false == state.auto_transitions) { vertex.transition_out = state.transition; vertex.transition_in = vertex.transition_out; } design.vertices.clear(); auto first_vertex{vertex}; first_vertex.offset = std::max(0.1, stations[0] - startalong); design.vertices.push_back(first_vertex); if (stations[2] - stations[1] < 1.0) { auto middle{vertex}; middle.position = place((stations[1] + stations[2]) * 0.5, offset); design.vertices.push_back(middle); } else { auto second{vertex}; second.position = place(stations[1], offset); auto third{vertex}; third.position = place(stations[2], offset); design.vertices.push_back(second); design.vertices.push_back(third); } auto last_vertex{vertex}; last_vertex.offset = std::max(0.1, endalong - stations[3]); design.vertices.push_back(last_vertex); route_update(); if (false == m_route.result.valid) return; route_apply(); straight_refresh(); } bool editor_mode::start_curve_fit(editor_track::straight const &Line, int const Handle) { auto &state{m_straights}; auto *before{editor_track::outside_neighbour(Line, false)}; auto *after{editor_track::outside_neighbour(Line, true)}; auto const curve_at = [&](TTrack *Neighbour, editor_track::curve &Curve) { Curve = {}; return Neighbour != nullptr && Neighbour->eType == tt_Normal && editor_track::find_curve(*Neighbour, state.tolerance, m_route.design.norms.gauge, Curve) && Curve.reversals == 0 && std::abs(Curve.turn) > 1e-4; }; state.fit_has_before = curve_at(before, state.fit_before); state.fit_has_after = curve_at(after, state.fit_after); if (false == state.fit_has_before && false == state.fit_has_after) return false; auto const side_follows = [&](bool const Curve, TTrack const *Neighbour, bool const Pivot) { return Curve || Neighbour == nullptr || Pivot; }; if (false == side_follows(state.fit_has_before, before, Handle == 1) || false == side_follows(state.fit_has_after, after, Handle == 0)) return false; auto const own_end = [&](glm::dvec3 const &Point) -> TTrack * { for (auto *track : Line.tracks) if (editor_track::touches(*track, Point)) return track; return Line.tracks.empty() ? nullptr : Line.tracks.front(); }; auto const other_end = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; m_route.from = state.fit_has_before ? other_end(state.fit_before, Line.start) : own_end(Line.start); m_route.to = state.fit_has_after ? other_end(state.fit_after, Line.end) : own_end(Line.end); route_reset(); if (m_route.chain.tracks.empty()) return false; return true; } void editor_mode::update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &End) { auto &state{m_straights}; auto &design{m_route.design}; glm::dvec2 const point{Start.x, Start.z}; glm::dvec2 const direction{glm::normalize(plan_of(End - Start))}; glm::dvec2 const start{design.start.x, design.start.z}; glm::dvec2 const end{design.end.x, design.end.z}; auto const startdirection{glm::normalize(design.start_direction)}; auto const enddirection{glm::normalize(design.end_direction)}; auto const first{cross(point - start, direction) / cross(startdirection, direction)}; auto const last{cross(end - point, direction) / cross(enddirection, direction)}; auto const make = [&](editor_track::curve const &Curve, double const Offset) { alignment::vertex vertex; vertex.radius = Curve.radius > 0.0 ? std::round(Curve.radius) : 1000.0; route_recommend(vertex); if (Curve.transition_in > 0.0) vertex.transition_in = std::round(Curve.transition_in); if (Curve.transition_out > 0.0) vertex.transition_out = std::round(Curve.transition_out); if (Curve.cant > 0.0) vertex.cant = std::round(Curve.cant); compound_from(Curve, vertex); vertex.offset = std::max(0.1, Offset); return vertex; }; design.vertices.clear(); if (state.fit_has_before) design.vertices.push_back(make(state.fit_before, first)); else { design.start = Start; design.start_direction = direction; design.start_reserve = 0.0; } if (state.fit_has_after) design.vertices.push_back(make(state.fit_after, last)); else { design.end = End; design.end_direction = direction; design.end_reserve = 0.0; } route_update(); }