diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 57cd984b..583a4954 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -750,6 +750,13 @@ bool editor_mode::route_from_curve(TTrack &Track) 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 @@ -932,8 +939,12 @@ void editor_mode::draw_route_overlay() const 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[24]; - std::snprintf(radius, sizeof(radius), "R %.0f", i < static_cast(result.curves.size()) ? result.curves[i].radius : design.vertices[i].radius); + 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]; @@ -1153,7 +1164,21 @@ void editor_mode::render_route_ui() changed |= ImGui::InputScalarN("Position (x, z)", ImGuiDataType_Double, glm::value_ptr(vertex.position), 2, nullptr, nullptr, "%.2f"); ImGui::PopItemWidth(); } - changed |= ImGui::InputDouble("Radius R (m)", &vertex.radius, 0.0, 0.0, "%.1f"); + 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"); diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 3dfdbeec..39df9129 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -94,6 +94,58 @@ local_point transition_point(transition_shape const Shape, double const Length, return {x * h / 3.0, y * h / 3.0, angle(distance)}; } +local_point spiral_point(double const Start, double const End, double const Length, double const Distance) +{ + if (Distance <= 0.0) + return {}; + auto const heading = [&](double const S) { return Start * S + (End - Start) * S * S / (2.0 * std::max(Length, 1e-9)); }; + int const steps{std::max(8, static_cast(std::ceil(Distance / 0.5)) * 2)}; + double const h{Distance / steps}; + double x{0.0}, y{0.0}; + for (int i = 0; i <= steps; ++i) + { + double const weight = (i == 0 || i == steps) ? 1.0 : (i % 2 ? 4.0 : 2.0); + auto const a{heading(i * h)}; + x += weight * std::cos(a); + y += weight * std::sin(a); + } + return {x * h / 3.0, y * h / 3.0, heading(Distance)}; +} + +struct compound_curve +{ + std::vector> segments; + local_point end; + double shortened{1.0}; +}; + +compound_curve build_compound(double const Deflection, double const Radius1, double const Radius2, double const Lengthin, double const Lengthmiddle, double const Lengthout, double const Split) +{ + compound_curve result; + auto const k1{1.0 / Radius1}; + auto const k2{1.0 / Radius2}; + auto const transitions{(k1 * Lengthin + (k1 + k2) * Lengthmiddle + k2 * Lengthout) * 0.5}; + if (transitions > Deflection) + result.shortened = Deflection / transitions * (1.0 - 1e-9); + auto const f{result.shortened}; + auto const arcs{std::max(0.0, Deflection - transitions * f)}; + auto const split{std::clamp(Split, 0.0, 1.0)}; + std::array const segments[] = {{0.0, k1, Lengthin * f}, {k1, k1, split * arcs / k1}, {k1, k2, Lengthmiddle * f}, {k2, k2, (1.0 - split) * arcs / k2}, {k2, 0.0, Lengthout * f}}; + double x{0.0}, y{0.0}, heading{0.0}; + for (auto const &segment : segments) + { + if (segment[2] < 1e-6) + continue; + result.segments.push_back(segment); + auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])}; + x += local.x * std::cos(heading) - local.y * std::sin(heading); + y += local.x * std::sin(heading) + local.y * std::cos(heading); + heading += local.angle; + } + result.end = {x, y, heading}; + return result; +} + void element_point(element const &Element, transition_shape const Shape, double const Distance, glm::dvec2 &Position, glm::dvec2 &Direction) { switch (Element.kind) @@ -116,6 +168,13 @@ void element_point(element const &Element, transition_shape const Shape, double Direction = Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle); break; } + case element_kind::spiral: + { + auto const local{spiral_point(Element.curvature_start, Element.curvature_end, Element.length, Distance)}; + Position = Element.origin + Element.direction * local.x + Element.normal * local.y; + Direction = Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle); + break; + } case element_kind::transition_out: { auto const local{transition_point(Shape, Element.transition, Element.radius, 1.0 - Distance / Element.transition)}; @@ -133,6 +192,11 @@ 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; + } default: return 0.0; } } @@ -164,6 +228,11 @@ std::size_t element_pieces(element const &Element, transition_shape const Shape) case element_kind::transition_in: case element_kind::transition_out: return Shape == transition_shape::cubic_parabola ? 1 : static_cast(std::max(1.0, std::ceil(Element.length / 20.0 - 1e-9))); + case element_kind::spiral: + { + auto const turn{(Element.curvature_start + Element.curvature_end) * 0.5 * Element.length}; + return static_cast(std::max({1.0, std::ceil(turn / (10.0 * kPi / 180.0) - 1e-9), std::ceil(Element.length / 20.0 - 1e-9)})); + } default: return 1; } @@ -290,6 +359,9 @@ result compute(design const &Design) double tangent_in{0.0}; double tangent_out{0.0}; double deflection{0.0}; + bool compound{false}; + double radius2{0.0}; + double transition_middle{0.0}; std::function tangents; }; std::vector fits(count); @@ -314,6 +386,25 @@ result compute(design const &Design) r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); return r; } + if (vertex.compound) + { + fit.compound = true; + fit.radius2 = std::max(1.0, vertex.radius2); + fit.transition_middle = std::max(0.0, vertex.transition_middle); + fit.deflection = deflection; + fit.tangents = [&, k, directionin, directionout, side](double const Radius, double &Tangentin, double &Tangentout) { + auto const &own{fits[k]}; + auto const scale{Radius / own.radius}; + auto const shape{build_compound(own.deflection, Radius, own.radius2 * scale, own.transition_in * scale, own.transition_middle * scale, own.transition_out * scale, Design.vertices[k].split)}; + auto const normalin{perpendicular(directionin) * static_cast(side)}; + auto const end{directionin * shape.end.x + normalin * shape.end.y}; + auto const along{cross(directionout, end) / cross(directionout, directionin)}; + Tangentin = std::max(0.0, along); + Tangentout = std::max(0.0, glm::dot(end - directionin * along, directionout)); + }; + fit.tangents(fit.radius, fit.tangent_in, fit.tangent_out); + continue; + } auto const angles = [&](double const Factor) { return transition_point(Design.shape, fit.transition_in * Factor, fit.radius, 1.0).angle + transition_point(Design.shape, fit.transition_out * Factor, fit.radius, 1.0).angle; }; @@ -382,7 +473,7 @@ result compute(design const &Design) } low = high * (1.0 + 1e-9); high = fit.radius; - if (fits_budget(low)) + if (false == fit.compound && fits_budget(low)) { for (int i = 0; i < 60; ++i) { @@ -394,7 +485,9 @@ result compute(design const &Design) else { fit.radius *= scales[k]; + fit.radius2 *= scales[k]; fit.transition_in *= scales[k]; + fit.transition_middle *= scales[k]; fit.transition_out *= scales[k]; } r.warnings.emplace_back(format("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends", k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out)); @@ -430,6 +523,71 @@ result compute(design const &Design) r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); return r; } + if (fit.compound) + { + auto const shape{build_compound(deflection, fit.radius, fit.radius2, fit.transition_in, fit.transition_middle, fit.transition_out, vertex.split)}; + if (shape.shortened < 1.0) + r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to fit the deflection angle", k + 1)); + auto const normalin{perpendicular(directionin) * static_cast(side)}; + auto const endoffset{directionin * shape.end.x + normalin * shape.end.y}; + auto const along{cross(directionout, endoffset) / cross(directionout, directionin)}; + auto const curvestart{position - directionin * along}; + auto const straight{glm::dot(curvestart - cursor, directionin)}; + if (straight < -1e-4) + { + if (k == 0) + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight)); + else + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); + } + push_straight(cursor, directionin, straight); + auto const startchainage{chainage}; + glm::dvec2 origin{curvestart}; + glm::dvec2 heading{directionin}; + auto const peak{std::max(1.0 / fit.radius, 1.0 / fit.radius2)}; + 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.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; + piece.direction = heading; + piece.normal = perpendicular(heading) * static_cast(side); + push(piece); + auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])}; + origin += heading * local.x + piece.normal * local.y; + heading = rotate(heading, side * local.angle); + } + vertexchainage[k] = (startchainage + chainage) * 0.5; + cursor = origin; + + curve_report report; + report.vertex = static_cast(k); + report.radius = fit.radius; + report.radius2 = fit.radius2; + report.transition_in = fit.transition_in * shape.shortened; + report.transition_out = fit.transition_out * shape.shortened; + report.deflection = deflection; + report.arc_length = chainage - startchainage; + report.tangent_in = glm::distance(position, curvestart); + report.tangent_out = glm::distance(position, cursor); + auto const sharpest{std::min(fit.radius, fit.radius2)}; + report.unbalanced = unbalanced_acceleration(Design.speed, sharpest, std::max(0.0, vertex.cant), Design.norms); + report.recommended = recommend(Design.speed, sharpest, Design.norms); + r.curves.push_back(report); + if (sharpest < report.recommended.radius_min - 1e-6) + r.warnings.emplace_back(format("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 (report.unbalanced > Design.norms.unbalanced + 1e-6) + r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2", k + 1, report.unbalanced, Design.norms.unbalanced)); + continue; + } auto const radius{fit.radius}; auto const lengthin{fit.transition_in}; auto const lengthout{fit.transition_out}; @@ -748,7 +906,7 @@ std::vector pieces(result const &Result, design const &Design, std } default: { - if (Design.shape == transition_shape::cubic_parabola) + if (Design.shape == transition_shape::cubic_parabola && e.kind != element_kind::spiral) { std::array const local{glm::dvec2{0.0, 0.0}, glm::dvec2{e.transition / 3.0, 0.0}, glm::dvec2{e.transition * 2.0 / 3.0, 0.0}, glm::dvec2{e.transition, e.transition * e.transition / (6.0 * e.radius)}}; auto const a{e.kind == element_kind::transition_in ? piece.from / e.transition : 1.0 - piece.from / e.transition}; @@ -785,6 +943,11 @@ std::vector pieces(result const &Result, design const &Design, std path.rolls[0] = roll(element_cant(e, piece.from)); path.rolls[1] = roll(element_cant(e, piece.to)); path.radius = e.kind == element_kind::straight ? 0.f : static_cast(e.radius); + if (e.kind == element_kind::spiral) + { + auto const curvature{e.curvature_start + (e.curvature_end - e.curvature_start) * (piece.from + piece.to) * 0.5 / std::max(e.length, 1e-9)}; + path.radius = curvature > 1e-9 ? static_cast(1.0 / curvature) : 0.f; + } result.push_back(path); } return result; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index a790b866..51c597a4 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -49,6 +49,10 @@ struct vertex double elevation{0.0}; double vertical_radius{0.0}; bool reverse_turn{false}; + bool compound{false}; + double radius2{600.0}; + double transition_middle{0.0}; + double split{0.5}; }; struct design @@ -85,7 +89,8 @@ enum class element_kind straight, transition_in, arc, - transition_out + transition_out, + spiral }; struct element @@ -98,6 +103,9 @@ struct element double transition{0.0}; int turn{0}; double cant{0.0}; + double curvature_start{0.0}; + double curvature_end{0.0}; + double curvature_peak{0.0}; glm::dvec2 origin{0.0}; glm::dvec2 direction{0.0, 1.0}; glm::dvec2 normal{0.0}; @@ -107,6 +115,7 @@ struct curve_report { int vertex{-1}; double radius{0.0}; + double radius2{0.0}; double transition_in{0.0}; double transition_out{0.0}; double deflection{0.0}; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 261afe2e..b7d82d87 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1039,6 +1039,46 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance 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; + std::vector> arcs; + for (std::size_t i = 0; i < run.size(); ++i) + { + if (varying[i]) + continue; + auto const radius{path_radius(run[i].first->m_paths.front(), 0.5)}; + if (radius <= 0.0) + continue; + if (false == arcs.empty() && std::abs(arcs.back().first - radius) < 0.05 * radius) + arcs.back().second += lengths[i]; + else + arcs.emplace_back(radius, lengths[i]); + } + if (arcs.size() >= 2 && Curve.reversals == 0) + { + auto const &first{arcs.front()}; + auto const &last{arcs.back()}; + if (std::abs(first.first - last.first) > 0.1 * std::min(first.first, last.first)) + { + Curve.compound = true; + Curve.radius = first.first; + Curve.radius2 = last.first; + auto const turnfirst{first.second / first.first}; + auto const turnlast{last.second / last.first}; + Curve.split = turnfirst / std::max(1e-9, turnfirst + turnlast); + double middle{0.0}; + bool inside{false}; + for (std::size_t i = 0; i < run.size(); ++i) + { + if (false == varying[i]) + { + inside = true; + continue; + } + if (inside && i + 1 < run.size() && std::any_of(varying.begin() + i + 1, varying.end(), [](bool const Varying) { return false == Varying; })) + middle += lengths[i]; + } + Curve.transition_middle = middle; + } + } Curve.from = run.front().first; Curve.to = run.back().first; return true; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index bab1d75c..d96358d4 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -128,6 +128,10 @@ class editor_track double transition_in{0.0}; double transition_out{0.0}; double cant{0.0}; + bool compound{false}; + double radius2{0.0}; + double transition_middle{0.0}; + double split{0.5}; }; static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); struct switch_template