From 2a399a92a55249adbfdba2daf39d59f73b086b7e Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 23:41:26 +0200 Subject: [PATCH] feat(editor): each arc of a compound curve has a cant of its own, set in the Plan by each radius ("Cant of R1 (mm)"...), ramped along the transition curves between the arcs; until it's set, the cant of the curve in proportion to the curvature of the arc, as before, "auto" taking it back to that and "Apply recommended for V" all of them. The transitions between the arcs of the norms follow the cants set; warnings where the cant of an arc exceeds the norms, where the unbalanced acceleration does on any arc, and where a transition curve, at the ends or between the arcs, is shorter than the change of the curvature and of the cant along it needs. The labels of the curves show the cants of the arcs where they differ. Stored with the vertex as "cants", reversed with the curve; a unit test --- application/editormoderoute.cpp | 29 +++++++- application/editormodetrackshared.h | 8 ++- editor/editorAlignment.cpp | 104 ++++++++++++++++++++-------- editor/editorAlignment.hpp | 11 ++- editor/editorAlignmentStore.cpp | 24 ++++++- lang/editor_pl.po | 15 ++++ tests/unit/test_alignment.cpp | 46 ++++++++++++ 7 files changed, 204 insertions(+), 33 deletions(-) diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index 1f093e50..b54b364d 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -58,6 +58,10 @@ void editor_mode::route_recommend(alignment::vertex &Vertex) const sharpest = std::min(sharpest, arc.radius); auto const recommended{alignment::recommend(m_route.design.speed, sharpest, laying_norms())}; Vertex.cant = recommended.cant; + // the cants of the arcs in proportion to their curvature, as the norms recommend them + Vertex.first_cant = -1.0; + for (auto &arc : Vertex.arcs) + arc.cant = -1.0; Vertex.transition_in = recommended.transition; Vertex.transition_out = recommended.transition; alignment::recommend_between(Vertex, m_route.design.speed, laying_norms()); @@ -1740,7 +1744,9 @@ bool editor_mode::render_route_vertex(int const Index) ImU32 const transition{IM_COL32(255, 140, 30, 255)}; changed |= field("##radius", vertex.radius, 5.0f, vertex.compound ? STR_C("R1 %.0f m") : STR_C("R %.0f m"), overlay_color::grip, "Radius of the arc: drag sideways, or a double click to type it"); ImGui::SameLine(); - changed |= field("##cant", vertex.cant, 1.0f, STR_C("cant %.0f mm"), IM_COL32(240, 170, 60, 255), "Height of the outer rail above the inner one"); + changed |= field("##cant", vertex.cant, 1.0f, STR_C("cant %.0f mm"), IM_COL32(240, 170, 60, 255), + vertex.compound ? "Height of the outer rail above the inner one on the sharpest arc; the arcs with no cant of their own get it in proportion to their curvature" + : "Height of the outer rail above the inner one"); changed |= field("##entry", vertex.transition_in, 1.0f, STR_C("entry %.0f m"), transition, "Length of the transition curve leading into the arc"); ImGui::SameLine(); changed |= field("##exit", vertex.transition_out, 1.0f, STR_C("exit %.0f m"), transition, "Length of the transition curve leading out of the arc"); @@ -1766,9 +1772,29 @@ bool editor_mode::render_route_vertex(int const Index) for (auto const &arc : vertex.arcs) total += std::max(0.0, arc.share); auto const percent = [&](double const Share) { return total > 0.0 ? 100.0 * std::max(0.0, Share) / total : 0.0; }; + auto const cants{alignment::compound_cants(vertex)}; + // the cant of one arc: the one in proportion to the curvature until it's given its own + auto const arc_cant = [&](int const Arc, double &Cant) { + auto value{static_cast(Arc) < cants.size() ? cants[Arc] : std::max(0.0, Cant)}; + auto edited{ImGui::DragScalar(format(STR_C("Cant of R%d (mm)"), Arc + 1).c_str(), ImGuiDataType_Double, &value, 1.0f, nullptr, nullptr, "%.0f")}; + if (edited) + Cant = std::max(0.0, value); + item_tooltip("The cant of this arc, ramped along the transition curves to the arcs either side of it; until it's set here, the cant of the curve in proportion to the curvature of the arc"); + ImGui::SameLine(); + if (Cant < 0.0) + ImGui::TextDisabled("%s", STR_C("auto")); + else if (ImGui::SmallButton(STR_C("auto"))) + { + Cant = -1.0; + edited = true; + } + item_tooltip("The cant of the arc in proportion to its curvature again"); + return edited; + }; changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), 1).c_str(), ImGuiDataType_Double, &vertex.share, 0.01f, nullptr, nullptr, "%.2f"); ImGui::SameLine(); ImGui::TextDisabled("%.0f%%", percent(vertex.share)); + changed |= arc_cant(0, vertex.first_cant); int removed{-1}; for (int k = 0; k < static_cast(vertex.arcs.size()); ++k) { @@ -1784,6 +1810,7 @@ bool editor_mode::render_route_vertex(int const Index) changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f"); ImGui::SameLine(); ImGui::TextDisabled("%.0f%%", percent(arc.share)); + changed |= arc_cant(k + 1, arc.cant); arc.radius = std::max(1.0, arc.radius); arc.transition = std::max(0.0, arc.transition); arc.share = std::max(0.0, arc.share); diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index fd97d2fe..96002301 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -92,7 +92,12 @@ inline std::string curve_label(alignment::curve_report const &Curve, alignment:: text += format(i == 0 ? "%.0f" : "/%.0f", Curve.radii[i]); if (Curve.transition_in > 0.0 || Curve.transition_out > 0.0) text += std::abs(Curve.transition_in - Curve.transition_out) < 0.5 ? format(" L %.0f", Curve.transition_in) : format(" L %.0f/%.0f", Curve.transition_in, Curve.transition_out); - if (Curve.cant > 0.0) + // a compound curve shows the cant of each arc where they aren't all the same + auto const varied{std::any_of(Curve.cants.begin(), Curve.cants.end(), [&](double const Cant) { return std::abs(Cant - Curve.cants.front()) >= 0.5; })}; + if (varied) + for (std::size_t i = 0; i < Curve.cants.size(); ++i) + text += format(i == 0 ? " h %.0f" : "/%.0f", Curve.cants[i]); + else if (Curve.cant > 0.0) text += format(" h %.0f", Curve.cant); return text + speed_of(curve_sharpest(Curve), Limits); } @@ -301,6 +306,7 @@ inline void compound_from(editor_track::curve const &Curve, alignment::vertex &V Vertex.compound = true; Vertex.radius = std::round(Curve.arcs.front().radius); Vertex.share = Curve.arcs.front().turn; + Vertex.first_cant = -1.0; Vertex.arcs.clear(); for (std::size_t i = 1; i < Curve.arcs.size(); ++i) Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 77d91caa..64823f7c 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -106,12 +106,14 @@ local_point spiral_point(double const Start, double const End, double const Leng struct compound_curve { - std::vector> segments; + // the curvature at the start and at the end, the length, the cant at the start and at the end + std::vector> segments; local_point end; double shortened{1.0}; }; -compound_curve build_compound(double const Deflection, std::vector const &Arcs, double const Lengthin, double const Lengthout) +// Cants: of the arcs, the cants of the segments stay 0 without them +compound_curve build_compound(double const Deflection, std::vector const &Arcs, double const Lengthin, double const Lengthout, std::vector const &Cants = {}) { compound_curve result; if (Arcs.empty()) @@ -119,6 +121,7 @@ compound_curve build_compound(double const Deflection, std::vector std::vector curvatures; for (auto const &arc : Arcs) curvatures.push_back(1.0 / std::max(1.0, arc.radius)); + auto const cant = [&](std::size_t const Arc) { return Arc < Cants.size() ? Cants[Arc] : 0.0; }; auto transitions{curvatures.front() * Lengthin + curvatures.back() * Lengthout}; for (std::size_t i = 1; i < Arcs.size(); ++i) transitions += (curvatures[i - 1] + curvatures[i]) * std::max(0.0, Arcs[i].transition); @@ -130,15 +133,15 @@ compound_curve build_compound(double const Deflection, std::vector double total{0.0}; for (auto const &arc : Arcs) total += std::max(0.0, arc.share); - std::vector> segments{{0.0, curvatures.front(), Lengthin * f}}; + std::vector> segments{{0.0, curvatures.front(), Lengthin * f, 0.0, cant(0)}}; for (std::size_t i = 0; i < Arcs.size(); ++i) { if (i > 0) - segments.push_back({curvatures[i - 1], curvatures[i], std::max(0.0, Arcs[i].transition) * f}); + segments.push_back({curvatures[i - 1], curvatures[i], std::max(0.0, Arcs[i].transition) * f, cant(i - 1), cant(i)}); auto const share{total > 0.0 ? std::max(0.0, Arcs[i].share) / total : 1.0 / static_cast(Arcs.size())}; - segments.push_back({curvatures[i], curvatures[i], share * arcs / curvatures[i]}); + segments.push_back({curvatures[i], curvatures[i], share * arcs / curvatures[i], cant(i), cant(i)}); } - segments.push_back({curvatures.back(), 0.0, Lengthout * f}); + segments.push_back({curvatures.back(), 0.0, Lengthout * f, cant(Arcs.size() - 1), 0.0}); double x{0.0}; double y{0.0}; double heading{0.0}; @@ -156,6 +159,18 @@ compound_curve build_compound(double const Deflection, std::vector return result; } +// the cants of the arcs, each its own or Cant at the sharpest of them in proportion to the curvature +std::vector arc_cants(std::vector const &Arcs, double const Cant) +{ + auto sharpest{std::numeric_limits::max()}; + for (auto const &arc : Arcs) + sharpest = std::min(sharpest, std::max(1.0, arc.radius)); + std::vector cants; + for (auto const &arc : Arcs) + cants.push_back(arc.cant >= 0.0 ? arc.cant : std::max(0.0, Cant) * sharpest / std::max(1.0, arc.radius)); + return cants; +} + void element_point(element const &Element, transition_shape const Shape, double const Distance, glm::dvec2 &Position, glm::dvec2 &Direction) { switch (Element.kind) @@ -202,11 +217,7 @@ double element_cant(element const &Element, double const Distance) case element_kind::transition_in: return Element.cant * Distance / Element.transition; case element_kind::transition_out: return Element.cant * (1.0 - Distance / Element.transition); case element_kind::arc: return Element.cant; - case element_kind::spiral: - { - auto const curvature{Element.curvature_start + (Element.curvature_end - Element.curvature_start) * Distance / std::max(Element.length, 1e-9)}; - return Element.curvature_peak > 0.0 ? Element.cant * curvature / Element.curvature_peak : 0.0; - } + case element_kind::spiral: return Element.cant_start + (Element.cant_end - Element.cant_start) * std::clamp(Distance / std::max(Element.length, 1e-9), 0.0, 1.0); default: return 0.0; } } @@ -261,6 +272,15 @@ double unbalanced_acceleration(double const Speed, double const Radius, double c return Speed * Speed / (12.96 * Radius) - kGravity * Cant / Limits.gauge; } +std::vector compound_cants(vertex const &Vertex) +{ + if (false == Vertex.compound || Vertex.arcs.empty()) + return {std::max(0.0, Vertex.cant)}; + std::vector arcs{{Vertex.radius, 0.0, Vertex.share, Vertex.first_cant}}; + arcs.insert(arcs.end(), Vertex.arcs.begin(), Vertex.arcs.end()); + return arc_cants(arcs, Vertex.cant); +} + double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits) { double length{0.0}; @@ -346,6 +366,10 @@ bool meets_norms(curve_report const &Curve, design const &Design) return false; if (false == Curve.spirals && sharpest < Curve.recommended.radius_min - 1e-6) return false; + if (Curve.radii.size() > 1 && Curve.cants.size() == Curve.radii.size()) + // a compound curve ramps the cant of its first arc in and the one of its last arc out + return Curve.transition_in + 1e-6 >= transition_length(Design.speed, Curve.radii.front(), Curve.cants.front(), norms) && + Curve.transition_out + 1e-6 >= transition_length(Design.speed, Curve.radii.back(), Curve.cants.back(), norms); auto const required{transition_length(Design.speed, sharpest, Curve.cant, norms)}; return Curve.transition_in + 1e-6 >= required && Curve.transition_out + 1e-6 >= required; } @@ -494,9 +518,9 @@ result fit_between(design const &Design) if (vertex.compound && false == vertex.arcs.empty()) { fit.compound = true; - fit.arcs = {{fit.radius, 0.0, vertex.share}}; + fit.arcs = {{fit.radius, 0.0, vertex.share, vertex.first_cant}}; for (auto const &arc : vertex.arcs) - fit.arcs.push_back({std::max(1.0, arc.radius), std::max(0.0, arc.transition), arc.share}); + fit.arcs.push_back({std::max(1.0, arc.radius), std::max(0.0, arc.transition), arc.share, arc.cant}); fit.deflection = deflection; fit.tangents = [&, k, directionin, directionout, side, scaled](double const Radius, double &Tangentin, double &Tangentout) { auto const &own{fits[k]}; @@ -674,7 +698,8 @@ result fit_between(design const &Design) } if (fit.compound) { - auto const shape{build_compound(deflection, fit.arcs, fit.transition_in, fit.transition_out)}; + auto const cants{arc_cants(fit.arcs, fit.cant)}; + auto const shape{build_compound(deflection, fit.arcs, fit.transition_in, fit.transition_out, cants)}; if (shape.shortened < 1.0) r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shortened to fit the deflection angle"), k + 1)); auto const normalin{perpendicular(directionin) * static_cast(side)}; @@ -693,17 +718,17 @@ result fit_between(design const &Design) auto sharpest{fit.radius}; for (auto const &arc : fit.arcs) sharpest = std::min(sharpest, arc.radius); - auto const peak{1.0 / sharpest}; for (auto const &segment : shape.segments) { element piece; piece.kind = element_kind::spiral; piece.vertex = static_cast(k); piece.turn = side; - piece.cant = std::max(0.0, vertex.cant); + piece.cant = std::max(segment[3], segment[4]); + piece.cant_start = segment[3]; + piece.cant_end = segment[4]; piece.curvature_start = segment[0]; piece.curvature_end = segment[1]; - piece.curvature_peak = peak; piece.radius = std::max(segment[0], segment[1]) > 0.0 ? 1.0 / std::max(segment[0], segment[1]) : 0.0; piece.length = segment[2]; piece.origin = origin; @@ -728,14 +753,37 @@ result fit_between(design const &Design) report.arc_length = chainage - startchainage; report.tangent_in = glm::distance(position, curvestart); report.tangent_out = glm::distance(position, cursor); - report.unbalanced = unbalanced_acceleration(Design.speed, sharpest, std::max(0.0, vertex.cant), Design.norms); - report.cant = std::max(0.0, vertex.cant); + report.cants = cants; + report.unbalanced = -std::numeric_limits::max(); + for (std::size_t i = 0; i < cants.size(); ++i) + { + report.unbalanced = std::max(report.unbalanced, unbalanced_acceleration(Design.speed, fit.arcs[i].radius, cants[i], Design.norms)); + report.cant = std::max(report.cant, cants[i]); + } report.recommended = recommend(Design.speed, sharpest, Design.norms); r.curves.push_back(report); - if (Design.norms.checked && sharpest < report.recommended.radius_min - 1e-6) + auto const &norms{Design.norms}; + if (false == norms.checked) + continue; + if (sharpest < report.recommended.radius_min - 1e-6) r.warnings.emplace_back(format(STR_C("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h"), k + 1, sharpest, report.recommended.radius_min, Design.speed)); - if (Design.norms.checked && report.unbalanced > Design.norms.unbalanced + 1e-6) - r.warnings.emplace_back(format(STR_C("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2"), k + 1, report.unbalanced, Design.norms.unbalanced)); + if (report.unbalanced > norms.unbalanced + 1e-6) + r.warnings.emplace_back(format(STR_C("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2"), k + 1, report.unbalanced, norms.unbalanced)); + if (report.cant > norms.cant_max + 1e-6) + r.warnings.emplace_back(format(STR_C("Vertex %zu: cant %.0f mm exceeds %.0f mm"), k + 1, report.cant, norms.cant_max)); + // each transition long enough for the change of the curvature and of the cant along it, at the speed the sharpest arc + // allows, as the transitions of the norms are set between the arcs + auto const allowed{std::max(1.0, std::min(Design.speed, speed_limit(sharpest, norms)))}; + auto const requiredin{transition_length(allowed, fit.arcs.front().radius, cants.front(), norms)}; + auto const requiredout{transition_length(allowed, fit.arcs.back().radius, cants.back(), norms)}; + if (report.transition_in + 1e-6 < requiredin || report.transition_out + 1e-6 < requiredout) + r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shorter than the required %.0f m"), k + 1, std::max(requiredin, requiredout))); + for (std::size_t i = 1; i < fit.arcs.size(); ++i) + { + auto const required{transition_between(allowed, 1.0 / fit.arcs[i - 1].radius, cants[i - 1], 1.0 / fit.arcs[i].radius, cants[i], norms)}; + if (fit.arcs[i].transition * shape.shortened + 1e-6 < required) + r.warnings.emplace_back(format(STR_C("Vertex %zu: the transition curve between R %.0f and R %.0f m shorter than the required %.0f m"), k + 1, fit.arcs[i - 1].radius, fit.arcs[i].radius, required)); + } continue; } auto const radius{fit.radius}; @@ -1276,8 +1324,8 @@ void chain_spiral(element_chain &Chain, double const From, double const To, doub spiral.turn = turn; spiral.curvature_start = From * turn; spiral.curvature_end = To * turn; - spiral.curvature_peak = std::max(std::abs(From), std::abs(To)); - spiral.radius = spiral.curvature_peak > 1e-12 ? 1.0 / spiral.curvature_peak : 0.0; + auto const peak{std::max(std::abs(From), std::abs(To))}; + spiral.radius = peak > 1e-12 ? 1.0 / peak : 0.0; spiral.length = Length; spiral.origin = Chain.position; spiral.direction = Chain.direction; @@ -1641,12 +1689,13 @@ void recommend_between(vertex &Vertex, double const Speed, limits const &Limits) for (auto const &arc : Vertex.arcs) sharpest = std::min(sharpest, std::max(1.0, arc.radius)); auto const speed{std::max(1.0, std::min(Speed, speed_limit(sharpest, Limits)))}; - auto const cant_of = [&](double const Radius) { return Vertex.cant * sharpest / Radius; }; + auto const cants{compound_cants(Vertex)}; auto previous{std::max(1.0, Vertex.radius)}; - for (auto &arc : Vertex.arcs) + for (std::size_t i = 0; i < Vertex.arcs.size(); ++i) { + auto &arc{Vertex.arcs[i]}; auto const radius{std::max(1.0, arc.radius)}; - arc.transition = transition_between(speed, 1.0 / previous, cant_of(previous), 1.0 / radius, cant_of(radius), Limits); + arc.transition = transition_between(speed, 1.0 / previous, cants[i], 1.0 / radius, cants[i + 1], Limits); previous = radius; } } @@ -2019,6 +2068,7 @@ vertex traced_shape(design const &Design, vertex const &Base, std::vector(Base.arcs.size()) ? Base.arcs[i - 1].transition : 0.0; + arc.cant = i - 1 < static_cast(Base.arcs.size()) ? Base.arcs[i - 1].cant : -1.0; arc.share = weights[i] / total; shape.arcs.push_back(arc); } diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 0dc9ef6d..793c3084 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -53,6 +53,8 @@ struct compound_arc double radius{600.0}; double transition{0.0}; double share{1.0}; + // the cant of the arc; below 0 the cant of the vertex, which is the one of the sharpest arc, in proportion to the curvature + double cant{-1.0}; }; struct vertex @@ -67,6 +69,8 @@ struct vertex bool compound{false}; double share{1.0}; std::vector arcs; + // the cant of the first arc of a compound curve, as the ones of the arcs after it + double first_cant{-1.0}; // two transition curves meeting at the curvature of the radius, with no arc between them and no cant: the bend of two straights // eased the way the lines are drawn. Their lengths follow from the radius and the deflection, the ones given are left out bool spirals{false}; @@ -107,6 +111,8 @@ double transition_length(double const Speed, double const Radius, double const C // turning one way, the cant with the same sign as the curvature it leans into double transition_between(double const Speed, double const Curvature0, double const Cant0, double const Curvature1, double const Cant1, limits const &Limits); double unbalanced_acceleration(double const Speed, double const Radius, double const Cant, limits const &Limits); +// the cants of the arcs of a compound curve, the first one's first: each its own, or the one in proportion to its curvature +std::vector compound_cants(vertex const &Vertex); enum class element_kind { @@ -129,7 +135,9 @@ struct element double cant{0.0}; double curvature_start{0.0}; double curvature_end{0.0}; - double curvature_peak{0.0}; + // the cant of a spiral at its start and at its end, going from the one to the other along it + double cant_start{0.0}; + double cant_end{0.0}; glm::dvec2 origin{0.0}; glm::dvec2 direction{0.0, 1.0}; glm::dvec2 normal{0.0}; @@ -140,6 +148,7 @@ struct curve_report int vertex{-1}; double radius{0.0}; std::vector radii; + std::vector cants; // of the radii of a compound curve double transition_in{0.0}; double transition_out{0.0}; double deflection{0.0}; diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp index 0b9a6d8d..74ae3d4b 100644 --- a/editor/editorAlignmentStore.cpp +++ b/editor/editorAlignmentStore.cpp @@ -47,6 +47,7 @@ bool g_loaded{false}; // pieces // vertex [ ]... [spirals 1] // spirals: two transition curves with no arc between them, their lengths following from R +// [cants ...]: the cants of the radii of a compound curve set one by one, below 0 the cant in proportion to the curvature // element // radii signed with the turn, 0 for no curvature // end @@ -139,6 +140,12 @@ void mark(scene::layer_handle const Layer) text += format(" %.3f %.3f %.9f", arc.radius, arc.transition, arc.share); if (vertex.spirals) text += " spirals 1"; + if (vertex.first_cant >= 0.0 || std::any_of(vertex.arcs.begin(), vertex.arcs.end(), [](alignment::compound_arc const &Arc) { return Arc.cant >= 0.0; })) + { + text += format(" cants %.1f", vertex.first_cant); + for (auto const &arc : vertex.arcs) + text += format(" %.1f", arc.cant); + } lines.push_back(text); } for (auto const &e : entry.result.elements) @@ -165,15 +172,17 @@ alignment::vertex reversed_vertex(alignment::vertex Vertex) double radius; double share; double transition; + double cant; }; - std::vector parts{{Vertex.radius, Vertex.share, 0.0}}; + std::vector parts{{Vertex.radius, Vertex.share, 0.0, Vertex.first_cant}}; for (auto const &arc : Vertex.arcs) - parts.push_back({arc.radius, arc.share, arc.transition}); + parts.push_back({arc.radius, arc.share, arc.transition, arc.cant}); auto const count{parts.size()}; Vertex.radius = parts.back().radius; Vertex.share = parts.back().share; + Vertex.first_cant = parts.back().cant; for (std::size_t j = 1; j < count; ++j) - Vertex.arcs[j - 1] = {parts[count - 1 - j].radius, parts[count - j].transition, parts[count - 1 - j].share}; + Vertex.arcs[j - 1] = {parts[count - 1 - j].radius, parts[count - j].transition, parts[count - 1 - j].share, parts[count - 1 - j].cant}; } return Vertex; } @@ -312,8 +321,17 @@ std::optional read_vertex(std::istringstream &Line, scene::la return std::nullopt; // written since the bends of the straights are eased with two transition curves, the lines of before end with the arcs for (std::string word; Line >> word;) + { if (int value{0}; word == "spirals" && Line >> value) vertex.spirals = (value != 0); + // the cants of the arcs of a compound curve, set one by one: of the first arc and of the ones after it, below 0 in proportion + if (word == "cants") + { + Line >> vertex.first_cant; + for (auto &arc : vertex.arcs) + Line >> arc.cant; + } + } position = Context.to_world(position); vertex.position = {position.x, position.z}; vertex.reverse_turn = (reverse != 0); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 48ca9766..a6c0e1c4 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11905,3 +11905,18 @@ msgstr "Załamanie wyokrąglone tak, jak projektuje się linie: dwie krzywe prze msgid "Each bend of the straights clicked at the height of the ground, an even grade between them: the grade changes only at the bends. A curve traced, or bends clicked close along a curve, goes at an even grade from one end of it to the other" msgstr "Każde kliknięte załamanie prostych na wysokości terenu, między nimi równe pochylenie: pochylenie zmienia się tylko na załamaniach. Łuk odrysowany albo załamania kliknięte gęsto wzdłuż łuku idą jednym pochyleniem od jego początku do końca" + +msgid "Vertex %zu: the transition curve between R %.0f and R %.0f m shorter than the required %.0f m" +msgstr "Wierzchołek %zu: krzywa przejściowa między R %.0f a R %.0f m krótsza niż wymagane %.0f m" + +msgid "Cant of R%d (mm)" +msgstr "Przechyłka R%d (mm)" + +msgid "The cant of this arc, ramped along the transition curves to the arcs either side of it; until it's set here, the cant of the curve in proportion to the curvature of the arc" +msgstr "Przechyłka tego łuku, zmieniana na krzywych przejściowych do łuków po obu jego stronach; dopóki nie jest tu ustawiona, przechyłka łuku koszowego proporcjonalna do krzywizny tego łuku" + +msgid "The cant of the arc in proportion to its curvature again" +msgstr "Przechyłka łuku znów proporcjonalna do jego krzywizny" + +msgid "Height of the outer rail above the inner one on the sharpest arc; the arcs with no cant of their own get it in proportion to their curvature" +msgstr "Wysokość szyny zewnętrznej ponad wewnętrzną na najostrzejszym łuku; łuki bez własnej przechyłki dostają ją proporcjonalnie do swojej krzywizny" diff --git a/tests/unit/test_alignment.cpp b/tests/unit/test_alignment.cpp index 64a8977f..9f4c5990 100644 --- a/tests/unit/test_alignment.cpp +++ b/tests/unit/test_alignment.cpp @@ -257,6 +257,52 @@ TEST_CASE("alignment: a compound curve traced is followed closely") { CHECK(fit.worst < 0.2); } +TEST_CASE("alignment: each arc of a compound curve has a cant of its own, ramped along the transitions between them") { + auto design{straights({500.0, 500.0}, {35.0 * pi / 180.0}, 100.0)}; + auto &vertex{design.vertices.front()}; + vertex.radius = 1200.0; + vertex.compound = true; + vertex.share = 0.5; + vertex.arcs = {{700.0, 60.0, 0.5}}; + vertex.transition_in = vertex.transition_out = 80.0; + vertex.cant = 120.0; + + // until they're set, the cants of the arcs go with their curvature, the cant of the curve on the sharpest one + auto const proportional{alignment::compound_cants(vertex)}; + REQUIRE(proportional.size() == 2); + CHECK(proportional[0] == doctest::Approx(70.0)); + CHECK(proportional[1] == doctest::Approx(120.0)); + + vertex.first_cant = 40.0; + vertex.arcs.front().cant = 110.0; + auto const line{alignment::compute(design)}; + REQUIRE(line.valid); + REQUIRE(line.curves.size() == 1); + auto const &curve{line.curves.front()}; + REQUIRE(curve.cants.size() == 2); + CHECK(curve.cants[0] == doctest::Approx(40.0)); + CHECK(curve.cants[1] == doctest::Approx(110.0)); + CHECK(curve.cant == doctest::Approx(110.0)); + + std::vector pieces; + for (auto const &e : line.elements) + if (e.vertex == 0) + pieces.push_back(e); + // in, R1, between, R2, out + REQUIRE(pieces.size() == 5); + auto const cant = [&](alignment::element const &Element, double const Along) { return alignment::evaluate(line, Element.chainage + Element.length * Along).cant; }; + CHECK(cant(pieces[0], 0.0) == doctest::Approx(0.0).epsilon(1e-6)); + CHECK(cant(pieces[0], 0.5) == doctest::Approx(20.0)); + CHECK(cant(pieces[1], 0.5) == doctest::Approx(40.0)); + CHECK(cant(pieces[2], 0.5) == doctest::Approx(75.0)); + CHECK(cant(pieces[3], 0.5) == doctest::Approx(110.0)); + CHECK(cant(pieces[4], 0.5) == doctest::Approx(55.0)); + + // the transition between the arcs of the norms for the change of the cant set + alignment::recommend_between(vertex, design.speed, design.norms); + CHECK(vertex.arcs.front().transition == doctest::Approx(alignment::transition_between(design.speed, 1.0 / 1200.0, 40.0, 1.0 / 700.0, 110.0, design.norms))); +} + TEST_CASE("alignment: bends clicked close along a curve make one compound curve, not curves with short straights between them") { // the points clicked in the scenery abc, at 60 km/h: from a straight along z into a curve to the left alignment::design design;