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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user