17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-28 01:47:28 +01:00

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

This commit is contained in:
maj00r
2026-10-25 00:11:58 +02:00
parent 2a399a92a5
commit aeef2cebb3
9 changed files with 633 additions and 68 deletions

View File

@@ -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<glm::dvec3> 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_state> 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<glm::dvec2> 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<std::size_t> 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();

View File

@@ -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);

View File

@@ -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<double>::max()};
glm::dvec3 high{std::numeric_limits<double>::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<std::size_t> 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<std::size_t> 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<double, double> {
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<double> radii;
for (auto const &curve : result.curves)
if (curve.vertex == focus.vertex)
radii = curve.radii.size() > 1 ? curve.radii : std::vector<double>{curve.radius};
std::vector<std::size_t> found;
for (auto const i : own)
{
auto const [start, end] {curvatures(result.elements[i])};
auto const arc{static_cast<std::size_t>(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<glm::dvec2> 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<int>(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<int>(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<glm::dvec2> editor_mode::route_clicks_of(int const Vertex) const
{
auto const &route{m_route};
std::vector<glm::dvec2> 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<int>(route.design.vertices.size()))
return false;
auto design{route.design};
design.norms.checked = true;
auto const fit{alignment::refit_radii(design, static_cast<std::size_t>(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<int>(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<std::size_t>(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<glm::dvec2> plan;
@@ -1712,6 +1917,20 @@ bool editor_mode::render_route_vertices()
{
bool changed{false};
auto const count{static_cast<int>(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<int>(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<double>(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<std::size_t>(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;
}

View File

@@ -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<double> 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<double> 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<double
arc.share = weights[i] / total;
shape.arcs.push_back(arc);
}
if (Design.norms.checked)
if (Between && Design.norms.checked)
recommend_between(shape, Design.speed, Design.norms);
return shape;
}
// the departures of the points from the curve of the vertex shaped by the parameters, as traced_shape takes them
residuals_of trace_residuals(design const &Design, std::size_t const Vertex, std::vector<glm::dvec2> const &Points, int const Count, bool const Between)
{
return [&Design, &Points, Vertex, Count, Between](std::vector<double> const &Parameters, std::vector<double> &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<double>::max()};
auto to{std::numeric_limits<double>::lowest()};
for (auto const &e : result.elements)
{
if (e.vertex != static_cast<int>(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<glm::dvec2> 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<double>(Points.size()));
return fit;
}
} // namespace
std::vector<plan_projection> project(result const &Result, std::vector<glm::dvec2> 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<double> const &Parameters, std::vector<double> &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<double>::max()};
auto to{std::numeric_limits<double>::lowest()};
for (auto const &e : result.elements)
{
if (e.vertex != static_cast<int>(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<double> const &Parameters) {
std::vector<double> values;
return Residuals(Parameters, values) ? squares(values) : std::numeric_limits<double>::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<glm::dvec2> 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<int>(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<double> 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<double>(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<double> breaks_at(result const &Result, std::vector<glm::dvec2> const &Points, double const From, double const To)

View File

@@ -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<glm::dvec2> 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<glm::dvec2> 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_result> 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);

View File

@@ -48,6 +48,7 @@ bool g_loaded{false};
// vertex <x> <z> <offset> <R> <L in> <L out> <cant> <reverse 0/1> <compound 0/1> <share> <arcs> [<R> <L> <share>]... [spirals 1]
// spirals: two transition curves with no arc between them, their lengths following from R
// [cants <first> <arc>...]: the cants of the radii of a compound curve set one by one, below 0 the cant in proportion to the curvature
// click <x y z>: a point clicked when the line was laid, its curves fitted to them
// element <straight|transition|arc> <vertex> <chainage> <length> <R at the start> <R at the end> <cant> <start x> <start z> <direction x> <direction z>
// 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<glm::dvec3> &To, std::vector<glm::dvec3> 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<double> 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<glm::dvec3> clicks_along(alignment::result const &Result, double const Reach)
{
load();
std::vector<glm::dvec3> along;
if (false == Result.valid || Result.elements.empty())
return along;
std::vector<glm::dvec3> all;
for (auto const &entry : g_records)
add_clicks(all, entry.clicks);
if (all.empty())
return along;
std::vector<glm::dvec2> 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<glm::dvec3> 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)

View File

@@ -51,6 +51,8 @@ struct record
glm::dvec2 low{0.0};
glm::dvec2 high{0.0};
std::vector<glm::dvec2> samples;
// the points clicked when the line was laid, its curves fitted to them, so they can be fitted again
std::vector<glm::dvec3> clicks;
};
struct hit
@@ -84,8 +86,12 @@ std::optional<designed_vertex> vertex_of(std::vector<std::pair<TTrack *, bool>>
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<TTrack *> 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<glm::dvec3> 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<glm::dvec3> clicks_along(alignment::result const &Result, double const Reach);
bool follows(alignment::result const &Result, std::vector<TTrack *> const &Tracks);
double deviation(alignment::result const &Result, std::vector<TTrack *> const &Tracks, TTrack *&Worst);
std::optional<double> chainage_on(alignment::result const &Result, glm::dvec3 const &Point);

View File

@@ -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"

View File

@@ -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<glm::dvec2> 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<double>(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;