From a392364f065ae9033d600b42baf28623fe2e69c1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 2 Oct 2026 23:14:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(editor):=20curves=20over=20180=C2=B0,=20cl?= =?UTF-8?q?ick=20to=20load=20curve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/editormode.h | 1 + application/editormodetrack.cpp | 45 +++++++++++++++++++-- editor/editorAlignment.cpp | 26 +++++++----- editor/editorAlignment.hpp | 1 + editor/editorTrack.cpp | 72 +++++++++++++++++++++++++++++++++ editor/editorTrack.hpp | 12 ++++++ 6 files changed, 143 insertions(+), 14 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 08e42fa3..7b1dcdb7 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -311,6 +311,7 @@ class editor_mode : public application_mode }; void render_route_ui(); void route_reset(); + bool route_from_curve(TTrack &Track); void route_update() { m_route.result = alignment::compute(m_route.design); } void route_apply(); void route_recommend(alignment::vertex &Vertex) const; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 2bcb39fd..db5b1412 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -134,6 +134,8 @@ void editor_mode::select_track(scene::basic_node *Node) m_track_point = {}; m_node = track; ui()->set_node(m_node); + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) == m_route.chain.tracks.end()) + route_from_curve(*track); } bool editor_mode::pick_track_handle() @@ -703,6 +705,42 @@ void editor_mode::route_reset() 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(); + 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}; @@ -913,9 +951,9 @@ void editor_mode::render_route_ui() { auto &route{m_route}; auto &design{route.design}; - ImGui::TextDisabled("Re-lays a chain of regular paths along an alignment designed with intersection points.\n" - "The outer ends of the chain and their directions stay fixed.\n" - "LMB: select a path or a vertex (W1, W2...) Esc: release the vertex"); + 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()); }; @@ -1051,6 +1089,7 @@ void editor_mode::render_route_ui() 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"); diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 815c0045..3dfdbeec 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -238,8 +238,8 @@ result compute(design const &Design) { auto const t{cross(offset, enddirection) / determinant}; auto const u{cross(startdirection, offset) / determinant}; - if (t <= 0.0 || u <= 0.0) - r.errors.emplace_back("Tangents of the ends intersect outside of the fragment, use two vertices"); + if ((t <= 0.0 || u <= 0.0) && false == Design.vertices.front().reverse_turn) + r.errors.emplace_back("Tangents of the ends intersect outside of the fragment, turn the longer way or use two vertices"); r.vertices[0] = start + startdirection * t; } } @@ -303,12 +303,15 @@ result compute(design const &Design) auto const directionin{glm::normalize(polygon[k + 1] - polygon[k])}; auto const directionout{glm::normalize(polygon[k + 2] - polygon[k + 1])}; auto const turn{cross(directionin, directionout)}; - auto const deflection{std::atan2(std::abs(turn), glm::dot(directionin, directionout))}; + auto const geometric{std::atan2(std::abs(turn), glm::dot(directionin, directionout))}; + auto const reverse{Design.vertices[k].reverse_turn}; + auto const deflection{reverse ? 2.0 * kPi - geometric : geometric}; + int const side{(turn > 0.0) != reverse ? 1 : -1}; if (deflection < 1e-7) continue; - if (deflection > kPi - 1e-3) + if (geometric > kPi - 1e-3 || (reverse && geometric < 1e-3)) { - r.errors.emplace_back(format("Vertex %zu: the route turns back on itself", k + 1)); + r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); return r; } auto const angles = [&](double const Factor) { @@ -327,8 +330,7 @@ result compute(design const &Design) fit.transition_out *= low; } fit.deflection = deflection; - fit.tangents = [&, k, directionin, directionout, turn](double const Radius, double &Tangentin, double &Tangentout) { - int const side{turn > 0.0 ? 1 : -1}; + fit.tangents = [&, k, directionin, directionout, side](double const Radius, double &Tangentin, double &Tangentout) { auto const endin{transition_point(Design.shape, fits[k].transition_in, Radius, 1.0)}; auto const endout{transition_point(Design.shape, fits[k].transition_out, Radius, 1.0)}; auto const a{Radius + endin.y - Radius * (1.0 - std::cos(endin.angle))}; @@ -408,7 +410,10 @@ result compute(design const &Design) auto const directionin{glm::normalize(polygon[k + 1] - polygon[k])}; auto const directionout{glm::normalize(polygon[k + 2] - polygon[k + 1])}; auto const turn{cross(directionin, directionout)}; - auto const deflection{std::atan2(std::abs(turn), glm::dot(directionin, directionout))}; + auto const geometric{std::atan2(std::abs(turn), glm::dot(directionin, directionout))}; + auto const reverse{Design.vertices[k].reverse_turn}; + auto const deflection{reverse ? 2.0 * kPi - geometric : geometric}; + int const side{(turn > 0.0) != reverse ? 1 : -1}; auto const lead{glm::dot(position - cursor, directionin)}; if (deflection < 1e-7) @@ -420,12 +425,11 @@ result compute(design const &Design) cursor = position; continue; } - if (deflection > kPi - 1e-3) + if (geometric > kPi - 1e-3 || (reverse && geometric < 1e-3)) { - r.errors.emplace_back(format("Vertex %zu: the route turns back on itself", k + 1)); + r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); return r; } - int const side{turn > 0.0 ? 1 : -1}; auto const radius{fit.radius}; auto const lengthin{fit.transition_in}; auto const lengthout{fit.transition_out}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 0928af7b..a790b866 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -48,6 +48,7 @@ struct vertex bool auto_elevation{true}; double elevation{0.0}; double vertical_radius{0.0}; + bool reverse_turn{false}; }; struct design diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index e8d7edde..32542f32 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -22,6 +22,7 @@ http: #include #include #include +#include #include #include #include @@ -971,6 +972,77 @@ std::vector editor_track::find_straights(double const Mi return result; } +bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve) +{ + Curve = {}; + auto const curved = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && false == is_straight(*Other, Tolerance); }; + if (false == curved(&Track)) + return false; + + std::deque> run{{&Track, true}}; + for (auto const throughend : {true, false}) + { + TTrack *previous{&Track}; + TTrack *current{throughend ? Track.trNext : Track.trPrev}; + while (curved(current) && std::none_of(run.begin(), run.end(), [&](auto const &Member) { return Member.first == current; })) + { + auto const entersatstart{current->trPrev == previous}; + if (throughend) + run.emplace_back(current, entersatstart); + else + run.emplace_front(current, false == entersatstart); + previous = current; + current = entersatstart ? current->trNext : current->trPrev; + } + } + + std::vector turns; + std::vector lengths; + std::vector varying; + double smallest{0.0}; + for (auto const &member : run) + { + auto const &path{member.first->m_paths.front()}; + auto const tangentstart{glm::normalize(plan_of(path_tangent(path, false)))}; + auto const tangentend{glm::normalize(plan_of(path_tangent(path, true)))}; + auto turn{std::atan2(lateral_of(tangentstart, tangentend), glm::dot(tangentstart, tangentend))}; + if (false == member.second) + turn = -turn; + turns.push_back(turn); + lengths.push_back(member.first->Length()); + auto const radiusstart{path_radius(path, 0.0)}; + auto const radiusend{path_radius(path, 1.0)}; + auto const curvaturestart{radiusstart > 0.0 ? 1.0 / radiusstart : 0.0}; + auto const curvatureend{radiusend > 0.0 ? 1.0 / radiusend : 0.0}; + varying.push_back(std::abs(curvaturestart - curvatureend) > 0.1 * std::max(curvaturestart, curvatureend)); + auto const radius{path_radius(path, 0.5)}; + if (radius > 0.0 && (smallest == 0.0 || radius < smallest)) + smallest = radius; + for (auto const roll : path.rolls) + Curve.cant = std::max(Curve.cant, Gauge * std::sin(std::abs(glm::radians(static_cast(roll))))); + } + int sign{0}; + for (auto const turn : turns) + { + Curve.turn += turn; + int const current{turn > 1e-9 ? 1 : turn < -1e-9 ? -1 : 0}; + if (current != 0 && sign != 0 && current != sign) + ++Curve.reversals; + if (current != 0) + sign = current; + } + for (std::size_t i = 0; i < run.size() && varying[i]; ++i) + Curve.transition_in += lengths[i]; + for (std::size_t i = run.size(); i > 0 && varying[i - 1]; --i) + Curve.transition_out += lengths[i - 1]; + if (Curve.transition_in + Curve.transition_out >= std::accumulate(lengths.begin(), lengths.end(), 0.0)) + Curve.transition_in = Curve.transition_out = 0.0; + Curve.radius = smallest; + Curve.from = run.front().first; + Curve.to = run.back().first; + return true; +} + std::vector editor_track::straight_affected(std::vector const &Lines) { std::vector result; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 58a6c2f1..27b9ac92 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -117,6 +117,18 @@ class editor_track static straight find_straight(TTrack &Track, straight_tolerance const &Tolerance); static std::vector find_straights(double const Minimumlength, straight_tolerance const &Tolerance); static std::vector straight_affected(std::vector const &Lines); + struct curve + { + TTrack *from{nullptr}; + TTrack *to{nullptr}; + double turn{0.0}; + int reversals{0}; + double radius{0.0}; + double transition_in{0.0}; + double transition_out{0.0}; + double cant{0.0}; + }; + static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); static void move_straights(std::vector const &Lines, std::vector> const &Ends); static std::vector relay(chain const &Chain, std::vector const &Pieces); static void retire(TTrack &Track);