From 97b6a84d1bd870ba9c5f0e1e5c16c0869ee55800 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 25 Oct 2026 12:28:55 +0100 Subject: [PATCH] fix(editor): a curve laid from a free end on a curve, or to one, goes on from its curvature: traced (Tab) from the end of the diverging track of a switch it begins right at P4 at the radius of the switch, not on a straight nor with a transition from a curvature of 0. The design of the line takes the curve of the adjoining track at each end (start_curved/end_curved with the radius, the side and the cant, kept with it in the scenery): the curve of the first vertex turning its way, or of the last one, begins right at the end, its transition going from that curvature and cant to those of its first arc, as between the arcs of a compound curve, none where it goes on at its radius; the radius of its arcs is the one which begins it there, solved for the tangent, the curves next to it making room on the straight. A curve traced next to such an end is fitted by its transitions alone, with none between the curves where the points keep to that as well, or laid from a straight there where it doesn't go on from it or that keeps to the points much closer: eased out of the curve of a plain track by the transition of the norms, the line beginning at its end, or right from P4, as a switch goes on with a straight. A bend of straights turning the way of the curve gets its curve going on from it with the transitions and the cant of the norms; the other way, or a straight, as before: eased out of the curve of a plain track, the jump of the curvature kept at a switch, as at its start, and not warned of. The cant of the adjoining track goes on into the curve with no ramp at that end; the norms ask for the transition between the curves only where the curvature differs by more than 5 % or the cant by 5 mm. The readout by the cursor, the status after laying and the plan tell the curve going on from the track, the plan with a box to go on or not. Dragged back to P4 from a point the curve goes on from its radius too. Selftests laywalk, laybends, curvejoint, endcurves; unit tests of a curve going on from a switch and into a free end, its cant, traced, through a transition into a larger radius, and of a bend of straights after such an end --- application/editormode.h | 3 +- application/editormodelay.cpp | 159 +++++++- application/editormoderoute.cpp | 18 + application/editormodeselftest.cpp | 5 + application/editormodeselftestjoin.cpp | 199 ++++++++++ application/editormodeselftesttrack.cpp | 7 + application/editormodetrack.cpp | 5 + application/editormodetrackshared.h | 16 + editor/editorAlignment.cpp | 488 ++++++++++++++++++++++-- editor/editorAlignment.hpp | 15 + editor/editorAlignmentStore.cpp | 18 +- lang/editor_pl.po | 36 ++ tests/unit/test_alignment.cpp | 279 ++++++++++++++ 13 files changed, 1210 insertions(+), 38 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 671da0e8..164efc18 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1513,7 +1513,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri // the track through the points clicked, each the way its mode says; End: the free end it finishes at, the last point then std::vector lay_pieces(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, double &Length, std::string &Error, lay_line_plan *Plan = nullptr) const; - bool lay_line(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error) const; + // Adjoining: the curve next to a free end on a curve goes on from its curvature, where it turns its way; else from a straight there + bool lay_line(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error, bool const Adjoining = true) const; // the curves of a stretch traced, the ones fitted lately kept for the same points // none while the stretch the cursor moves along is still being fitted; Meanwhile: the fit done last of the same points but the one // at the cursor until then diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index d2a031c9..d0b9261d 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -46,6 +46,29 @@ http: using namespace track_mode; +namespace +{ + +// the curve of the track of a free end the line laid goes on from at its start, or into at its end: its radius, the side it turns to +// as the line runs and its cant leaning into the turn; nothing on a straight +void adjoining_curve(editor_track::snap_target const &End, bool const Atend, double const Gauge, alignment::design &Design) +{ + if (End.track == nullptr || (End.track->eType != tt_Normal && End.track->eType != tt_Switch)) + return; + auto const bend{editor_track::bend_at(*End.track, End.position)}; + if (false == bend.has_value() || std::abs(bend->curvature) < 1e-4) + return; + // the curvature as the track goes on beyond its end: the way the line runs at its start, against it at its end + auto const curvature{Atend ? -bend->curvature : bend->curvature}; + auto const cant{Gauge * std::sin(glm::radians(bend->roll)) * (bend->curvature > 0.0 ? 1.0 : -1.0)}; + (Atend ? Design.end_radius : Design.start_radius) = 1.0 / std::abs(curvature); + (Atend ? Design.end_turn : Design.start_turn) = curvature > 0.0 ? 1 : -1; + (Atend ? Design.end_cant : Design.start_cant) = std::max(0.0, cant); + (Atend ? Design.end_curved : Design.start_curved) = true; +} + +} // namespace + void editor_mode::update_build_tools() { glm::dvec3 const ground{cursor_ground()}; @@ -437,7 +460,8 @@ void editor_mode::point_drag_finish() 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 (From == nullptr || From->eType != tt_Normal) + // from the diverging track of a switch a curve goes on from its curvature, a straight goes on right from its end, as from the switch + if (From == nullptr || (From->eType != tt_Normal && (From->eType != tt_Switch || m_track_layout == track_layout::straight))) return std::nullopt; auto const end{editor_track::bend_at(*From, Point)}; if (false == end.has_value()) @@ -971,7 +995,8 @@ std::vector editor_mode::lay_pieces(std::vector const return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); }; std::vector pieces; - // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them + // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them. Not + // at a switch: its diverging track ends its curve with a jump as it begins it, a straight going on right from there auto const grade{(Points.back().y - Points.front().y) / std::max(1.0, plan_distance(Points.front(), Points.back()))}; auto const eased = [&](editor_track::snap_target const &Joint, double const Chainage, double const Sign) { if (Joint.track == nullptr || Joint.track->eType != tt_Normal) @@ -987,15 +1012,22 @@ std::vector editor_mode::lay_pieces(std::vector const if ((false == in.pieces.empty() || false == out.pieces.empty()) && editor_track::eases_room(in, out, Points.front(), design.start_direction, Points.back(), design.end_direction)) { auto easeddesign{design}; + // an end moved out of the curve of the adjoining track is on a straight if (false == in.pieces.empty()) { easeddesign.start = in.end; easeddesign.start_direction = in.direction; + easeddesign.start_curved = false; + easeddesign.start_radius = 0.0; + easeddesign.start_turn = 0; } if (false == out.pieces.empty()) { easeddesign.end = out.end; easeddesign.end_direction = -out.direction; + easeddesign.end_curved = false; + easeddesign.end_radius = 0.0; + easeddesign.end_turn = 0; } // the bends stay where they are, measured from the ends moved if (easeddesign.vertices.size() >= 2 && shape.vertices.size() == easeddesign.vertices.size()) @@ -1019,6 +1051,12 @@ std::vector editor_mode::lay_pieces(std::vector const { Length = shape.length; pieces = alignment::pieces(shape, design, count(shape, design)); + // a line lay_line begins at the end of the transition out of the curve of the free end keeps the transition before it + auto const after = [&](editor_track::easing const &Easing, glm::dvec3 const &Joint, glm::dvec3 const &Own) { return false == Easing.pieces.empty() && plan_distance(Joint, Own) > 0.01; }; + auto const &before{after(in, Points.front(), design.start) ? in : editor_track::easing{}}; + auto const &beyond{after(out, Points.back(), design.end) ? out : editor_track::easing{}}; + Length = editor_track::eased_length(before, Length, beyond); + pieces = editor_track::eased_pieces(before, pieces, beyond); } // the heights: fitted to the ground, or from point to point through the points clicked TTrack const *style{tool.start.track != nullptr ? tool.start.track : End.track != nullptr ? End.track : tool.copy_selected ? selected_track() : nullptr}; @@ -1033,7 +1071,7 @@ std::vector editor_mode::lay_pieces(std::vector const return pieces; } -bool editor_mode::lay_line(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error) const +bool editor_mode::lay_line(std::vector const &Points, std::vector const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error, bool const Adjoining) const { auto const &tool{m_lay}; Plan = {}; @@ -1043,6 +1081,22 @@ bool editor_mode::lay_line(std::vector const &Points, std::vector 1e-6}; + // from a free end on a curve, or to one, the curve laid next to it turning its way goes on from its curvature: it begins right at the + // end, not on a straight, its transition going from that curvature. Where no such curve fits, the line is laid from a straight there, + // which lay_pieces eases out of the curve + if (Adjoining) + { + if (tool.start.track != nullptr && glm::length(plan_of(tool.start.direction)) > 1e-6) + adjoining_curve(tool.start, false, design.norms.gauge, design); + if (joined) + adjoining_curve(End, true, design.norms.gauge, design); + } + auto const curved{design.start_curved || design.end_curved}; + auto const from_straight = [&]() { return curved && lay_line(Points, Modes, End, Plan, Error, false); }; + // the end of the transition out of the curve of the free end, the start of the line where a curve traced next to it is laid from a + // straight there; the same at the end + std::optional> easedstart; + std::optional> easedend; // the points between the start and the end: a free end reached is the last point auto const inner{Points.size() - (joined ? 1 : 0)}; auto const mode_of = [&](std::size_t const Index) { return Index < Modes.size() ? Modes[Index] : lay_mode::straights; }; @@ -1117,6 +1171,8 @@ bool editor_mode::lay_line(std::vector const &Points, std::vector const &Points, std::vector kept; + auto const easing = [&](editor_track::snap_target const &Joint, double const Grade) { + return Joint.track != nullptr && Joint.track->eType == tt_Normal ? editor_track::ease_out_of(*Joint.track, Joint.position, Grade, design.speed, design.norms) : editor_track::easing{}; + }; + auto const in{base.start_curved ? easing(tool.start, 0.0) : editor_track::easing{}}; + auto const out{base.end_curved ? easing(End, 0.0) : editor_track::easing{}}; + if (false == in.pieces.empty()) + { + plain.start = in.end; + plain.start_direction = in.direction; + } + if (false == out.pieces.empty()) + { + plain.end = out.end; + plain.end_direction = -out.direction; + } + // the points along the transitions out of the curves left out, the line from the straights after them + for (auto const &point : traced) + if ((in.pieces.empty() || glm::distance(point, plan_of(base.start)) > in.length) && (out.pieces.empty() || glm::distance(point, plan_of(base.end)) > out.length)) + kept.push_back(point); + auto const other{kept.size() >= 2 ? traced_stretch(plain, kept, tool.radii, tool.straights_between) : std::optional{alignment::trace_stretch{}}}; + if (false == other.has_value()) + { + Error = STR_C("Fitting the curve to the points..."); + return false; + } + auto goes_on{false}; + if (fitted->valid) + { + auto laid{base}; + laid.vertices = fitted->vertices; + for (auto const &curve : alignment::compute(laid).curves) + goes_on = goes_on || curve.curvature_in > 0.0 || curve.curvature_out > 0.0; + } + // a curve going on which keeps to the points within 8 cm on average and 30 cm at most, as the fits take it for one keeping to + // them, stays: the straight one fits the points clicked a little off closer with the radii it has more + auto const loose{fitted->valid && (fitted->rms > 0.08 || fitted->worst > 0.3)}; + if (other->valid && (false == fitted->valid || false == goes_on || (loose && other->rms < 0.75 * fitted->rms))) + { + fitted = other; + // the line laid from the end of the transition out of the curve, which lay_pieces lays before it + if (false == in.pieces.empty()) + easedstart = std::make_pair(in.end, in.direction); + if (false == out.pieces.empty()) + easedend = std::make_pair(out.end, -out.direction); + // from a straight there; at a switch with the jump of the curvature it has, which isn't warned of + auto const straight_at = [&](editor_track::snap_target const &Joint, bool &Curved, double &Radius, int &Turn) { + Curved = false; + if (Joint.track != nullptr && Joint.track->eType == tt_Switch) + { + Radius = 0.0; + Turn = 0; + } + }; + if (base.start_curved) + straight_at(tool.start, design.start_curved, design.start_radius, design.start_turn); + if (base.end_curved) + straight_at(End, design.end_curved, design.end_radius, design.end_turn); + } + } auto const &fit{*fitted}; if (false == fit.valid) { Error = STR_C("No curve follows the points clicked along it"); - return false; + return from_straight(); } for (auto const &vertex : fit.vertices) bends.push_back({lifted(vertex.position, entry.y), vertex, false}); @@ -1259,6 +1386,13 @@ bool editor_mode::lay_line(std::vector const &Points, std::vector const &Points, std::vector const &Points, std::vector editor_mode::traced_stretch(alignment::design const &Base, std::vector const &Points, int const Radii, bool const Straights, bool const Meanwhile) const { // the stretches traced before the one the cursor moves along are the same from frame to frame: their fits are kept + // the curves of the adjoining tracks it goes on from or into, signed by their turn, and their cants + auto const curvature = [](bool const Curved, double const Radius, int const Turn) { return Curved && Radius > 0.0 ? Turn / Radius : 0.0; }; std::vector key{Base.start.x, Base.start.z, Base.start_direction.x, Base.start_direction.y, Base.end.x, Base.end.z, Base.end_direction.x, Base.end_direction.y, Base.speed, - static_cast(Base.shape), static_cast(Radii), Straights ? 1.0 : 0.0, Base.start_reserve, Base.end_reserve}; + static_cast(Base.shape), static_cast(Radii), Straights ? 1.0 : 0.0, Base.start_reserve, Base.end_reserve, + curvature(Base.start_curved, Base.start_radius, Base.start_turn), Base.start_curved ? Base.start_cant : 0.0, curvature(Base.end_curved, Base.end_radius, Base.end_turn), + Base.end_curved ? Base.end_cant : 0.0}; for (auto const &point : Points) { key.push_back(point.x); @@ -1330,7 +1468,7 @@ std::optional editor_mode::traced_stretch(alignment::d } m_stretch_fitter.ask({key, Base, Points, Radii, Straights}); std::vector last; - auto const header{std::size_t{14}}; + auto const header{std::size_t{18}}; if (Meanwhile && m_stretch_fitter.latest(last, fit) && last.size() >= header + 2 && key.size() >= header + 2 && std::equal(key.begin(), key.begin() + 4, last.begin()) && std::equal(key.begin() + 8, key.begin() + header, last.begin() + 8)) { @@ -1447,6 +1585,11 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) 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); + // the curves next to the free ends going on from the curves of their tracks + if (plan.shape.valid) + for (auto const &curve : plan.shape.curves) + if (auto const on{curve_goes_on(curve)}; false == on.empty()) + tool.status += "\n" + format(STR_C("Curve %d %s"), curve.vertex + 1, on.c_str()); if (eased.has_value() && false == eased->note.empty()) tool.status += "\n" + eased->note; { diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index d4695b6f..ed3331a5 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -277,6 +277,11 @@ void editor_mode::route_take_stored(int const Index) design.norms = stored.design.norms; design.transition_pieces = stored.design.transition_pieces; design.arc_piece_angle = stored.design.arc_piece_angle; + // a curve laid going on from the curve of the track adjoining keeps going on from it, its radius the one which begins it there + design.start_curved = stored.design.start_curved; + design.end_curved = stored.design.end_curved; + design.start_cant = stored.design.start_cant; + design.end_cant = stored.design.end_cant; route.designed = static_cast(design.vertices.size()); route.origin = stored.origin; route.vertex = -1; @@ -2105,6 +2110,19 @@ bool editor_mode::render_route_vertex(int const Index) return edited; }; ImU32 const transition{IM_COL32(255, 140, 30, 255)}; + // the first curve, or the last one, may go on from the curve of the track adjoining at that end: it begins right there, its radius + // the one which begins it there, its entry, or exit, the transition between the two curves + auto const count{static_cast(design.vertices.size())}; + for (auto const atend : {false, true}) + { + auto const radius{atend ? design.end_radius : design.start_radius}; + if ((atend ? Index + 1 != count : Index != 0) || radius <= 0.0 || radius >= 10000.0 || (atend && count == 1 && design.start_curved)) + continue; + auto &curved{atend ? design.end_curved : design.start_curved}; + changed |= ImGui::Checkbox((format(atend ? STR_C("Goes into the curve of the track at the end (R %.0f m)") : STR_C("Goes on from the curve of the track at the start (R %.0f m)"), radius) + (atend ? "##curvedend" : "##curvedstart")).c_str(), &curved); + item_tooltip("The curve begins right at the end of the fragment, at the curvature of the track adjoining there, where it turns the same way; its radius is the one " + "which begins it there, the transition at that end goes between the two curves"); + } 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", part::arc); ImGui::SameLine(); changed |= field("##cant", vertex.cant, 1.0f, STR_C("cant %.0f mm"), IM_COL32(240, 170, 60, 255), diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 6ec1a20c..c4bc00d7 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -129,6 +129,11 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined // layspeed the speed set for the laying, and whether it goes also on from a track with its own // laymode , layradius , laytrace as the panel of the laying sets them +// endcurves logs every free end of the rails: where, the way out and the curvature its track goes on with +// curvejoint logs the paths meeting at the end nearest the point, the curvature of each and its jump from the ones before to the ones after +// laywalk ... a line of known curves on from the free end nearest the point, +// the points along it clicked in the mode of a curve, from the end or towards it (see the command) +// laybends ... the bends of straights clicked from the free end nearest the point // layp ... the points clicked in the mode set, on the ground, a free end within 3 m reached // layreport