From aeef2cebb384e7205f907c0cb0fdfe32512848c1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 25 Oct 2026 00:11:58 +0200 Subject: [PATCH] feat(editor): the plan shows which curve is edited: each named W1, W2... as its vertex is in the view, with its km and "Show in the view", which flies the camera over it; the part of it whose field the mouse is over or which is being edited (the whole curve, an arc of a compound one, the transition in, out or between two arcs) lit up in the view and named there with its radius, cant or length. The points clicked when a line is laid are kept with its design in the scenery ("click" lines, moved with it): the plan shows them in the colours of their departure and, while "Keep to the points clicked" is on, fits the radii of a curve whose transitions or cants are changed again to them, its transitions and cants as set; "Transitions for these cants" makes the transitions as long as the norms ask for at the radii and the cants set, an arc of a compound curve with no cant lengthening the ones either side of it. Unit test: 50 m between the arcs become 85 m, the curve 4 cm off the points clicked instead of 58 --- application/editormode.h | 40 ++++ application/editormodelay.cpp | 3 +- application/editormoderoute.cpp | 341 ++++++++++++++++++++++++++++++-- editor/editorAlignment.cpp | 156 ++++++++++----- editor/editorAlignment.hpp | 8 +- editor/editorAlignmentStore.cpp | 46 ++++- editor/editorAlignmentStore.hpp | 10 +- lang/editor_pl.po | 54 +++++ tests/unit/test_alignment.cpp | 43 ++++ 9 files changed, 633 insertions(+), 68 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 390aa04e..1743486d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -985,6 +985,35 @@ class editor_mode : public application_mode, private editor_track::observer, pri float metres{1.0f}; // pixels of a metre and of a unit of the curvature, kept as the drag goes float curvature{1.0f}; } band; + // the part of a curve whose field the mouse is over in the panel, or which is being edited there, shown in the view + struct curve_focus + { + enum class part + { + whole, + entry, + exit, + arc, // of the radius `arc`, the first one 0 + between // the transition leading into the arc of the radius `arc` + }; + int vertex{-1}; + part what{part::whole}; + int arc{0}; + bool held{false}; // by a field being edited, the mouse over the others doesn't move it + } focus; + // the points clicked when the fragment was laid, kept with its design: the curves keep to them as their transitions and cants + // change, while hold is on + std::vector clicks; + bool hold{true}; + struct click_state + { + double offset{0.0}; // from the line, m + int vertex{-1}; // whose curve it's a point of + bool corner{false}; // a bend of straights clicked, the vertex itself rather than a point of its curve + }; + std::vector click_states; // as the line was at the revision measured + int revision{0}; // of the result, counted as it's computed + int measured{-1}; // points clicked along the middle of the track seen on the orthophoto, the radii of the curves and their shares fitted to them after each click struct trace_state { @@ -1003,6 +1032,16 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool render_route_vertex(int const Index); void render_route_result(); void render_route_trace(); + // the camera over the stretch of the line between the chainages, a curve seen whole + void route_show_curve(double const From, double const To); + // the departures of the points clicked from the line and the curves they belong to, measured again when the line changed + void route_measure_clicks(); + // the points clicked along the curve of the vertex, in the plan, the bends of straights left out + std::vector route_clicks_of(int const Vertex) const; + // the radii of the curve of the vertex fitted again to the points clicked along it, its transitions and cants as they are + bool route_hold_clicks(int const Vertex); + // the elements of the part of the curve in focus in the panel, in their order along the line + std::vector route_focused() const; void route_trace_start(); void route_trace_fit(); void route_trace_end(bool const Keep); @@ -1072,6 +1111,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri { m_route.design.norms.checked = true; m_route.result = alignment::compute(m_route.design); + ++m_route.revision; } void route_apply(); void route_lay(); diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 7d81a871..395e3eed 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -1429,8 +1429,9 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) // the scenery keeps the design the tracks were laid by, as it is, for the plan to edit; read from the paths again it wouldn't be // the same, a compound curve with transitions between its arcs least of all. A track laid with no design of the line, between two // free ends or back from one, keeps none: the plan reads its curves when it's opened + // with the points clicked, which the plan fits the curves to again when their transitions or cants change if (plan.shape.valid && false == created.empty() && false == scene::Layers.empty()) - alignment_store::store(plan.design, plan.shape, 0.0, alignment_store::layer_of(*created.front()), "drawn"); + alignment_store::store(plan.design, plan.shape, 0.0, alignment_store::layer_of(*created.front()), "drawn", 0.0, tool.points); if (tool.chain.empty()) tool.history = m_history.size(); push_track_snapshot({}, created); diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index b54b364d..768898c7 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -93,6 +93,9 @@ void editor_mode::route_reset() route.grip = -1; route.result = {}; route.trace = {}; + route.clicks.clear(); + route.click_states.clear(); + route.measured = -1; if (route.from == nullptr || route.to == nullptr) return; if (false == editor_track::find_chain(route.from, route.to, route.chain, route.error, true)) @@ -191,6 +194,9 @@ bool editor_mode::route_load(TTrack &Track) if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track))) return false; route_read_stored(Track); + // the points clicked along the line when it was laid, wherever they're kept + m_route.clicks = alignment_store::clicks_along(m_route.result, 3.0); + m_route.measured = -1; return true; } @@ -317,7 +323,7 @@ void editor_mode::route_store() auto &route{m_route}; if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) return; - alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front())); + alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front()), "designed", 0.0, route.clicks); route.departed = -1; route.departure = 0.0; } @@ -1043,6 +1049,137 @@ bool editor_mode::route_hit(int &Vertex, int &Grip) const return hit >= 0 || griphit >= 0; } +void editor_mode::route_show_curve(double const From, double const To) +{ + auto const &result{m_route.result}; + if (result.elements.empty() || To <= From) + return; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{std::numeric_limits::lowest()}; + for (int i = 0; i <= 32; ++i) + { + auto const point{alignment::evaluate(result, From + (To - From) * i / 32.0).position}; + low = glm::min(low, point); + high = glm::max(high, point); + } + auto const along{plan_of(alignment::evaluate(result, (From + To) * 0.5).direction)}; + auto const flat{glm::length(along) > 1e-9 ? glm::normalize(along) : glm::dvec2{0.0, 1.0}}; + // from above, leaning a little along the line, the whole curve in sight + auto const look{glm::normalize(glm::dvec3{flat.x * 0.45, -1.0, flat.y * 0.45})}; + auto const extent{std::max(high.x - low.x, high.z - low.z)}; + fly_to((low + high) * 0.5, look, std::clamp(extent * 1.2, 60.0, 3000.0)); +} + +std::vector editor_mode::route_focused() const +{ + using part = route_design::curve_focus::part; + auto const &focus{m_route.focus}; + auto const &result{m_route.result}; + std::vector own; + if (focus.vertex < 0) + return own; + for (std::size_t i = 0; i < result.elements.size(); ++i) + if (result.elements[i].vertex == focus.vertex && result.elements[i].length > 1e-6) + own.push_back(i); + if (own.empty() || focus.what == part::whole) + return own; + // the curvature at the start and at the end of an element, the arcs of a compound curve found by their radii + auto const curvatures = [](alignment::element const &Element) -> std::pair { + auto const own{Element.radius > 0.0 ? 1.0 / Element.radius : 0.0}; + switch (Element.kind) + { + case alignment::element_kind::transition_in: return {0.0, own}; + case alignment::element_kind::arc: return {own, own}; + case alignment::element_kind::transition_out: return {own, 0.0}; + case alignment::element_kind::spiral: return {Element.curvature_start, Element.curvature_end}; + default: return {0.0, 0.0}; + } + }; + auto const same = [](double const A, double const B) { return std::abs(A - B) <= 1e-6 * std::max(std::abs(A), std::abs(B)) + 1e-12; }; + std::vector radii; + for (auto const &curve : result.curves) + if (curve.vertex == focus.vertex) + radii = curve.radii.size() > 1 ? curve.radii : std::vector{curve.radius}; + std::vector found; + for (auto const i : own) + { + auto const [start, end] {curvatures(result.elements[i])}; + auto const arc{static_cast(focus.arc)}; + switch (focus.what) + { + case part::entry: if (i == own.front() && start == 0.0 && end > 0.0) found.push_back(i); break; + case part::exit: if (i == own.back() && end == 0.0 && start > 0.0) found.push_back(i); break; + case part::arc: if (arc < radii.size() && radii[arc] > 0.0 && same(start, 1.0 / radii[arc]) && same(end, start)) found.push_back(i); break; + case part::between: + if (arc >= 1 && arc < radii.size() && radii[arc] > 0.0 && radii[arc - 1] > 0.0 && same(start, 1.0 / radii[arc - 1]) && same(end, 1.0 / radii[arc])) + found.push_back(i); + break; + default: break; + } + } + // a part of no length, the arc of two transition curves meeting say, shows the whole curve + return found.empty() ? own : found; +} + +void editor_mode::route_measure_clicks() +{ + auto &route{m_route}; + if (route.measured == route.revision) + return; + route.measured = route.revision; + route.click_states.clear(); + auto const &result{route.result}; + if (route.clicks.empty() || false == result.valid) + return; + std::vector plan; + for (auto const &click : route.clicks) + plan.push_back(plan_of(click)); + auto const projections{alignment::project(result, plan)}; + auto const count{static_cast(route.design.vertices.size())}; + for (std::size_t i = 0; i < plan.size() && i < projections.size(); ++i) + { + route_design::click_state state; + state.offset = projections[i].offset; + // the curve nearest along the line, as the points traced in the plan go to their curves + for (int k = 0; k < count && k < static_cast(result.vertex_chainages.size()); ++k) + if (state.vertex < 0 || std::abs(result.vertex_chainages[k] - projections[i].chainage) < std::abs(result.vertex_chainages[state.vertex] - projections[i].chainage)) + state.vertex = k; + for (int k = 0; k < count && false == state.corner; ++k) + state.corner = plan_distance(route.clicks[i], route_vertex_position(k)) < 0.05; + route.click_states.push_back(state); + } +} + +std::vector editor_mode::route_clicks_of(int const Vertex) const +{ + auto const &route{m_route}; + std::vector points; + for (std::size_t i = 0; i < route.click_states.size() && i < route.clicks.size(); ++i) + { + auto const &state{route.click_states[i]}; + // a point clicked far off the line belongs to none of its curves + if (state.vertex == Vertex && false == state.corner && std::abs(state.offset) <= 3.0) + points.push_back(plan_of(route.clicks[i])); + } + return points; +} + +bool editor_mode::route_hold_clicks(int const Vertex) +{ + auto &route{m_route}; + route_measure_clicks(); + auto const points{route_clicks_of(Vertex)}; + if (points.size() < 3 || Vertex < 0 || Vertex >= static_cast(route.design.vertices.size())) + return false; + auto design{route.design}; + design.norms.checked = true; + auto const fit{alignment::refit_radii(design, static_cast(Vertex), points)}; + if (false == fit.valid) + return false; + route.design.vertices[Vertex] = fit.fitted; + return true; +} + void editor_mode::draw_route_overlay() const { auto const &route{m_route}; @@ -1090,6 +1227,60 @@ void editor_mode::draw_route_overlay() const previous = next; } } + // the part of the curve whose fields are pointed at or edited in the panel, lit up and named + if (auto const focused{route_focused()}; false == focused.empty()) + { + using part = route_design::curve_focus::part; + auto const &focus{route.focus}; + for (auto const index : focused) + { + auto const &e{result.elements[index]}; + int const steps = std::clamp(static_cast(e.length / 2.0), 1, 400); + auto previous{alignment::evaluate(result, e.chainage).position}; + for (int j = 1; j <= steps; ++j) + { + auto const next{alignment::evaluate(result, e.chainage + e.length * j / steps).position}; + projection.line(drawlist, previous, next, IM_COL32(60, 230, 255, 120), 11.0f); + projection.line(drawlist, previous, next, IM_COL32(200, 250, 255, 255), 3.0f); + previous = next; + } + } + auto const &first{result.elements[focused.front()]}; + auto const &last{result.elements[focused.back()]}; + alignment::curve_report const *report{nullptr}; + for (auto const &curve : result.curves) + if (curve.vertex == focus.vertex) + report = &curve; + std::string name; + if (report != nullptr) + { + auto const compound{report->radii.size() > 1}; + auto const arc{static_cast(focus.arc)}; + switch (focus.what) + { + case part::entry: name = format(STR_C("W%d transition in %.0f m"), focus.vertex + 1, first.length); break; + case part::exit: name = format(STR_C("W%d transition out %.0f m"), focus.vertex + 1, last.length); break; + case part::between: name = format(STR_C("W%d transition R%d-R%d %.0f m"), focus.vertex + 1, focus.arc, focus.arc + 1, first.length); break; + case part::arc: + if (compound && arc < report->radii.size()) + name = format(STR_C("W%d R%d %.0f m cant %.0f mm"), focus.vertex + 1, focus.arc + 1, report->radii[arc], arc < report->cants.size() ? report->cants[arc] : report->cant); + else + name = format(STR_C("W%d R %.0f m cant %.0f mm"), focus.vertex + 1, report->radius, report->cant); + break; + default: name = format("W%d ", focus.vertex + 1) + curve_label(*report, design.norms); break; + } + } + ImVec2 middle; + if (false == name.empty() && projection.project(alignment::evaluate(result, (first.chainage + last.chainage + last.length) * 0.5).position, middle)) + { + auto const size{ImGui::GetFontSize() * 1.25f}; + auto const text{ImGui::GetFont()->CalcTextSizeA(size, FLT_MAX, 0.0f, name.c_str())}; + ImVec2 const corner{middle.x - text.x * 0.5f, middle.y - text.y - 22.0f}; + drawlist->AddRectFilled(ImVec2(corner.x - 6.0f, corner.y - 3.0f), ImVec2(corner.x + text.x + 6.0f, corner.y + text.y + 3.0f), IM_COL32(10, 20, 30, 210), 4.0f); + drawlist->AddText(ImGui::GetFont(), size, corner, IM_COL32(200, 250, 255, 255), name.c_str()); + drawlist->AddLine(ImVec2(middle.x, corner.y + text.y + 3.0f), middle, IM_COL32(200, 250, 255, 200), 1.5f); + } + } ImVec2 pointed; if (route.band.at >= 0.0 && projection.project(alignment::evaluate(result, std::min(route.band.at, result.length)).position, pointed)) { @@ -1133,6 +1324,20 @@ void editor_mode::draw_route_overlay() const } drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); } + // the points clicked when the line was laid, in the colours of their departure; how far each is, along the curve in focus + for (std::size_t i = 0; i < route.clicks.size() && i < route.click_states.size(); ++i) + { + auto const &state{route.click_states[i]}; + ImVec2 screen; + if (state.corner || std::abs(state.offset) > 3.0 || false == projection.project(route.clicks[i], screen)) + continue; + auto const departure{std::abs(state.offset)}; + auto const colour{departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255)}; + drawlist->AddCircleFilled(screen, 4.5f, IM_COL32(0, 0, 0, 200), 12); + drawlist->AddCircleFilled(screen, 3.0f, colour, 12); + if (state.vertex == route.focus.vertex) + drawlist->AddText(ImVec2(screen.x + 7.0f, screen.y - 7.0f), colour, format("%.0f cm", departure * 100.0).c_str()); + } if (route.trace.points.empty()) return; std::vector plan; @@ -1712,6 +1917,20 @@ bool editor_mode::render_route_vertices() { bool changed{false}; auto const count{static_cast(m_route.design.vertices.size())}; + m_route.focus = {}; + route_measure_clicks(); + if (false == m_route.clicks.empty()) + { + ImGui::Checkbox(STR_C("Keep to the points clicked"), &m_route.hold); + item_tooltip("The scenery keeps the points clicked when the line was laid: a curve whose transitions or cants are changed gets its radii fitted to them again, so it stays where it was seen on the photo"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Fit all to them"))) + { + for (int i = 0; i < count; ++i) + changed |= route_hold_clicks(i); + } + item_tooltip("The radii of every curve fitted again to the points clicked along it, the transitions and the cants as they are"); + } for (int i = 0; i < count; ++i) { ImGui::PushID(i); @@ -1726,15 +1945,81 @@ bool editor_mode::render_route_vertex(int const Index) auto &route{m_route}; auto &design{route.design}; auto &vertex{design.vertices[Index]}; - auto const count{static_cast(design.vertices.size())}; bool changed{false}; - if (count > 1) - ImGui::SeparatorText(format(STR_C("Curve %d of the S-curve"), Index + 1).c_str()); + // a transition or a cant changed, the edit done: the curve fitted to the points clicked again, its radii given by them + bool settled{false}; + using part = route_design::curve_focus::part; + // the field just drawn shows its part of the curve in the view while the mouse is over it or it's being edited + auto const mark = [&](part const What, int const Arc) { + if (ImGui::IsItemActive()) + route.focus = {Index, What, Arc, true}; + else if (false == route.focus.held && ImGui::IsItemHovered()) + route.focus = {Index, What, Arc, false}; + }; + ImGui::BeginGroup(); + // named as the vertex is in the view, W1, W2..., with where it lies along the line + { + auto from{-1.0}; + auto to{-1.0}; + for (auto const &e : route.result.elements) + if (e.vertex == Index) + { + from = from < 0.0 ? e.chainage : from; + to = e.chainage + e.length; + } + auto title{format(STR_C("Curve W%d"), Index + 1)}; + if (from >= 0.0) + title += format(" km %s - %s", profile::format_chainage(route.origin + from).c_str(), profile::format_chainage(route.origin + to).c_str()); + // the vertex picked in the view stands out + auto const picked{route.vertex == Index || route.grip == Index}; + if (picked) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(overlay_color::highlight)); + ImGui::SeparatorText(title.c_str()); + if (picked) + ImGui::PopStyleColor(); + mark(part::whole, 0); + if (from >= 0.0) + { + if (ImGui::SmallButton(STR_C("Show in the view"))) + route_show_curve(from, to); + item_tooltip("The camera flies over the curve, which is lit up in the view while the mouse is over its fields"); + mark(part::whole, 0); + } + // how far the curve is from the points clicked along it when it was laid + std::size_t clicked{0}; + double sum{0.0}; + double worst{0.0}; + for (auto const &state : route.click_states) + if (state.vertex == Index && false == state.corner && std::abs(state.offset) <= 3.0) + { + ++clicked; + sum += state.offset * state.offset; + worst = std::max(worst, std::abs(state.offset)); + } + if (clicked > 0) + { + ImGui::SameLine(); + auto const colour{worst <= 0.1 ? ImVec4(0.35f, 0.86f, 0.43f, 1.0f) : worst <= 0.3 ? ImVec4(1.0f, 0.78f, 0.24f, 1.0f) : ImVec4(1.0f, 0.35f, 0.27f, 1.0f)}; + ImGui::TextColored(colour, STR_C("points clicked %zu: %.0f cm on average, at most %.0f cm"), clicked, std::sqrt(sum / static_cast(clicked)) * 100.0, worst * 100.0); + item_tooltip("The points clicked along the curve when it was laid, kept in the scenery, and how far the curve is from them now"); + mark(part::whole, 0); + if (clicked >= 3) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Fit to them"))) + changed |= route_hold_clicks(Index); + item_tooltip("The radii of the curve and their shares fitted again to the points clicked, the transitions and the cants as they are"); + } + } + } // two fields in a row, the name and the unit inside them, a bar in the colour of the element in the band and in the view auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; - auto const field = [&](char const *Id, double &Value, float const Speed, char const *Format, ImU32 const Colour, char const *Tooltip) { + auto const field = [&](char const *Id, double &Value, float const Speed, char const *Format, ImU32 const Colour, char const *Tooltip, part const What) { ImGui::SetNextItemWidth(half); auto const edited{ImGui::DragScalar(Id, ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format)}; + // the radius is the user's own, the curve no longer fitted after it + settled |= What != part::arc && ImGui::IsItemDeactivatedAfterEdit(); + mark(What, 0); auto const low{ImGui::GetItemRectMin()}; auto const high{ImGui::GetItemRectMax()}; ImGui::GetWindowDrawList()->AddRectFilled(low, ImVec2(low.x + 3.0f, high.y), Colour, 2.0f, ImDrawFlags_RoundCornersLeft); @@ -1742,14 +2027,15 @@ bool editor_mode::render_route_vertex(int const Index) return edited; }; 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"); + 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), 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"); + : "Height of the outer rail above the inner one", + part::whole); + changed |= field("##entry", vertex.transition_in, 1.0f, STR_C("entry %.0f m"), transition, "Length of the transition curve leading into the arc", part::entry); 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"); + changed |= field("##exit", vertex.transition_out, 1.0f, STR_C("exit %.0f m"), transition, "Length of the transition curve leading out of the arc", part::exit); ImGui::PushItemWidth(110.0f); if (ImGui::Checkbox(STR_C("Two transition curves, no arc"), &vertex.spirals)) { @@ -1777,6 +2063,8 @@ bool editor_mode::render_route_vertex(int const Index) 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")}; + settled |= ImGui::IsItemDeactivatedAfterEdit(); + mark(part::arc, Arc); 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"); @@ -1787,11 +2075,13 @@ bool editor_mode::render_route_vertex(int const Index) { Cant = -1.0; edited = true; + settled = 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"); + mark(part::arc, 0); ImGui::SameLine(); ImGui::TextDisabled("%.0f%%", percent(vertex.share)); changed |= arc_cant(0, vertex.first_cant); @@ -1802,12 +2092,17 @@ bool editor_mode::render_route_vertex(int const Index) ImGui::PushID(k); ImGui::Separator(); changed |= ImGui::DragScalar(format(STR_C("Radius R%d (m)"), k + 2).c_str(), ImGuiDataType_Double, &arc.radius, 5.0f, nullptr, nullptr, "%.0f"); + mark(part::arc, k + 1); ImGui::SameLine(); if (ImGui::SmallButton("x")) removed = k; item_tooltip("Takes this radius out of the curve"); + mark(part::arc, k + 1); changed |= ImGui::DragScalar(format(STR_C("Transition R%d-R%d (m)"), k + 1, k + 2).c_str(), ImGuiDataType_Double, &arc.transition, 1.0f, nullptr, nullptr, "%.0f"); + settled |= ImGui::IsItemDeactivatedAfterEdit(); + mark(part::between, k + 1); changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f"); + mark(part::arc, k + 1); ImGui::SameLine(); ImGui::TextDisabled("%.0f%%", percent(arc.share)); changed |= arc_cant(k + 1, arc.cant); @@ -1839,17 +2134,32 @@ bool editor_mode::render_route_vertex(int const Index) vertex.transition_out = std::max(0.0, vertex.transition_out); vertex.cant = std::max(0.0, vertex.cant); { - if (ImGui::SmallButton(STR_C("Apply recommended for V"))) - { - route_recommend(vertex); + // the transitions follow the radii, which follow the points clicked: a few rounds settle them + auto const reshape = [&](auto const &Shape) { + for (int pass = 0; pass < 3; ++pass) + { + Shape(); + if (false == route.hold || false == route_hold_clicks(Index)) + break; + } changed = true; - } + }; + if (ImGui::SmallButton(STR_C("Apply recommended for V"))) + reshape([&] { route_recommend(vertex); }); if (ImGui::IsItemHovered()) { auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; ImGui::SetTooltip("%s\n%s", STR_C("Cant and transitions recommended for the design speed and the radius"), format(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition).c_str()); } + if (false == vertex.spirals) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Transitions for these cants"))) + reshape([&] { alignment::norm_transitions(vertex, design.speed, design.norms); }); + item_tooltip("The transition curves as long as the norms ask for at these radii and cants, the cants staying: a cant lowered asks for longer ones"); + mark(part::whole, 0); + } } if (ImGui::TreeNode(STR_C("Details"))) { @@ -1864,6 +2174,11 @@ bool editor_mode::render_route_vertex(int const Index) item_tooltip("For the loops: the curve turns by more than a half circle"); ImGui::TreePop(); } + ImGui::EndGroup(); + if (route.focus.vertex != Index && false == route.focus.held && ImGui::IsItemHovered()) + route.focus = {Index, part::whole, 0, false}; + if (settled && route.hold) + changed |= route_hold_clicks(Index); return changed; } diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 64823f7c..66410010 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -1700,6 +1700,26 @@ void recommend_between(vertex &Vertex, double const Speed, limits const &Limits) } } +void norm_transitions(vertex &Vertex, double const Speed, limits const &Limits) +{ + if (Vertex.spirals) + return; + if (false == Vertex.compound || Vertex.arcs.empty()) + { + Vertex.transition_in = Vertex.transition_out = transition_length(std::max(1.0, Speed), std::max(1.0, Vertex.radius), std::max(0.0, Vertex.cant), Limits); + return; + } + // at the speed the sharpest arc allows, as the warnings of compute check them + 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 allowed{std::max(1.0, std::min(Speed, speed_limit(sharpest, Limits)))}; + auto const cants{compound_cants(Vertex)}; + Vertex.transition_in = transition_length(allowed, std::max(1.0, Vertex.radius), cants.front(), Limits); + Vertex.transition_out = transition_length(allowed, std::max(1.0, Vertex.arcs.back().radius), cants.back(), Limits); + recommend_between(Vertex, Speed, Limits); +} + bool tangent_intersection(design const &Design, glm::dvec2 &Point) { auto const start{plan_of(Design.start)}; @@ -2042,8 +2062,9 @@ constexpr double kPointsWithin{0.3}; // m, how far they may be from it, clicked by hand far out: a row of bends making a curve no closer is cut in parts constexpr double kPointsLoose{1.0}; -// 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(design const &Design, vertex const &Base, std::vector const &Parameters, int const Radii) +// the parameters: the logarithms of the radii, then the weights of the shares of all but the first radius, as logarithms too. +// Between: the transitions between the arcs those of the norms, else the ones of the base +vertex traced_shape(design const &Design, vertex const &Base, std::vector const &Parameters, int const Radii, bool const Between = true) { auto shape{Base}; auto const radius = [&](int const Index) { return std::exp(std::clamp(Parameters[Index], std::log(kTraceRadiusMin), std::log(kTraceRadiusMax))); }; @@ -2072,11 +2093,69 @@ vertex traced_shape(design const &Design, vertex const &Base, std::vector const &Points, int const Count, bool const Between) +{ + return [&Design, &Points, Vertex, Count, Between](std::vector const &Parameters, std::vector &Residuals) { + auto trial{Design}; + trial.norms.checked = false; + trial.vertices[Vertex] = traced_shape(Design, Design.vertices[Vertex], Parameters, Count, Between); + // a compound curve alone made straight away + if (Count >= 2 && trial.vertices.size() == 1) + return compound_residuals(trial, Points, Residuals); + auto const result{compute(trial)}; + if (false == result.valid) + return false; + auto from{std::numeric_limits::max()}; + auto to{std::numeric_limits::lowest()}; + for (auto const &e : result.elements) + { + if (e.vertex != static_cast(Vertex)) + continue; + from = std::min(from, e.chainage); + to = std::max(to, e.chainage + e.length); + } + if (from > to) + return false; + auto const line{sample_plan(result, from - 1.0, to + 1.0, std::clamp((to - from) / 300.0, 0.25, 2.0))}; + Residuals.clear(); + for (auto const &point : Points) + { + // a point clicked by mistake far off pulls the curve less than the square of its departure would + auto const offset{project_on(line, point).offset}; + auto const departure{std::abs(offset)}; + Residuals.push_back(departure <= kTraceRobust ? offset : std::copysign(std::sqrt(2.0 * kTraceRobust * departure - kTraceRobust * kTraceRobust), offset)); + } + return true; + }; +} + +// the curve of the vertex given the shape, and how far the points are from it +trace_fit trace_of(design const &Design, std::size_t const Vertex, vertex const &Shape, std::vector const &Points) +{ + trace_fit fit; + auto trial{Design}; + trial.vertices[Vertex] = Shape; + auto const result{compute(trial)}; + if (false == result.valid) + return fit; + fit.valid = true; + fit.fitted = Shape; + double sum{0.0}; + for (auto const &projection : project(result, Points)) + { + sum += projection.offset * projection.offset; + fit.worst = std::max(fit.worst, std::abs(projection.offset)); + } + fit.rms = std::sqrt(sum / static_cast(Points.size())); + return fit; +} + } // namespace std::vector project(result const &Result, std::vector const &Points) @@ -2096,40 +2175,7 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< if (Vertex >= Design.vertices.size() || Points.empty()) return best; auto const &base{Design.vertices[Vertex]}; - auto const residuals = [&](int const Count) -> residuals_of { - return [&, Count](std::vector const &Parameters, std::vector &Residuals) { - auto trial{Design}; - trial.norms.checked = false; - trial.vertices[Vertex] = traced_shape(Design, base, Parameters, Count); - // a compound curve alone made straight away - if (Count >= 2 && trial.vertices.size() == 1) - return compound_residuals(trial, Points, Residuals); - auto const result{compute(trial)}; - if (false == result.valid) - return false; - auto from{std::numeric_limits::max()}; - auto to{std::numeric_limits::lowest()}; - for (auto const &e : result.elements) - { - if (e.vertex != static_cast(Vertex)) - continue; - from = std::min(from, e.chainage); - to = std::max(to, e.chainage + e.length); - } - if (from > to) - return false; - auto const line{sample_plan(result, from - 1.0, to + 1.0, std::clamp((to - from) / 300.0, 0.25, 2.0))}; - Residuals.clear(); - for (auto const &point : Points) - { - // a point clicked by mistake far off pulls the curve less than the square of its departure would - auto const offset{project_on(line, point).offset}; - auto const departure{std::abs(offset)}; - Residuals.push_back(departure <= kTraceRobust ? offset : std::copysign(std::sqrt(2.0 * kTraceRobust * departure - kTraceRobust * kTraceRobust), offset)); - } - return true; - }; - }; + auto const residuals = [&](int const Count) { return trace_residuals(Design, Vertex, Points, Count, true); }; auto const cost_of = [](residuals_of const &Residuals, std::vector const &Parameters) { std::vector values; return Residuals(Parameters, values) ? squares(values) : std::numeric_limits::max(); @@ -2240,21 +2286,31 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< 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)}; - if (false == result.valid) - return best; - best.valid = true; - best.fitted = shape; - double sum{0.0}; - for (auto const &projection : project(result, Points)) + return trace_of(Design, Vertex, shape, Points); +} + +trace_fit refit_radii(design const &Design, std::size_t const Vertex, std::vector const &Points) +{ + if (Vertex >= Design.vertices.size() || Points.empty()) + return {}; + auto const &base{Design.vertices[Vertex]}; + auto const compound{base.compound && false == base.arcs.empty()}; + auto const count{compound ? static_cast(base.arcs.size()) + 1 : 1}; + // started from the curve as it is: the radii, then the shares weighed against the one of the first arc + std::vector parameters{std::log(std::max(kTraceRadiusMin, base.radius))}; + if (compound) { - sum += projection.offset * projection.offset; - best.worst = std::max(best.worst, std::abs(projection.offset)); + for (auto const &arc : base.arcs) + parameters.push_back(std::log(std::max(kTraceRadiusMin, arc.radius))); + for (auto const &arc : base.arcs) + parameters.push_back(std::log(std::max(1e-3, arc.share) / std::max(1e-3, base.share))); } - best.rms = std::sqrt(sum / static_cast(Points.size())); - return best; + least_squares(parameters, trace_residuals(Design, Vertex, Points, count, false)); + auto shape{traced_shape(Design, base, parameters, count, false)}; + shape.radius = std::max(kTraceRadiusMin, std::round(shape.radius)); + for (auto &arc : shape.arcs) + arc.radius = std::max(kTraceRadiusMin, std::round(arc.radius)); + return trace_of(Design, Vertex, shape, Points); } std::vector breaks_at(result const &Result, std::vector const &Points, double const From, double const To) diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 793c3084..d815006f 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -230,6 +230,9 @@ struct trace_fit // the radii of the curve of the vertex and their shares which follow the points traced along the track best; the tangents, the // transitions and the cant stay. Radii 0 takes the fewest radii, up to three, which follow the points about as well as more would trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector const &Points, int const Radii); +// the radii of the curve of the vertex and their shares fitted again to the points, as many radii as it has; its transitions, the +// ones between the arcs too, and its cants kept: a curve whose transitions were lengthened keeps to the points it was laid through +trace_fit refit_radii(design const &Design, std::size_t const Vertex, std::vector const &Points); std::size_t minimum_pieces(result const &Result, design const &Design); // Breaks: ascending chainages at which pieces have to end. Counts: least number of the pieces between the consecutive breaks, @@ -333,8 +336,11 @@ struct lay_result 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 +// the transitions between the arcs of a compound curve which the norms ask for at the speed and at the cants of the arcs void recommend_between(vertex &Vertex, double const Speed, limits const &Limits); +// all the transitions of the vertex as long as the norms ask for at its radii and its cants, the cants kept: a cant lowered +// lengthens them, the acceleration across left changing more along them +void norm_transitions(vertex &Vertex, double const Speed, limits const &Limits); // the length of each of the two transition curves of a vertex with spirals, meeting at the curvature of the radius double spirals_length(transition_shape const Shape, double const Deflection, double const Radius); diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp index 74ae3d4b..83acbf92 100644 --- a/editor/editorAlignmentStore.cpp +++ b/editor/editorAlignmentStore.cpp @@ -48,6 +48,7 @@ bool g_loaded{false}; // 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 +// click : a point clicked when the line was laid, its curves fitted to them // element // radii signed with the turn, 0 for no curvature // end @@ -148,6 +149,8 @@ void mark(scene::layer_handle const Layer) } lines.push_back(text); } + for (auto const &click : entry.clicks) + lines.push_back("click " + local(click)); for (auto const &e : entry.result.elements) { auto const station{alignment::evaluate(entry.result, e.chainage)}; @@ -193,6 +196,14 @@ void finish(record &Record) sample(Record); } +// the points clicked added to the ones there, each once +void add_clicks(std::vector &To, std::vector const &From) +{ + for (auto const &click : From) + if (std::none_of(To.begin(), To.end(), [&](glm::dvec3 const &Other) { return glm::distance(Other, click) < 0.01; })) + To.push_back(click); +} + // the curves of a design whose vertices are vertices of a stored alignment are taken into that alignment, in place of its own int merge(record const &Entry) { @@ -214,6 +225,7 @@ int merge(record const &Entry) if (false == same) continue; auto merged{g_records[r]}; + add_clicks(merged.clicks, Entry.clicks); for (std::size_t k = 0; k < vertices.size(); ++k) { auto const shape{backwards ? reversed_vertex(Entry.design.vertices[k]) : Entry.design.vertices[k]}; @@ -378,6 +390,12 @@ void read_part(record &Entry, std::string const &Keyword, std::istringstream &Li if (false == Line.fail()) Entry.elements.push_back(e); } + else if (Keyword == "click") + { + glm::dvec3 point; + if (Line >> point.x >> point.y >> point.z) + Entry.clicks.push_back(Context.to_world(point)); + } } void close_record(record &Entry, std::string const &Layer) @@ -756,12 +774,34 @@ std::optional chainage_on(alignment::result const &Result, glm::dvec3 co return found->chainage; } -int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer, std::string const &Source, double const Fit) +std::vector clicks_along(alignment::result const &Result, double const Reach) +{ + load(); + std::vector along; + if (false == Result.valid || Result.elements.empty()) + return along; + std::vector all; + for (auto const &entry : g_records) + add_clicks(all, entry.clicks); + if (all.empty()) + return along; + std::vector plan; + for (auto const &click : all) + plan.push_back(plan_of(click)); + auto const projections{alignment::project(Result, plan)}; + for (std::size_t i = 0; i < all.size() && i < projections.size(); ++i) + if (projections[i].chainage > 0.0 && projections[i].chainage < Result.length && std::abs(projections[i].offset) <= Reach) + along.push_back(all[i]); + return along; +} + +int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer, std::string const &Source, double const Fit, std::vector const &Clicks) { load(); record entry; entry.source = Source; entry.fit = Fit; + entry.clicks = Clicks; entry.design = Design; entry.design.start_reserve = 0.0; entry.design.end_reserve = 0.0; @@ -780,6 +820,8 @@ int store(alignment::design const &Design, alignment::result const &Result, doub if (index >= 0) { entry.layer = g_records[index].layer; + if (entry.clicks.empty()) + entry.clicks = g_records[index].clicks; g_records[index] = std::move(entry); } else if ((index = merge(entry)) >= 0) @@ -842,6 +884,8 @@ void transform(int const Index, glm::dvec3 const &From, glm::dvec2 const &Fromdi design.end_direction = geometry::turned(design.end_direction, angle); for (auto &vertex : design.vertices) vertex.position = plan_of(place({vertex.position.x, 0.0, vertex.position.y})); + for (auto &click : entry.clicks) + click = place(click); finish(entry); entry.elements.clear(); for (auto const &e : entry.result.elements) diff --git a/editor/editorAlignmentStore.hpp b/editor/editorAlignmentStore.hpp index aea28a14..0d0dbdbb 100644 --- a/editor/editorAlignmentStore.hpp +++ b/editor/editorAlignmentStore.hpp @@ -51,6 +51,8 @@ struct record glm::dvec2 low{0.0}; glm::dvec2 high{0.0}; std::vector samples; + // the points clicked when the line was laid, its curves fitted to them, so they can be fitted again + std::vector clicks; }; struct hit @@ -84,8 +86,12 @@ std::optional vertex_of(std::vector> int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed); // stored alignment near the tracks, whose line they no longer follow; Departure receives the largest distance int departed(std::vector const &Tracks, double &Departure); -// keeps the design in the file of the layer, in place of the one with the same ends -int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer, std::string const &Source = "designed", double const Fit = 0.0); +// keeps the design in the file of the layer, in place of the one with the same ends; with no points clicked given it keeps the +// ones of the design it takes the place of +int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer, std::string const &Source = "designed", double const Fit = 0.0, + std::vector const &Clicks = {}); +// the points clicked when the lines were laid, kept with them, which lie by the line within the reach, between its ends +std::vector clicks_along(alignment::result const &Result, double const Reach); bool follows(alignment::result const &Result, std::vector const &Tracks); double deviation(alignment::result const &Result, std::vector const &Tracks, TTrack *&Worst); std::optional chainage_on(alignment::result const &Result, glm::dvec3 const &Point); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index a6c0e1c4..464111a9 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11920,3 +11920,57 @@ 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" + +msgid "Curve W%d" +msgstr "Łuk W%d" + +msgid "Show in the view" +msgstr "Pokaż w widoku" + +msgid "The camera flies over the curve, which is lit up in the view while the mouse is over its fields" +msgstr "Kamera przelatuje nad łuk; łuk świeci w widoku, gdy mysz jest nad jego polami" + +msgid "points clicked %zu: %.0f cm on average, at most %.0f cm" +msgstr "kliknięte punkty %zu: średnio %.0f cm, najwyżej %.0f cm" + +msgid "The points clicked along the curve when it was laid, kept in the scenery, and how far the curve is from them now" +msgstr "Punkty kliknięte wzdłuż łuku przy kładzeniu, zapisane w scenerii, i jak daleko łuk jest teraz od nich" + +msgid "Fit to them" +msgstr "Dopasuj do nich" + +msgid "The radii of the curve and their shares fitted again to the points clicked, the transitions and the cants as they are" +msgstr "Promienie łuku i ich udziały dopasowane od nowa do klikniętych punktów; przejściówki i przechyłki bez zmian" + +msgid "Keep to the points clicked" +msgstr "Trzymaj się kliknięć" + +msgid "The scenery keeps the points clicked when the line was laid: a curve whose transitions or cants are changed gets its radii fitted to them again, so it stays where it was seen on the photo" +msgstr "Sceneria pamięta punkty kliknięte przy kładzeniu linii: łuk ze zmienioną przejściówką albo przechyłką dostaje promienie dopasowane do nich od nowa, więc zostaje tam, gdzie był widoczny na zdjęciu" + +msgid "Fit all to them" +msgstr "Dopasuj wszystkie" + +msgid "The radii of every curve fitted again to the points clicked along it, the transitions and the cants as they are" +msgstr "Promienie każdego łuku dopasowane od nowa do punktów klikniętych wzdłuż niego; przejściówki i przechyłki bez zmian" + +msgid "Transitions for these cants" +msgstr "Przejściówki do tych przechyłek" + +msgid "The transition curves as long as the norms ask for at these radii and cants, the cants staying: a cant lowered asks for longer ones" +msgstr "Krzywe przejściowe tak długie, jak normy wymagają przy tych promieniach i przechyłkach; przechyłki zostają. Mniejsza przechyłka wymaga dłuższych przejściówek" + +msgid "W%d transition in %.0f m" +msgstr "W%d przejściowa wejściowa %.0f m" + +msgid "W%d transition out %.0f m" +msgstr "W%d przejściowa wyjściowa %.0f m" + +msgid "W%d transition R%d-R%d %.0f m" +msgstr "W%d przejściowa R%d-R%d %.0f m" + +msgid "W%d R%d %.0f m cant %.0f mm" +msgstr "W%d R%d %.0f m przechyłka %.0f mm" + +msgid "W%d R %.0f m cant %.0f mm" +msgstr "W%d R %.0f m przechyłka %.0f mm" diff --git a/tests/unit/test_alignment.cpp b/tests/unit/test_alignment.cpp index 9f4c5990..033bbc81 100644 --- a/tests/unit/test_alignment.cpp +++ b/tests/unit/test_alignment.cpp @@ -303,6 +303,49 @@ TEST_CASE("alignment: each arc of a compound curve has a cant of its own, ramped 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: an arc of a compound curve with no cant gets a longer transition, the curve fitted again to the points it was laid through") { + auto design{straights({600.0, 600.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, 0.0, 0.5}}; + vertex.transition_in = vertex.transition_out = 80.0; + vertex.cant = 120.0; + alignment::recommend_between(vertex, design.speed, design.norms); + auto const line{alignment::compute(design)}; + REQUIRE(line.valid); + auto const points{traced(line, 15.0, 0.0)}; + auto const before{vertex.arcs.front().transition}; + + // the second arc with no cant, the first one keeping its own + auto lowered{design}; + auto &shape{lowered.vertices.front()}; + shape.first_cant = 70.0; + shape.arcs.front().cant = 0.0; + alignment::norm_transitions(shape, lowered.speed, lowered.norms); + auto const between{shape.arcs.front().transition}; + CHECK(between == doctest::Approx(alignment::transition_between(lowered.speed, 1.0 / 1200.0, 70.0, 1.0 / 700.0, 0.0, lowered.norms))); + CHECK(between > before); + + auto const departure = [&](alignment::design const &Design) { + double sum{0.0}; + std::vector plan(points.begin(), points.end()); + for (auto const &projection : alignment::project(alignment::compute(Design), plan)) + sum += projection.offset * projection.offset; + return std::sqrt(sum / static_cast(points.size())); + }; + auto const fit{alignment::refit_radii(lowered, 0, points)}; + REQUIRE(fit.valid); + // the transitions and the cants as they were set, the radii moved to keep to the points + CHECK(fit.fitted.arcs.front().transition == doctest::Approx(between)); + CHECK(fit.fitted.transition_in == doctest::Approx(shape.transition_in)); + CHECK(fit.fitted.arcs.front().cant == doctest::Approx(0.0)); + CHECK(fit.fitted.first_cant == doctest::Approx(70.0)); + CHECK(fit.rms < departure(lowered)); + CHECK(fit.rms < 0.3); +} + 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;