diff --git a/application/editormode.h b/application/editormode.h index 2b6bcdfa..2ca12cac 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1482,6 +1482,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri TTrack const *m_join_track{nullptr}; editor_track::point_ref m_join_point; mutable alignment::join_result m_join_last; + // the layout laid on from the end of From with the transition curves of the norms; none in the free laying, or where it doesn't fit + std::optional eased_layout(TTrack const *From, glm::dvec3 const &Point, glm::dvec2 const &Direction, glm::dvec3 const &Target, double const Radius) const; + mutable std::optional m_eased_last; + mutable std::optional>> m_eased_cache; bool track_wheel(double const Offset); // a touchpad sends fractions of a notch: a choice moves a step for each whole notch gathered, and only one in a gesture when // Gesture; a pause, another key or the other way starts over diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 9023e1ed..f0e7081b 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -408,8 +408,40 @@ void editor_mode::point_drag_finish() m_track_joints.clear(); } +std::optional editor_mode::eased_layout(TTrack const *From, glm::dvec3 const &Point, glm::dvec2 const &Direction, glm::dvec3 const &Target, double const Radius) const +{ + m_eased_last.reset(); + if (false == m_lay.transitions || From == nullptr || From->eType != tt_Normal) + return std::nullopt; + auto const end{editor_track::bend_at(*From, Point)}; + if (false == end.has_value()) + return std::nullopt; + alignment::lay_request request; + request.start.position = Point; + request.start.direction = Direction; + request.start.roll = end->roll; + request.curvature = end->curvature; + request.target = Target; + switch (m_track_layout) + { + case track_layout::straight: request.kind = alignment::lay_kind::straight; break; + case track_layout::arc: request.kind = alignment::lay_kind::arc; break; + case track_layout::straight_arc: request.kind = alignment::lay_kind::straight_arc; break; + default: request.kind = alignment::lay_kind::arc_straight; break; + } + request.radius = Radius; + request.speed = design_speed(From, nullptr); + request.piece_length = m_lay.piece_length; + request.norms = laying_norms(); + if (false == m_eased_cache.has_value() || m_eased_cache->first != request) + m_eased_cache.emplace(request, alignment::lay_on(request)); + m_eased_last = m_eased_cache->second; + return m_eased_last; +} + std::vector editor_mode::extend_pieces() const { + m_eased_last.reset(); std::vector result; auto const &tool{m_extend}; editor_track::snap_target target; @@ -452,7 +484,8 @@ std::vector editor_mode::extend_pieces() const auto radius{m_lay.radius}; if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) radius = std::abs(values[0]); - auto pieces{editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; + auto const eased{eased_layout(tool.track, tool.point, tool.direction, tool.mouse, radius)}; + auto pieces{eased.has_value() && false == eased->pieces.empty() ? eased->pieces : editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); return pieces; } @@ -807,6 +840,7 @@ std::vector editor_mode::lay_pieces(std::vector const { auto const &tool{m_lay}; Length = 0.0; + m_eased_last.reset(); if (Points.size() < 2) return {}; if (Points.size() == 2) @@ -824,8 +858,10 @@ std::vector editor_mode::lay_pieces(std::vector const if (tool.start.track != nullptr) { auto const grade{-grade_of(tool.start.direction)}; - pieces = editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); - Error = narrowed(pieces, m_track_layout, tool.radius); + auto const eased{eased_layout(tool.start.track, Points[0], glm::normalize(leaving), Points[1], tool.radius)}; + auto const laid{eased.has_value() && false == eased->pieces.empty()}; + pieces = laid ? eased->pieces : editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); + Error = laid ? narrowed(m_track_layout, tool.radius, eased->radius) : narrowed(pieces, m_track_layout, tool.radius); follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN(), tool.start.track); } else if (End.track == nullptr) @@ -836,10 +872,12 @@ std::vector editor_mode::lay_pieces(std::vector const } else if (glm::length(plan_of(End.direction)) > 1e-6) { - auto const back{editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; + auto const eased{eased_layout(End.track, End.position, glm::normalize(-plan_of(End.direction)), Points[0], tool.radius)}; + auto const laid{eased.has_value() && false == eased->pieces.empty()}; + auto const back{laid ? eased->pieces : editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) pieces.push_back(reversed(*piece)); - Error = narrowed(pieces, m_track_layout, tool.radius); + Error = laid ? narrowed(m_track_layout, tool.radius, eased->radius) : narrowed(pieces, m_track_layout, tool.radius); follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); } for (auto const &piece : pieces) @@ -1020,6 +1058,7 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) double length; std::string error; auto const pieces{lay_pieces(tool.points, End, length, error)}; + auto const eased{m_eased_last}; tool.status_time = ImGui::GetTime(); if (pieces.empty()) { @@ -1043,6 +1082,10 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) tool.status = format(STR_P("Laid %zu track, %.1f m, Ctrl+Z takes it back", "Laid %zu tracks, %.1f m, Ctrl+Z takes them back", static_cast(created.size())), created.size(), length); if (false == error.empty()) tool.status += "\n" + error; + if (eased.has_value() && eased->compound) + tool.status += "\n" + format(STR_C("Compound curve: R %.0f m goes on into R %.0f m through a transition curve of %.0f m"), eased->from_radius, eased->radius, eased->entry); + if (eased.has_value() && false == eased->note.empty()) + tool.status += "\n" + eased->note; { std::vector> run; if (tool.start.track != nullptr && tool.start.track->eType == tt_Normal) diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index d4c2419b..187b55b5 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -52,10 +52,15 @@ bool editor_mode::route_active() const void editor_mode::route_recommend(alignment::vertex &Vertex) const { - auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, laying_norms())}; + auto sharpest{Vertex.radius}; + if (Vertex.compound) + for (auto const &arc : Vertex.arcs) + sharpest = std::min(sharpest, arc.radius); + auto const recommended{alignment::recommend(m_route.design.speed, sharpest, laying_norms())}; Vertex.cant = recommended.cant; Vertex.transition_in = recommended.transition; Vertex.transition_out = recommended.transition; + alignment::recommend_between(Vertex, m_route.design.speed, laying_norms()); } void editor_mode::route_bind_ends() diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 8e32ea20..1cbc072e 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1782,6 +1782,8 @@ std::string editor_mode::track_readout() const if (piece.radius != 0.0f && (radius == 0.0 || std::abs(piece.radius) < radius)) radius = std::abs(piece.radius); } + if (m_eased_last.has_value() && false == m_eased_last->pieces.empty()) + radius = m_eased_last->radius; editor_track::snap_target target; auto const snapped{extend_snap(target) && target.track != nullptr}; editor_track::branch_link link; @@ -1791,6 +1793,8 @@ std::string editor_mode::track_readout() const if (snapped && pieces.empty()) return format(STR_C("no track fits to the end of %s"), name(target.track).c_str()); auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) + speed_of(radius, laying_norms()) : format("L %.2f m %s", length, STR_C("straight"))}; + if (auto const eased{eased_of(m_eased_last)}; false == eased.empty()) + text += "\n" + eased; if (auto const vertical{vertical_of(pieces)}; false == vertical.empty()) text += "\n" + vertical; if (snapped) @@ -1817,10 +1821,14 @@ std::string editor_mode::track_readout() const else if (radius == 0.0 || r < radius) radius = r; } + if (m_eased_last.has_value() && false == m_eased_last->pieces.empty()) + radius = m_eased_last->radius; if (radius > 0.0) text = (straight ? format("L %.1f m %s + R %.0f m", lay.preview_length, STR_C("straight"), radius) : format("L %.1f m R %.0f m", lay.preview_length, radius)) + speed_of(radius, laying_norms()); else text = format("L %.1f m %s", lay.preview_length, STR_C("straight")); + if (auto const eased{eased_of(m_eased_last)}; false == eased.empty()) + text += "\n" + eased; if (auto const vertical{vertical_of(lay.preview)}; false == vertical.empty()) text += "\n" + vertical; } diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index a3278716..67f92ef7 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -70,6 +70,21 @@ inline std::string speed_of(double const Radius, alignment::limits const &Limits return speed > 0.0 ? format(STR_C(" (up to %.0f km/h)"), speed) : std::string{}; } +// the transition curves and the cant the norms put in a layout, a compound curve where it goes on from an arc turning the same way +inline std::string eased_of(std::optional const &Eased) +{ + if (false == Eased.has_value()) + return {}; + if (Eased->pieces.empty() || (Eased->entry <= 0.0 && Eased->exit <= 0.0)) + return Eased->note; + auto text{Eased->compound ? format(STR_C("compound curve on from R %.0f m, a transition of %.0f m between the arcs"), Eased->from_radius, Eased->entry) : format(STR_C("transition curves %.0f / %.0f m"), Eased->entry, Eased->exit)}; + if (Eased->cant > 0.0) + text += format(STR_C(" cant %.0f mm"), Eased->cant); + if (false == Eased->note.empty()) + text += "\n" + Eased->note; + return text; +} + // the rise of the track from its start to its end, and the grades along it inline std::string vertical_of(std::vector const &Pieces) { @@ -106,17 +121,20 @@ inline std::string lines_of(std::vector const &Texts) return text; } +inline std::string narrowed(editor_track::layout const Layout, double const Radius, double const Arc) +{ + if ((Layout != editor_track::layout::arc_straight && Layout != editor_track::layout::straight_arc) || Arc == 0.0 || std::abs(Arc - Radius) < Radius * 0.01) + return {}; + return format(STR_C("R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m"), Radius, Arc); +} + inline std::string narrowed(std::vector const &Pieces, editor_track::layout const Layout, double const Radius) { - if (Layout != editor_track::layout::arc_straight && Layout != editor_track::layout::straight_arc) - return {}; double sharpest{0.0}; for (auto const &piece : Pieces) if (auto const radius{piece_plan_radius(piece)}; radius >= 0.5 && (sharpest == 0.0 || radius < sharpest)) sharpest = radius; - if (sharpest == 0.0 || sharpest >= Radius * 0.99) - return {}; - return format(STR_C("R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m"), Radius, sharpest); + return narrowed(Layout, Radius, sharpest > Radius ? 0.0 : sharpest); } inline std::string describe(editor_track::straight const &Line) diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index dfc86eb2..c8a88a32 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -265,6 +265,20 @@ double transition_length(double const Speed, double const Radius, double const C return length > 0.0 ? ceil_to(length, 5.0) : 0.0; } +double transition_between(double const Speed, double const Curvature0, double const Cant0, double const Curvature1, double const Cant1, limits const &Limits) +{ + auto const cant{std::abs(Cant1 - Cant0)}; + double length{0.0}; + if (cant > 0.0) + { + length = std::max(length, Speed * cant / (3.6 * Limits.cant_rate)); + length = std::max(length, cant / 1000.0 * Limits.ramp_factor * Speed); + } + auto const unbalanced = [&](double const Curvature, double const Cant) { return Speed * Speed / 12.96 * Curvature - kGravity * Cant / Limits.gauge; }; + length = std::max(length, std::abs(unbalanced(Curvature1, Cant1) - unbalanced(Curvature0, Cant0)) * Speed / (3.6 * Limits.jerk)); + return length > 0.0 ? ceil_to(length, 5.0) : 0.0; +} + recommendation recommend(double const Speed, double const Radius, limits const &Limits) { recommendation result; @@ -1124,6 +1138,450 @@ join_result join(join_request const &Request) return chosen; } +namespace +{ + +// elements laid one after another from a point in a direction, put together the way the alignment computes them; the cant along +// them is kept apart, a transition between two canted arcs ramps it from the one to the other +struct element_chain +{ + result shape; + glm::dvec2 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + std::vector> cants; // chainage, cant +}; + +void chain_push(element_chain &Chain, element Element, double const Cant0, double const Cant1) +{ + Element.chainage = Chain.shape.length; + Chain.shape.length += Element.length; + Chain.shape.elements.push_back(Element); + Chain.cants.push_back({Element.chainage, Cant0}); + Chain.cants.push_back({Chain.shape.length, Cant1}); +} + +void chain_straight(element_chain &Chain, double const Length) +{ + if (Length < 1e-6) + return; + element straight; + straight.kind = element_kind::straight; + straight.origin = Chain.position; + straight.direction = Chain.direction; + straight.length = Length; + Chain.position += Chain.direction * Length; + chain_push(Chain, straight, 0.0, 0.0); +} + +void chain_arc(element_chain &Chain, double const Curvature, double const Length, double const Cant) +{ + if (Length < 1e-6) + return; + if (std::abs(Curvature) < 1e-9) + { + chain_straight(Chain, Length); + return; + } + element arc; + arc.kind = element_kind::arc; + arc.turn = Curvature > 0.0 ? 1 : -1; + arc.radius = 1.0 / std::abs(Curvature); + arc.length = Length; + arc.origin = Chain.position + perpendicular(Chain.direction) / Curvature; + arc.normal = Chain.position - arc.origin; + arc.direction = Chain.direction; + auto const angle{arc.turn * Length / arc.radius}; + Chain.position = arc.origin + turned(arc.normal, angle); + Chain.direction = turned(Chain.direction, angle); + chain_push(Chain, arc, Cant, Cant); +} + +// a curvature going on through 0 turns the curve the other way: it's kept signed, the element turning to the left +void chain_spiral(element_chain &Chain, double const From, double const To, double const Length, double const Cant0, double const Cant1) +{ + if (Length < 1e-6) + return; + int const turn{(From * To < 0.0 || From + To >= 0.0) ? 1 : -1}; + element spiral; + spiral.kind = element_kind::spiral; + 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; + spiral.length = Length; + spiral.origin = Chain.position; + spiral.direction = Chain.direction; + spiral.normal = perpendicular(Chain.direction) * static_cast(turn); + auto const local{spiral_point(spiral.curvature_start, spiral.curvature_end, Length, Length)}; + Chain.position = spiral.origin + spiral.direction * local.x + spiral.normal * local.y; + Chain.direction = glm::normalize(spiral.direction * std::cos(local.angle) + spiral.normal * std::sin(local.angle)); + chain_push(Chain, spiral, Cant0, Cant1); +} + +lay_result chain_pieces(element_chain &Chain, lay_request const &Request) +{ + auto &shape{Chain.shape}; + shape.gauge = Request.norms.gauge; + shape.start = Request.start.position; + shape.end = {Chain.position.x, Request.target.y, Chain.position.y}; + shape.profile = {{0.0, Request.start.position.y, 0.0, 0.0}, {shape.length, Request.target.y, 0.0, 0.0}}; + shape.valid = shape.length > 1e-3; + design style; + style.norms = Request.norms; + lay_result result; + result.length = shape.length; + auto const count{std::max(minimum_pieces(shape, style), static_cast(std::ceil(shape.length / std::max(5.0, Request.piece_length) - 1e-9)))}; + result.pieces = pieces(shape, style, count); + auto const cant_at = [&](double const Chainage) { + auto const &cants{Chain.cants}; + for (std::size_t i = 0; i + 1 < cants.size(); i += 2) + if (Chainage <= cants[i + 1][0] + 1e-6) + { + auto const span{cants[i + 1][0] - cants[i][0]}; + auto const t{span > 1e-9 ? std::clamp((Chainage - cants[i][0]) / span, 0.0, 1.0) : 1.0}; + return cants[i][1] + (cants[i + 1][1] - cants[i][1]) * t; + } + return cants.empty() ? 0.0 : cants.back()[1]; + }; + auto const roll = [&](double const Cant) { return static_cast(glm::degrees(std::asin(std::clamp(Cant / Request.norms.gauge, -1.0, 1.0)))); }; + double chainage{0.0}; + for (auto &piece : result.pieces) + { + piece.rolls[0] = roll(cant_at(chainage)); + chainage += geometry::bezier{piece}.plan_length(); + piece.rolls[1] = roll(cant_at(chainage)); + } + if (false == result.pieces.empty()) + result.pieces.front().rolls[0] = static_cast(Request.start.roll); + return result; +} + +// a transition of the length given from the curvature of the start, as the position and the direction it ends with +struct spiral_end +{ + double x{0.0}; + double y{0.0}; + double angle{0.0}; + int turn{1}; + + spiral_end(double const From, double const To, double const Length) + { + turn = (From * To < 0.0 || From + To >= 0.0) ? 1 : -1; + auto const local{spiral_point(From * turn, To * turn, Length, Length)}; + x = local.x; + y = local.y; + angle = local.angle; + } + void apply(glm::dvec2 &Position, glm::dvec2 &Direction) const + { + auto const normal{perpendicular(Direction) * static_cast(turn)}; + Position += Direction * x + normal * y; + Direction = glm::normalize(Direction * std::cos(angle) + normal * std::sin(angle)); + } +}; + +void arc_end(glm::dvec2 &Position, glm::dvec2 &Direction, double const Curvature, double const Length) +{ + auto const centre{Position + perpendicular(Direction) / Curvature}; + auto const angle{Curvature * Length}; + Position = centre + turned(Position - centre, angle); + Direction = turned(Direction, angle); +} + +} // namespace + +std::optional lay_on(lay_request const &Request) +{ + auto const &norms{Request.norms}; + if (glm::length(Request.start.direction) < 1e-9) + return std::nullopt; + glm::dvec2 const start{Request.start.position.x, Request.start.position.z}; + auto const direction{glm::normalize(Request.start.direction)}; + glm::dvec2 const target{Request.target.x, Request.target.z}; + auto const offset{target - start}; + auto const chord{glm::length(offset)}; + auto const lateral{cross(direction, offset)}; + if (chord < 0.5 || glm::dot(offset, direction) <= 0.0) + return std::nullopt; + auto const k0{Request.curvature}; + auto const c0{norms.gauge * std::sin(glm::radians(Request.start.roll))}; + if (Request.kind == lay_kind::straight && std::abs(k0) < 1e-9 && std::abs(c0) < 5.0) + return std::nullopt; + if (Request.kind == lay_kind::arc && std::abs(lateral) < 0.005 * chord + 0.05 && std::abs(k0) < 1e-9 && std::abs(c0) < 5.0) + return std::nullopt; + int const side{lateral > 0.0 ? 1 : -1}; + // the speed the arc allows and its cant; a transition is ramped for the lower speed of its two ends + auto const allowed = [&](double const Curvature) { return std::abs(Curvature) < 1e-9 ? Request.speed : std::max(1.0, std::min(Request.speed, speed_limit(1.0 / std::abs(Curvature), norms))); }; + auto const cant_of = [&](double const Curvature) { return std::abs(Curvature) < 1e-9 ? 0.0 : std::copysign(recommend(allowed(Curvature), 1.0 / std::abs(Curvature), norms).cant, Curvature); }; + auto const slower = [&](double const From, double const To) { return std::min(allowed(From), allowed(To)); }; + auto const between = [&](double const From, double const Fromcant, double const To, double const Tocant) { + if (std::abs(To - From) <= 0.05 * std::max(std::abs(From), std::abs(To)) && std::abs(Tocant - Fromcant) < 5.0) + return 0.0; + return transition_between(slower(From, To), From, Fromcant, To, Tocant, norms); + }; + // the change of the cant a transition of the length ramps at the speed + auto const ramped = [&](double const Length, double const Speed) { return std::min(Length * 1000.0 / (norms.ramp_factor * Speed), norms.cant_rate * 3.6 * Length / Speed); }; + + // the transitions of the norms cut to the share, the cant of the arc lowered to what they can ramp; none where the cant of the + // end can't be ramped to one leaning into the arc + struct layout_plan + { + double out{0.0}; // out of the curvature of the end to the straight, before the straight of the layout + double entry{0.0}; + double exit{0.0}; + double cant{0.0}; + double from{0.0}; // the curvature and the cant the entry starts from + double fromcant{0.0}; + bool lowered{false}; + }; + auto const planned = [&](double const Curvature, bool const Lead, bool const Exit, double const Share) -> std::optional { + layout_plan result; + auto const wanted{cant_of(Curvature)}; + if (Lead) + { + result.out = between(k0, c0, 0.0, 0.0) * Share; + if (std::abs(c0) > ramped(result.out, slower(k0, 0.0)) + 1e-6) + return std::nullopt; + } + else + { + result.from = k0; + result.fromcant = c0; + } + result.entry = between(result.from, result.fromcant, Curvature, wanted) * Share; + result.exit = Exit ? between(Curvature, wanted, 0.0, 0.0) * Share : 0.0; + auto cant{wanted}; + if (result.entry > 0.0) + { + auto const reach{ramped(result.entry, slower(result.from, Curvature))}; + cant = std::clamp(cant, result.fromcant - reach, result.fromcant + reach); + } + else + cant = result.fromcant; + if (Exit) + { + auto const reach{result.exit > 0.0 ? ramped(result.exit, allowed(Curvature)) : 0.0}; + cant = std::clamp(cant, -reach, reach); + } + if (cant * Curvature < -1e-9 || (result.entry > 0.0 && std::abs(cant - result.fromcant) > ramped(result.entry, slower(result.from, Curvature)) + 1e-6) || (result.entry <= 0.0 && std::abs(cant - result.fromcant) > 1e-6)) + return std::nullopt; + result.cant = cant; + result.lowered = std::abs(cant) < std::abs(wanted) - 0.5; + return result; + }; + auto const begin = [&]() { + element_chain chain; + chain.position = start; + chain.direction = direction; + return chain; + }; + auto const made = [&](element_chain &Chain, double const Curvature, layout_plan const &Plan, double const Share) { + auto result{chain_pieces(Chain, Request)}; + result.radius = std::abs(Curvature) > 1e-9 ? 1.0 / std::abs(Curvature) : 0.0; + result.cant = std::abs(Plan.cant); + result.entry = Plan.out > 0.0 ? Plan.out : Plan.entry; + result.exit = Plan.out > 0.0 ? Plan.entry : Plan.exit; + result.from_radius = std::abs(k0) > 1e-9 ? 1.0 / std::abs(k0) : 0.0; + result.compound = k0 * Curvature > 0.0 && Plan.out <= 0.0 && Plan.entry > 0.0; + if (Share < 1.0) + result.note = format(STR_C("The transition curves are cut to %.0f%% of what the norms ask for, there's no more room"), Share * 100.0); + if (Plan.lowered) + result.note += (result.note.empty() ? "" : "\n") + format(STR_C("The cant is lowered to %.0f mm, what the transition curves ramp"), std::abs(Plan.cant)); + return result; + }; + + auto const arc_through = [&](double const Share) -> std::optional { + auto const reference{2.0 * lateral / (chord * chord)}; + auto curvature{reference}; + std::optional plan; + // the transition and the cant follow the curvature found, which moves with them + for (int pass = 0; pass < 4; ++pass) + { + plan = planned(curvature, false, false, Share); + if (false == plan.has_value()) + return std::nullopt; + // over the curvatures turning to the target, from flat to sharp, it goes from inside of the circle of the arc to outside + auto const outside = [&](double const Curvature) { + auto position{start}; + auto heading{direction}; + spiral_end{k0, Curvature, plan->entry}.apply(position, heading); + return glm::distance(target, position + perpendicular(heading) / Curvature) - 1.0 / std::abs(Curvature); + }; + auto low{std::log(1e-6)}; + auto high{std::log(0.1)}; + if (outside(side * std::exp(low)) >= 0.0 || outside(side * std::exp(high)) <= 0.0) + return std::nullopt; + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (outside(side * std::exp(middle)) < 0.0 ? low : high) = middle; + } + curvature = side * std::exp((low + high) * 0.5); + } + // a transition too long for the way to the target squeezes the arc after it, a shorter one is tried then + if (std::abs(curvature) > 1.3 * std::abs(reference) + 1e-5) + return std::nullopt; + auto chain{begin()}; + chain_spiral(chain, k0, curvature, plan->entry, c0, plan->cant); + auto const centre{chain.position + perpendicular(chain.direction) / curvature}; + auto const angle{geometry::signed_angle(chain.position - centre, target - centre) * side}; + if (angle <= 1e-4 || angle >= kPi) + return std::nullopt; + chain_arc(chain, curvature, angle / std::abs(curvature), plan->cant); + return made(chain, curvature, *plan, Share); + }; + + auto const arc_straight = [&](double const Share) -> std::optional { + auto const curvature{side / std::max(1.0, Request.radius)}; + auto const plan{planned(curvature, false, true, Share)}; + if (false == plan.has_value()) + return std::nullopt; + auto position{start}; + auto heading{direction}; + spiral_end{k0, curvature, plan->entry}.apply(position, heading); + spiral_end const out{curvature, 0.0, plan->exit}; + // the arc grows until the straight after the transition out of it runs through the target + auto const miss = [&](double const Arc) { + auto at{position}; + auto towards{heading}; + arc_end(at, towards, curvature, Arc); + out.apply(at, towards); + return std::array{cross(towards, target - at) * side, glm::dot(towards, target - at)}; + }; + if (miss(0.0)[0] <= 0.0) + return std::nullopt; + auto const radius{1.0 / std::abs(curvature)}; + auto const step{std::max(0.5, radius * glm::radians(2.0))}; + double low{0.0}; + double high{-1.0}; + for (auto arc{step}; arc < 2.0 * kPi * radius; arc += step) + { + if (miss(arc)[0] <= 0.0) + { + high = arc; + break; + } + low = arc; + } + if (high < 0.0) + return std::nullopt; + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (miss(middle)[0] > 0.0 ? low : high) = middle; + } + auto const arc{(low + high) * 0.5}; + auto const straight{miss(arc)[1]}; + if (straight < 0.0) + return std::nullopt; + auto chain{begin()}; + chain_spiral(chain, k0, curvature, plan->entry, c0, plan->cant); + chain_arc(chain, curvature, arc, plan->cant); + chain_spiral(chain, curvature, 0.0, plan->exit, plan->cant, 0.0); + chain_straight(chain, straight); + return made(chain, curvature, *plan, Share); + }; + + auto const straight_arc = [&](double const Share) -> std::optional { + auto const curvature{side / std::max(1.0, Request.radius)}; + auto const plan{planned(curvature, true, false, Share)}; + if (false == plan.has_value()) + return std::nullopt; + auto lead{begin()}; + chain_spiral(lead, k0, 0.0, plan->out, c0, 0.0); + spiral_end const into{0.0, curvature, plan->entry}; + // the end of the arc with no straight before it; the straight moves it along the lead, the arc sideways + auto const end = [&](double const Arc) { + auto at{lead.position}; + auto towards{lead.direction}; + into.apply(at, towards); + arc_end(at, towards, curvature, Arc); + return at; + }; + auto const miss = [&](double const Arc) { return cross(lead.direction, target - end(Arc)) * side; }; + auto const most{kPi / std::abs(curvature)}; + if (miss(0.0) <= 0.0 || miss(most) > 0.0) + return std::nullopt; + double low{0.0}; + double high{most}; + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (miss(middle) > 0.0 ? low : high) = middle; + } + auto const arc{(low + high) * 0.5}; + auto const straight{glm::dot(lead.direction, target - end(arc))}; + if (straight < 0.5) + return std::nullopt; + auto chain{lead}; + chain_straight(chain, straight); + chain_spiral(chain, 0.0, curvature, plan->entry, 0.0, plan->cant); + chain_arc(chain, curvature, arc, plan->cant); + return made(chain, curvature, *plan, Share); + }; + + auto const straight = [&](double const Share) -> std::optional { + auto const plan{planned(0.0, true, false, Share)}; + if (false == plan.has_value()) + return std::nullopt; + auto chain{begin()}; + chain_spiral(chain, k0, 0.0, plan->out, c0, 0.0); + auto const along{glm::dot(chain.direction, target - chain.position)}; + if (along < 0.5) + return std::nullopt; + chain_straight(chain, along); + return made(chain, 0.0, *plan, Share); + }; + + auto const first = [](auto const &Layout) -> std::optional { + for (auto const share : {1.0, 0.75, 0.5, 0.35, 0.25}) + if (auto laid{Layout(share)}; laid.has_value()) + return laid; + return std::nullopt; + }; + std::optional laid; + switch (Request.kind) + { + case lay_kind::straight: laid = first(straight); break; + case lay_kind::arc: laid = first(arc_through); break; + case lay_kind::straight_arc: + laid = first(straight_arc); + if (false == laid.has_value()) + laid = first(arc_through); + break; + default: + laid = first(arc_straight); + if (false == laid.has_value()) + laid = first(arc_through); + break; + } + if (laid.has_value()) + return laid; + lay_result refused; + refused.note = std::abs(c0) >= 5.0 ? format(STR_C("No room to ramp the cant of %.0f mm the track ends with, the track is laid without transition curves"), std::abs(c0)) + : std::string{STR_C("No room for the transition curves of the norms, the track is laid without them")}; + return refused; +} + +void recommend_between(vertex &Vertex, double const Speed, limits const &Limits) +{ + if (false == Vertex.compound || Vertex.arcs.empty()) + return; + auto sharpest{std::max(1.0, Vertex.radius)}; + 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 previous{std::max(1.0, Vertex.radius)}; + for (auto &arc : Vertex.arcs) + { + 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); + previous = radius; + } +} + bool tangent_intersection(design const &Design, glm::dvec2 &Point) { auto const start{plan_of(Design.start)}; @@ -1460,7 +1918,7 @@ constexpr double kTraceRadiusMax{50000.0}; constexpr double kTraceRobust{0.25}; // the parameters: the logarithms of the radii, then the weights of the shares of all but the first radius, as logarithms too -vertex traced_shape(vertex const &Base, std::vector const &Parameters, int const Radii) +vertex traced_shape(design const &Design, vertex const &Base, std::vector const &Parameters, int const Radii) { auto shape{Base}; auto const radius = [&](int const Index) { return std::exp(std::clamp(Parameters[Index], std::log(kTraceRadiusMin), std::log(kTraceRadiusMax))); }; @@ -1488,6 +1946,8 @@ vertex traced_shape(vertex const &Base, std::vector const &Parameters, i arc.share = weights[i] / total; shape.arcs.push_back(arc); } + if (Design.norms.checked) + recommend_between(shape, Design.speed, Design.norms); return shape; } @@ -1514,7 +1974,7 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< return [&, Count](std::vector const &Parameters, std::vector &Residuals) { auto trial{Design}; trial.norms.checked = false; - trial.vertices[Vertex] = traced_shape(base, Parameters, Count); + trial.vertices[Vertex] = traced_shape(Design, base, Parameters, Count); auto const result{compute(trial)}; if (false == result.valid) return false; @@ -1645,10 +2105,12 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< take(3); } - auto shape{traced_shape(base, fits[chosen].parameters, chosen)}; + auto shape{traced_shape(Design, base, fits[chosen].parameters, chosen)}; shape.radius = std::max(kTraceRadiusMin, std::round(shape.radius)); for (auto &arc : shape.arcs) arc.radius = std::max(kTraceRadiusMin, std::round(arc.radius)); + if (Design.norms.checked) + recommend_between(shape, Design.speed, Design.norms); auto trial{Design}; trial.vertices[Vertex] = shape; auto const result{compute(trial)}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index eaba18a0..a96c00b8 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -9,6 +9,7 @@ http: #pragma once +#include #include #include #include @@ -38,6 +39,8 @@ struct limits double arc_min{20.0}; double arc_min_factor{0.4}; bool checked{true}; + + bool operator==(limits const &) const = default; }; struct compound_arc @@ -92,6 +95,9 @@ struct recommendation }; recommendation recommend(double const Speed, double const Radius, limits const &Limits); double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits); +// the transition curve from the curvature and the cant of one arc to those of the next one, 0 for a straight; the curvature positive +// 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); enum class element_kind @@ -217,6 +223,8 @@ struct join_end glm::dvec3 position{0.0}; glm::dvec2 direction{0.0, 1.0}; double roll{0.0}; + + bool operator==(join_end const &) const = default; }; enum class join_kind @@ -259,4 +267,47 @@ struct join_result join_result join(join_request const &Request); +enum class lay_kind +{ + arc_straight, + straight_arc, + arc, + straight +}; + +// a track laid on from an end the way the layout lays it, with the transition curves of the norms: from the curvature the end has +// into the arc, between the arcs of a compound curve where both turn the same way, and out of the arc to the straight +struct lay_request +{ + join_end start; // the roll in degrees, as the track goes on + double curvature{0.0}; // of the end as the track goes on, positive turning from x towards z + glm::dvec3 target{0.0}; + lay_kind kind{lay_kind::arc_straight}; + double radius{500.0}; // of the arc, but the arc through the target + double speed{100.0}; + double piece_length{100.0}; + limits norms; + + bool operator==(lay_request const &) const = default; +}; + +// no pieces where the norms find no room for the transitions, the note says why +struct lay_result +{ + std::vector pieces; + std::string note; + double length{0.0}; + double radius{0.0}; + double cant{0.0}; + double entry{0.0}; // the transition out of the curvature of the end + double exit{0.0}; + double from_radius{0.0}; // of the end, 0 for a straight + bool compound{false}; +}; + +std::optional lay_on(lay_request const &Request); + +// the transitions between the arcs of a compound curve which the norms ask for at the speed, the cant of the vertex on its sharpest arc +void recommend_between(vertex &Vertex, double const Speed, limits const &Limits); + } diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 5c8cf9b0..8fc41f2b 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -178,6 +178,14 @@ class editor_track glm::dvec2 direction{0.0}; std::vector pieces; }; + // the curvature and the roll a track ends with at Joint, as it goes on beyond the joint + struct bend + { + double curvature{0.0}; // positive turning from x towards z + double roll{0.0}; + glm::dvec2 direction{0.0}; + }; + static std::optional bend_at(TTrack const &Neighbour, glm::dvec3 const &Joint); static easing ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits); static bool eases_room(easing const &In, easing const &Out, glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection); static std::vector eased_pieces(easing const &In, std::vector const &Middle, easing const &Out); diff --git a/editor/editorTrackShape.cpp b/editor/editorTrackShape.cpp index ff0a2e01..0c856517 100644 --- a/editor/editorTrackShape.cpp +++ b/editor/editorTrackShape.cpp @@ -640,33 +640,48 @@ std::vector editor_track::layout_pieces(layout const Kind, glm::dv return straight_then_arc(Point, Direction, Grade, offset, radius, side); } -editor_track::easing editor_track::ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits) +std::optional editor_track::bend_at(TTrack const &Neighbour, glm::dvec3 const &Joint) { - easing result; for (auto const &path : Neighbour.m_paths) for (auto const atend : {false, true}) { if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > 0.05) continue; + bend result; + auto const sign{atend ? 1.0 : -1.0}; + result.roll = atend ? static_cast(path.rolls[1]) : -static_cast(path.rolls[0]); if (false == has_controls(path)) + { + auto const chord{plan_of(path.points[segment_data::point::end] - path.points[segment_data::point::start])}; + if (glm::length(chord) < 1e-9) + return std::nullopt; + result.direction = glm::normalize(chord) * sign; return result; + } bezier const curve{path}; auto const first{curve.first(atend ? 1.0 : 0.0)}; auto const second{curve.second(atend ? 1.0 : 0.0)}; auto const speed{std::hypot(first.x, first.z)}; if (speed < 1e-9) - return result; - auto const sign{atend ? 1.0 : -1.0}; - auto const curvature{sign * (first.x * second.z - first.z * second.x) / (speed * speed * speed)}; - glm::dvec2 const direction{sign * first.x / speed, sign * first.z / speed}; - auto const roll{atend ? static_cast(path.rolls[1]) : -static_cast(path.rolls[0])}; - result.curvature = curvature; - if (std::abs(curvature) < 1e-4) - return result; - result.length = std::max(20.0, alignment::recommend(Speed, 1.0 / std::abs(curvature), Limits).transition); - lay_transition(result, Joint, direction, Grade, roll); + return std::nullopt; + result.curvature = sign * (first.x * second.z - first.z * second.x) / (speed * speed * speed); + result.direction = {sign * first.x / speed, sign * first.z / speed}; return result; } + return std::nullopt; +} + +editor_track::easing editor_track::ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits) +{ + easing result; + auto const end{bend_at(Neighbour, Joint)}; + if (false == end.has_value()) + return result; + result.curvature = end->curvature; + if (std::abs(end->curvature) < 1e-4) + return result; + result.length = std::max(20.0, alignment::recommend(Speed, 1.0 / std::abs(end->curvature), Limits).transition); + lay_transition(result, Joint, end->direction, Grade, end->roll); return result; } diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 77d020a8..5043132f 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10222,3 +10222,24 @@ msgstr "LPM: punkt na środku toru na zdjęciu. Enter: zastosuj, Esc: porzuć" msgid "Click: one more point, the curve follows. Ctrl+click: take a point out" msgstr "LPM: kolejny punkt, łuk idzie za nimi. Ctrl+LPM: usuń punkt" + +msgid "compound curve on from R %.0f m, a transition of %.0f m between the arcs" +msgstr "łuk koszowy dalej od R %.0f m, krzywa przejściowa %.0f m między łukami" + +msgid "Compound curve: R %.0f m goes on into R %.0f m through a transition curve of %.0f m" +msgstr "Łuk koszowy: R %.0f m przechodzi w R %.0f m przez krzywą przejściową %.0f m" + +msgid "transition curves %.0f / %.0f m" +msgstr "krzywe przejściowe %.0f / %.0f m" + +msgid "No room for the transition curves of the norms, the track is laid without them" +msgstr "Brak miejsca na krzywe przejściowe wg norm, tor ułożony bez nich" + +msgid "No room to ramp the cant of %.0f mm the track ends with, the track is laid without transition curves" +msgstr "Brak miejsca, żeby zejść z przechyłką %.0f mm z końca toru, tor ułożony bez krzywych przejściowych" + +msgid "The transition curves are cut to %.0f%% of what the norms ask for, there's no more room" +msgstr "Krzywe przejściowe skrócone do %.0f%% długości wg norm, nie ma na więcej miejsca" + +msgid "The cant is lowered to %.0f mm, what the transition curves ramp" +msgstr "Przechyłka obniżona do %.0f mm, tyle zdążą podnieść krzywe przejściowe"