17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-31 21:39:03 +01:00

fix(editor): a curve laid from a free end on a curve, or to one, goes on from its curvature: traced (Tab) from the end of the diverging track of a switch it begins right at P4 at the radius of the switch, not on a straight nor with a transition from a curvature of 0. The design of the line takes the curve of the adjoining track at each end (start_curved/end_curved with the radius, the side and the cant, kept with it in the scenery): the curve of the first vertex turning its way, or of the last one, begins right at the end, its transition going from that curvature and cant to those of its first arc, as between the arcs of a compound curve, none where it goes on at its radius; the radius of its arcs is the one which begins it there, solved for the tangent, the curves next to it making room on the straight. A curve traced next to such an end is fitted by its transitions alone, with none between the curves where the points keep to that as well, or laid from a straight there where it doesn't go on from it or that keeps to the points much closer: eased out of the curve of a plain track by the transition of the norms, the line beginning at its end, or right from P4, as a switch goes on with a straight. A bend of straights turning the way of the curve gets its curve going on from it with the transitions and the cant of the norms; the other way, or a straight, as before: eased out of the curve of a plain track, the jump of the curvature kept at a switch, as at its start, and not warned of. The cant of the adjoining track goes on into the curve with no ramp at that end; the norms ask for the transition between the curves only where the curvature differs by more than 5 % or the cant by 5 mm. The readout by the cursor, the status after laying and the plan tell the curve going on from the track, the plan with a box to go on or not. Dragged back to P4 from a point the curve goes on from its radius too. Selftests laywalk, laybends, curvejoint, endcurves; unit tests of a curve going on from a switch and into a free end, its cant, traced, through a transition into a larger radius, and of a bend of straights after such an end

This commit is contained in:
maj00r
2026-10-25 12:28:55 +01:00
parent 91900a0ead
commit 97b6a84d1b
13 changed files with 1210 additions and 38 deletions

View File

@@ -1513,7 +1513,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri
// the track through the points clicked, each the way its mode says; End: the free end it finishes at, the last point then
std::vector<segment_data> lay_pieces(std::vector<glm::dvec3> const &Points, std::vector<lay_mode> const &Modes, editor_track::snap_target const &End, double &Length, std::string &Error,
lay_line_plan *Plan = nullptr) const;
bool lay_line(std::vector<glm::dvec3> const &Points, std::vector<lay_mode> const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error) const;
// Adjoining: the curve next to a free end on a curve goes on from its curvature, where it turns its way; else from a straight there
bool lay_line(std::vector<glm::dvec3> const &Points, std::vector<lay_mode> const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error, bool const Adjoining = true) const;
// the curves of a stretch traced, the ones fitted lately kept for the same points
// none while the stretch the cursor moves along is still being fitted; Meanwhile: the fit done last of the same points but the one
// at the cursor until then

View File

@@ -46,6 +46,29 @@ http:
using namespace track_mode;
namespace
{
// the curve of the track of a free end the line laid goes on from at its start, or into at its end: its radius, the side it turns to
// as the line runs and its cant leaning into the turn; nothing on a straight
void adjoining_curve(editor_track::snap_target const &End, bool const Atend, double const Gauge, alignment::design &Design)
{
if (End.track == nullptr || (End.track->eType != tt_Normal && End.track->eType != tt_Switch))
return;
auto const bend{editor_track::bend_at(*End.track, End.position)};
if (false == bend.has_value() || std::abs(bend->curvature) < 1e-4)
return;
// the curvature as the track goes on beyond its end: the way the line runs at its start, against it at its end
auto const curvature{Atend ? -bend->curvature : bend->curvature};
auto const cant{Gauge * std::sin(glm::radians(bend->roll)) * (bend->curvature > 0.0 ? 1.0 : -1.0)};
(Atend ? Design.end_radius : Design.start_radius) = 1.0 / std::abs(curvature);
(Atend ? Design.end_turn : Design.start_turn) = curvature > 0.0 ? 1 : -1;
(Atend ? Design.end_cant : Design.start_cant) = std::max(0.0, cant);
(Atend ? Design.end_curved : Design.start_curved) = true;
}
} // namespace
void editor_mode::update_build_tools()
{
glm::dvec3 const ground{cursor_ground()};
@@ -437,7 +460,8 @@ void editor_mode::point_drag_finish()
std::optional<alignment::lay_result> editor_mode::eased_layout(TTrack const *From, glm::dvec3 const &Point, glm::dvec2 const &Direction, glm::dvec3 const &Target, double const Radius) const
{
m_eased_last.reset();
if (From == nullptr || From->eType != tt_Normal)
// from the diverging track of a switch a curve goes on from its curvature, a straight goes on right from its end, as from the switch
if (From == nullptr || (From->eType != tt_Normal && (From->eType != tt_Switch || m_track_layout == track_layout::straight)))
return std::nullopt;
auto const end{editor_track::bend_at(*From, Point)};
if (false == end.has_value())
@@ -971,7 +995,8 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
return std::max(alignment::minimum_pieces(Shape, Design), static_cast<std::size_t>(std::ceil(Shape.length / std::max(5.0, tool.piece_length))));
};
std::vector<segment_data> pieces;
// the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them
// the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them. Not
// at a switch: its diverging track ends its curve with a jump as it begins it, a straight going on right from there
auto const grade{(Points.back().y - Points.front().y) / std::max(1.0, plan_distance(Points.front(), Points.back()))};
auto const eased = [&](editor_track::snap_target const &Joint, double const Chainage, double const Sign) {
if (Joint.track == nullptr || Joint.track->eType != tt_Normal)
@@ -987,15 +1012,22 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
if ((false == in.pieces.empty() || false == out.pieces.empty()) && editor_track::eases_room(in, out, Points.front(), design.start_direction, Points.back(), design.end_direction))
{
auto easeddesign{design};
// an end moved out of the curve of the adjoining track is on a straight
if (false == in.pieces.empty())
{
easeddesign.start = in.end;
easeddesign.start_direction = in.direction;
easeddesign.start_curved = false;
easeddesign.start_radius = 0.0;
easeddesign.start_turn = 0;
}
if (false == out.pieces.empty())
{
easeddesign.end = out.end;
easeddesign.end_direction = -out.direction;
easeddesign.end_curved = false;
easeddesign.end_radius = 0.0;
easeddesign.end_turn = 0;
}
// the bends stay where they are, measured from the ends moved
if (easeddesign.vertices.size() >= 2 && shape.vertices.size() == easeddesign.vertices.size())
@@ -1019,6 +1051,12 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
{
Length = shape.length;
pieces = alignment::pieces(shape, design, count(shape, design));
// a line lay_line begins at the end of the transition out of the curve of the free end keeps the transition before it
auto const after = [&](editor_track::easing const &Easing, glm::dvec3 const &Joint, glm::dvec3 const &Own) { return false == Easing.pieces.empty() && plan_distance(Joint, Own) > 0.01; };
auto const &before{after(in, Points.front(), design.start) ? in : editor_track::easing{}};
auto const &beyond{after(out, Points.back(), design.end) ? out : editor_track::easing{}};
Length = editor_track::eased_length(before, Length, beyond);
pieces = editor_track::eased_pieces(before, pieces, beyond);
}
// the heights: fitted to the ground, or from point to point through the points clicked
TTrack const *style{tool.start.track != nullptr ? tool.start.track : End.track != nullptr ? End.track : tool.copy_selected ? selected_track() : nullptr};
@@ -1033,7 +1071,7 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
return pieces;
}
bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<lay_mode> const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error) const
bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<lay_mode> const &Modes, editor_track::snap_target const &End, lay_line_plan &Plan, std::string &Error, bool const Adjoining) const
{
auto const &tool{m_lay};
Plan = {};
@@ -1043,6 +1081,22 @@ bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<la
design.speed = design_speed(tool.start.track, End.track);
design.start = Points.front();
auto const joined{End.track != nullptr && glm::length(plan_of(End.direction)) > 1e-6};
// from a free end on a curve, or to one, the curve laid next to it turning its way goes on from its curvature: it begins right at the
// end, not on a straight, its transition going from that curvature. Where no such curve fits, the line is laid from a straight there,
// which lay_pieces eases out of the curve
if (Adjoining)
{
if (tool.start.track != nullptr && glm::length(plan_of(tool.start.direction)) > 1e-6)
adjoining_curve(tool.start, false, design.norms.gauge, design);
if (joined)
adjoining_curve(End, true, design.norms.gauge, design);
}
auto const curved{design.start_curved || design.end_curved};
auto const from_straight = [&]() { return curved && lay_line(Points, Modes, End, Plan, Error, false); };
// the end of the transition out of the curve of the free end, the start of the line where a curve traced next to it is laid from a
// straight there; the same at the end
std::optional<std::pair<glm::dvec3, glm::dvec2>> easedstart;
std::optional<std::pair<glm::dvec3, glm::dvec2>> easedend;
// the points between the start and the end: a free end reached is the last point
auto const inner{Points.size() - (joined ? 1 : 0)};
auto const mode_of = [&](std::size_t const Index) { return Index < Modes.size() ? Modes[Index] : lay_mode::straights; };
@@ -1117,6 +1171,8 @@ bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<la
traced.push_back(plan_of(Points[k]));
}
i = last;
// the curve right from the free end the line starts at
auto const fromend{bends.empty() && Plan.traces.empty() && along.has_value()};
// the straight the curve comes from: the one of the free end or of the curve before, else the one through the last two
// points clicked before the curve, the last of them then no bend
auto entry{anchor};
@@ -1179,17 +1235,88 @@ bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<la
// the curve may begin before the point the straight was clicked at, and end past the last point where the line goes on
base.start_reserve = behind;
base.end_reserve = last == inner ? 0.0 : 1e4;
auto const fitted{traced_stretch(base, traced, tool.radii, tool.straights_between)};
// it goes on from the curve of the free end it starts at, or into the one it finishes at
base.start_curved = design.start_curved && fromend;
base.end_curved = design.end_curved && reaching;
auto fitted{traced_stretch(base, traced, tool.radii, tool.straights_between)};
if (false == fitted.has_value())
{
Error = STR_C("Fitting the curve to the points...");
return false;
}
// next to a free end on a curve the stretch goes on from that curve, or begins from a straight there: eased out of the curve
// by the transition lay_pieces lays, or right from the end of the diverging track of a switch, as the switch goes on. The
// straight one where the curve next to the free end doesn't go on from it, or where that one doesn't keep to the points and
// this one keeps much closer
if (base.start_curved || base.end_curved)
{
auto plain{base};
plain.start_curved = plain.end_curved = false;
std::vector<glm::dvec2> kept;
auto const easing = [&](editor_track::snap_target const &Joint, double const Grade) {
return Joint.track != nullptr && Joint.track->eType == tt_Normal ? editor_track::ease_out_of(*Joint.track, Joint.position, Grade, design.speed, design.norms) : editor_track::easing{};
};
auto const in{base.start_curved ? easing(tool.start, 0.0) : editor_track::easing{}};
auto const out{base.end_curved ? easing(End, 0.0) : editor_track::easing{}};
if (false == in.pieces.empty())
{
plain.start = in.end;
plain.start_direction = in.direction;
}
if (false == out.pieces.empty())
{
plain.end = out.end;
plain.end_direction = -out.direction;
}
// the points along the transitions out of the curves left out, the line from the straights after them
for (auto const &point : traced)
if ((in.pieces.empty() || glm::distance(point, plan_of(base.start)) > in.length) && (out.pieces.empty() || glm::distance(point, plan_of(base.end)) > out.length))
kept.push_back(point);
auto const other{kept.size() >= 2 ? traced_stretch(plain, kept, tool.radii, tool.straights_between) : std::optional<alignment::trace_stretch>{alignment::trace_stretch{}}};
if (false == other.has_value())
{
Error = STR_C("Fitting the curve to the points...");
return false;
}
auto goes_on{false};
if (fitted->valid)
{
auto laid{base};
laid.vertices = fitted->vertices;
for (auto const &curve : alignment::compute(laid).curves)
goes_on = goes_on || curve.curvature_in > 0.0 || curve.curvature_out > 0.0;
}
// a curve going on which keeps to the points within 8 cm on average and 30 cm at most, as the fits take it for one keeping to
// them, stays: the straight one fits the points clicked a little off closer with the radii it has more
auto const loose{fitted->valid && (fitted->rms > 0.08 || fitted->worst > 0.3)};
if (other->valid && (false == fitted->valid || false == goes_on || (loose && other->rms < 0.75 * fitted->rms)))
{
fitted = other;
// the line laid from the end of the transition out of the curve, which lay_pieces lays before it
if (false == in.pieces.empty())
easedstart = std::make_pair(in.end, in.direction);
if (false == out.pieces.empty())
easedend = std::make_pair(out.end, -out.direction);
// from a straight there; at a switch with the jump of the curvature it has, which isn't warned of
auto const straight_at = [&](editor_track::snap_target const &Joint, bool &Curved, double &Radius, int &Turn) {
Curved = false;
if (Joint.track != nullptr && Joint.track->eType == tt_Switch)
{
Radius = 0.0;
Turn = 0;
}
};
if (base.start_curved)
straight_at(tool.start, design.start_curved, design.start_radius, design.start_turn);
if (base.end_curved)
straight_at(End, design.end_curved, design.end_radius, design.end_turn);
}
}
auto const &fit{*fitted};
if (false == fit.valid)
{
Error = STR_C("No curve follows the points clicked along it");
return false;
return from_straight();
}
for (auto const &vertex : fit.vertices)
bends.push_back({lifted(vertex.position, entry.y), vertex, false});
@@ -1259,6 +1386,13 @@ bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<la
return false;
design.start_direction = glm::normalize(first);
}
if (easedstart.has_value())
std::tie(design.start, design.start_direction) = *easedstart;
if (easedend.has_value())
{
std::tie(design.end, design.end_direction) = *easedend;
end = design.end;
}
for (auto const &bend : bends)
{
auto vertex{bend.corner ? alignment::vertex{} : bend.vertex};
@@ -1275,7 +1409,7 @@ bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<la
if (false == Plan.shape.valid)
{
Error = Plan.shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : Plan.shape.errors.front();
return false;
return from_straight();
}
// the grade breaks at the bends which stayed bends; along a curve, traced or made of bends, it goes on even from end to end
for (std::size_t k = 0; k < design.vertices.size() && k < Plan.corners.size(); ++k)
@@ -1295,8 +1429,12 @@ bool editor_mode::lay_line(std::vector<glm::dvec3> const &Points, std::vector<la
std::optional<alignment::trace_stretch> editor_mode::traced_stretch(alignment::design const &Base, std::vector<glm::dvec2> const &Points, int const Radii, bool const Straights, bool const Meanwhile) const
{
// the stretches traced before the one the cursor moves along are the same from frame to frame: their fits are kept
// the curves of the adjoining tracks it goes on from or into, signed by their turn, and their cants
auto const curvature = [](bool const Curved, double const Radius, int const Turn) { return Curved && Radius > 0.0 ? Turn / Radius : 0.0; };
std::vector<double> key{Base.start.x, Base.start.z, Base.start_direction.x, Base.start_direction.y, Base.end.x, Base.end.z, Base.end_direction.x, Base.end_direction.y, Base.speed,
static_cast<double>(Base.shape), static_cast<double>(Radii), Straights ? 1.0 : 0.0, Base.start_reserve, Base.end_reserve};
static_cast<double>(Base.shape), static_cast<double>(Radii), Straights ? 1.0 : 0.0, Base.start_reserve, Base.end_reserve,
curvature(Base.start_curved, Base.start_radius, Base.start_turn), Base.start_curved ? Base.start_cant : 0.0, curvature(Base.end_curved, Base.end_radius, Base.end_turn),
Base.end_curved ? Base.end_cant : 0.0};
for (auto const &point : Points)
{
key.push_back(point.x);
@@ -1330,7 +1468,7 @@ std::optional<alignment::trace_stretch> editor_mode::traced_stretch(alignment::d
}
m_stretch_fitter.ask({key, Base, Points, Radii, Straights});
std::vector<double> last;
auto const header{std::size_t{14}};
auto const header{std::size_t{18}};
if (Meanwhile && m_stretch_fitter.latest(last, fit) && last.size() >= header + 2 && key.size() >= header + 2 && std::equal(key.begin(), key.begin() + 4, last.begin()) &&
std::equal(key.begin() + 8, key.begin() + header, last.begin() + 8))
{
@@ -1447,6 +1585,11 @@ void editor_mode::lay_finish(editor_track::snap_target const &End)
tool.status += "\n" + error;
if (eased.has_value() && eased->compound)
tool.status += "\n" + format(STR_C("Compound curve: R %.0f m goes on into R %.0f m through a transition curve of %.0f m"), eased->from_radius, eased->radius, eased->entry);
// the curves next to the free ends going on from the curves of their tracks
if (plan.shape.valid)
for (auto const &curve : plan.shape.curves)
if (auto const on{curve_goes_on(curve)}; false == on.empty())
tool.status += "\n" + format(STR_C("Curve %d %s"), curve.vertex + 1, on.c_str());
if (eased.has_value() && false == eased->note.empty())
tool.status += "\n" + eased->note;
{

View File

@@ -277,6 +277,11 @@ void editor_mode::route_take_stored(int const Index)
design.norms = stored.design.norms;
design.transition_pieces = stored.design.transition_pieces;
design.arc_piece_angle = stored.design.arc_piece_angle;
// a curve laid going on from the curve of the track adjoining keeps going on from it, its radius the one which begins it there
design.start_curved = stored.design.start_curved;
design.end_curved = stored.design.end_curved;
design.start_cant = stored.design.start_cant;
design.end_cant = stored.design.end_cant;
route.designed = static_cast<int>(design.vertices.size());
route.origin = stored.origin;
route.vertex = -1;
@@ -2105,6 +2110,19 @@ bool editor_mode::render_route_vertex(int const Index)
return edited;
};
ImU32 const transition{IM_COL32(255, 140, 30, 255)};
// the first curve, or the last one, may go on from the curve of the track adjoining at that end: it begins right there, its radius
// the one which begins it there, its entry, or exit, the transition between the two curves
auto const count{static_cast<int>(design.vertices.size())};
for (auto const atend : {false, true})
{
auto const radius{atend ? design.end_radius : design.start_radius};
if ((atend ? Index + 1 != count : Index != 0) || radius <= 0.0 || radius >= 10000.0 || (atend && count == 1 && design.start_curved))
continue;
auto &curved{atend ? design.end_curved : design.start_curved};
changed |= ImGui::Checkbox((format(atend ? STR_C("Goes into the curve of the track at the end (R %.0f m)") : STR_C("Goes on from the curve of the track at the start (R %.0f m)"), radius) + (atend ? "##curvedend" : "##curvedstart")).c_str(), &curved);
item_tooltip("The curve begins right at the end of the fragment, at the curvature of the track adjoining there, where it turns the same way; its radius is the one "
"which begins it there, the transition at that end goes between the two curves");
}
changed |= field("##radius", vertex.radius, 5.0f, vertex.compound ? STR_C("R1 %.0f m") : STR_C("R %.0f m"), overlay_color::grip, "Radius of the arc: drag sideways, or a double click to type it", part::arc);
ImGui::SameLine();
changed |= field("##cant", vertex.cant, 1.0f, STR_C("cant %.0f mm"), IM_COL32(240, 170, 60, 255),

View File

@@ -129,6 +129,11 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c
// layclicks <layout 0..3> <radius> <x> <z>... clicks of the lay tool at the points, a free end within 3 m joined
// layspeed <km/h> <own 0|1> the speed set for the laying, and whether it goes also on from a track with its own
// laymode <straights|curve>, layradius <R from> <R to>, laytrace <radii 0..3> <straights between 0|1> as the panel of the laying sets them
// endcurves logs every free end of the rails: where, the way out and the curvature its track goes on with
// curvejoint <x> <z> logs the paths meeting at the end nearest the point, the curvature of each and its jump from the ones before to the ones after
// laywalk <from|into> <x> <z> <spacing> <noise> <k0> <k1> <L>... a line of known curves on from the free end nearest the point,
// the points along it clicked in the mode of a curve, from the end or towards it (see the command)
// laybends <x> <z> <distance> <degrees> ... <distance> the bends of straights clicked from the free end nearest the point
// layp <x> <z>... the points clicked in the mode set, on the ground, a free end within 3 m reached
// layreport <label> logs the line the points clicked make, as Enter would lay it: its bends and curves, warnings, traces
// layenter, layback, laycancel as Enter, Backspace and Esc while laying

View File

@@ -46,6 +46,7 @@ http://mozilla.org/MPL/2.0/.
#include <fstream>
#include <map>
#include <sstream>
#include <array>
#include "application/editormodeselftest.h"
editor_mode::selftest_result editor_mode::selftest_join(std::string const &command, std::istringstream &words)
@@ -129,6 +130,87 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b)));
return selftest_result::handled;
}
if (command == "endcurves")
{
// every free end of the rails: where, the way out of it and the curvature of its track as it would go on beyond it
for (auto const &end : editor_track::free_ends(nullptr, 1, glm::dvec3{0.0}, 1e7, {}))
{
auto const bend{editor_track::bend_at(*end.track, end.position)};
auto const curvature{bend.has_value() ? bend->curvature : 0.0};
WriteLog(format("SELFTEST freeend %s[%d].%s: %.3f %.3f %.3f, out %.6f %.6f, k %.6f (R %.1f), roll %.3f", end.track->name().c_str(), end.point.path, end.point.kind == editor_track::point_kind::end ? "end" : "start", end.position.x, end.position.y,
end.position.z, bend.has_value() ? bend->direction.x : 0.0, bend.has_value() ? bend->direction.y : 0.0, curvature, std::abs(curvature) > 1e-9 ? 1.0 / std::abs(curvature) : 0.0, bend.has_value() ? bend->roll : 0.0));
}
return selftest_result::handled;
}
if (command == "curvejoint")
{
// the paths meeting at the point: the curvature of each, signed the way the first one runs into the point, and the jumps of the
// curvature and of the direction from the ones before the point to the ones after it
glm::dvec3 point{0.0};
words >> point.x >> point.z;
// the end of a path nearest the point, within 3 m
auto nearest{3.0};
auto const given{point};
for (auto const *track : simulation::Paths.sequence())
if (track != nullptr && false == track->m_editorremoved)
for (auto const &path : track->m_paths)
for (auto const index : {segment_data::point::start, segment_data::point::end})
if (auto const distance{geometry::plan_distance(path.points[index], given)}; distance < nearest)
{
nearest = distance;
point = path.points[index];
}
struct meeting
{
TTrack const *track;
int path;
bool atend;
double curvature; // as the path goes away from the point
double roll;
glm::dvec2 out;
};
std::vector<meeting> meetings;
for (auto const *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved)
continue;
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
for (auto const atend : {false, true})
{
auto const &path{track->m_paths[i]};
if (geometry::plan_distance(path.points[atend ? segment_data::point::end : segment_data::point::start], point) > 0.05)
continue;
geometry::bezier const curve{path};
auto const first{curve.first(atend ? 1.0 : 0.0)};
auto const second{curve.second(atend ? 1.0 : 0.0)};
auto const speed{std::hypot(first.x, first.z)};
if (speed < 1e-9)
continue;
auto const curvature{(first.x * second.z - first.z * second.x) / (speed * speed * speed)};
glm::dvec2 const along{first.x / speed, first.z / speed};
meetings.push_back({track, i, atend, atend ? -curvature : curvature, static_cast<double>(atend ? path.rolls[1] : path.rolls[0]), atend ? -along : along});
}
}
if (meetings.empty())
{
WriteLog(format("SELFTEST curvejoint %.1f %.1f: no path ends there", point.x, point.z));
return selftest_result::handled;
}
auto const into{-meetings.front().out};
std::vector<std::pair<double, glm::dvec2>> before;
std::vector<std::pair<double, glm::dvec2>> after;
for (auto const &each : meetings)
{
auto const leaving{glm::dot(each.out, into) > 0.0};
auto const curvature{leaving ? each.curvature : -each.curvature};
(leaving ? after : before).emplace_back(curvature, leaving ? each.out : -each.out);
WriteLog(format("SELFTEST curvejoint %s[%d].%s: %s, k %.6f (R %.1f), roll %.3f", each.track->name().c_str(), each.path, each.atend ? "end" : "start", leaving ? "after" : "before", curvature, std::abs(curvature) > 1e-9 ? 1.0 / std::abs(curvature) : 0.0, each.roll));
}
for (auto const &[k0, d0] : before)
for (auto const &[k1, d1] : after)
WriteLog(format("SELFTEST curvejoint jump: k %.6f -> %.6f, by %.6f 1/m (%.1f%%), kink %.4f deg", k0, k1, k1 - k0, std::abs(k0) > 1e-9 ? 100.0 * std::abs(k1 - k0) / std::abs(k0) : 0.0, glm::degrees(std::abs(geometry::signed_angle(d0, d1)))));
return selftest_result::handled;
}
if (command == "fittime")
{
// the grade line of a straight laid over rolling ground fitted, as at a click, and as while the track follows the
@@ -267,6 +349,123 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
lay_report(label);
return selftest_result::handled;
}
// a line of known curves going on from the free end nearest the point, within 60 m: segments of the curvature going evenly from the
// first to the second (1/m, positive turning the way the track of the end turns, to the left from a straight one) over the length. from: the free end
// clicked, then in the mode of a curve the points along the line every Spacing metres and two on the straight after it, 30 and 60 m
// on, left for Enter; into: the same line laid towards the free end, the point 60 m on clicked first, the one 30 m on and the ones
// along the line in the mode of a curve, the free end last, which lays it. The points shifted across by up to Noise
if (command == "laywalk")
{
std::string way;
glm::dvec3 around{0.0};
double spacing{10.0};
double noise{0.0};
words >> way >> around.x >> around.z >> spacing >> noise;
std::vector<std::array<double, 3>> segments;
for (std::array<double, 3> segment; words >> segment[0] >> segment[1] >> segment[2];)
segments.push_back(segment);
auto const end{editor_track::find_free_end(nullptr, 1, around, 60.0, {})};
auto const bend{end.track != nullptr ? editor_track::bend_at(*end.track, end.position) : std::nullopt};
if (false == bend.has_value() || segments.empty() || spacing <= 0.0)
{
WriteLog("SELFTEST laywalk: no free end near the point, or no line");
return selftest_result::handled;
}
glm::dvec2 position{end.position.x, end.position.z};
auto heading{std::atan2(bend->direction.y, bend->direction.x)};
auto const sense{bend->curvature < 0.0 ? -1.0 : 1.0};
double chainage{0.0};
std::vector<glm::dvec2> along;
std::vector<glm::dvec2> across;
auto next{spacing};
for (auto const &[from, to, length] : segments)
{
auto const steps{std::max(1, static_cast<int>(std::ceil(length / 0.05)))};
auto const step{length / steps};
auto const start{heading};
auto const angle = [&, from = from * sense, to = to * sense, length = length](double const S) { return start + from * S + (to - from) * S * S / (2.0 * length); };
for (int i = 0; i < steps; ++i)
{
auto const middle{angle((i + 0.5) * step)};
position += glm::dvec2{std::cos(middle), std::sin(middle)} * step;
chainage += step;
if (chainage >= next - 1e-6)
{
auto const now{angle((i + 1) * step)};
along.push_back(position);
across.push_back({-std::sin(now), std::cos(now)});
next += spacing;
}
}
heading = angle(length);
}
glm::dvec2 const out{std::cos(heading), std::sin(heading)};
glm::dvec2 const side{-out.y, out.x};
auto const shifted = [&](glm::dvec2 const &Point, glm::dvec2 const &Across, int const Count) {
auto const shift{noise * (Count % 3 == 0 ? 1.0 : Count % 3 == 1 ? -0.6 : 0.2)};
return glm::dvec3{Point.x + Across.x * shift, 0.0, Point.y + Across.y * shift};
};
WriteLog(format("SELFTEST laywalk %s from %s[%d]: %.3f %.3f heading %.4f, k %.6f, line %.1f m to %.3f %.3f heading %.4f, %zu points along it", way.c_str(), end.track->name().c_str(), end.point.path, end.position.x, end.position.z, std::atan2(bend->direction.y, bend->direction.x),
bend->curvature, chainage, position.x, position.y, heading, along.size()));
m_lay.active = true;
lay_cancel();
if (m_lay.mode != lay_mode::straights)
lay_switch_mode();
if (way == "into")
{
lay_point(shifted(position + out * 60.0, side, 0));
lay_switch_mode();
lay_point(shifted(position + out * 30.0, side, 1));
for (auto i{along.size()}; i-- > 0;)
if (glm::distance(along[i], glm::dvec2{end.position.x, end.position.z}) > 3.5)
lay_point(shifted(along[i], across[i], static_cast<int>(i)));
lay_point(end.position);
WriteLog(format("SELFTEST laywalk into: %zu points, %s", m_lay.points.size(), m_lay.status.c_str()));
}
else
{
lay_point(end.position);
lay_switch_mode();
for (std::size_t i = 0; i < along.size(); ++i)
lay_point(shifted(along[i], across[i], static_cast<int>(i)));
lay_point(shifted(position + out * 30.0, side, 1));
lay_point(shifted(position + out * 60.0, side, 2));
WriteLog(format("SELFTEST laywalk from: %zu points, start %s", m_lay.points.size(), m_lay.start.track != nullptr ? m_lay.start.track->name().c_str() : "-"));
}
lay_switch_mode();
return selftest_result::handled;
}
// the bends of straights from the free end nearest the point, within 60 m: the free end clicked, then each point the distance on
// along the way it goes, which then turns by the angle (degrees, positive the way the track of the end turns, to the left from a
// straight one); the last one the end, left for Enter
if (command == "laybends")
{
glm::dvec3 around{0.0};
words >> around.x >> around.z;
auto const end{editor_track::find_free_end(nullptr, 1, around, 60.0, {})};
auto const bend{end.track != nullptr ? editor_track::bend_at(*end.track, end.position) : std::nullopt};
if (false == bend.has_value())
{
WriteLog("SELFTEST laybends: no free end near the point");
return selftest_result::handled;
}
m_lay.active = true;
lay_cancel();
if (m_lay.mode != lay_mode::straights)
lay_switch_mode();
lay_point(end.position);
glm::dvec2 position{end.position.x, end.position.z};
auto heading{std::atan2(bend->direction.y, bend->direction.x)};
for (double distance{0.0}; words >> distance;)
{
position += glm::dvec2{std::cos(heading), std::sin(heading)} * distance;
lay_point({position.x, 0.0, position.y});
if (double turn{0.0}; words >> turn)
heading += glm::radians(turn) * (bend->curvature < 0.0 ? -1.0 : 1.0);
}
WriteLog(format("SELFTEST laybends from %s[%d] %.3f %.3f: %zu points", end.track->name().c_str(), end.point.path, end.position.x, end.position.z, m_lay.points.size()));
return selftest_result::handled;
}
if (command == "layenter" || command == "laycancel" || command == "layback")
{
m_typed.clear();

View File

@@ -233,6 +233,13 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm
text += format(" [R %.3f L %.3f share %.4f]", arc.radius, arc.transition, arc.share);
WriteLog(text);
}
// the curves the design lays, going on from the curves of the tracks adjoining where they do, and its warnings
WriteLog(format("SELFTEST route ends: start R %.1f turn %d curved %d, end R %.1f turn %d curved %d", m_route.design.start_radius, m_route.design.start_turn, m_route.design.start_curved ? 1 : 0, m_route.design.end_radius, m_route.design.end_turn, m_route.design.end_curved ? 1 : 0));
for (auto const &curve : m_route.result.curves)
WriteLog(format("SELFTEST route curve %d: R %.1f, L %.1f/%.1f, from R %.1f, into R %.1f", curve.vertex, curve.radius, curve.transition_in, curve.transition_out, curve.curvature_in > 0.0 ? 1.0 / curve.curvature_in : 0.0,
curve.curvature_out > 0.0 ? 1.0 / curve.curvature_out : 0.0));
for (auto const &warning : m_route.result.warnings)
WriteLog("SELFTEST route warning: " + warning);
return selftest_result::handled;
}
if (command == "routevertex" || command == "routearc" || command == "routeshare")

View File

@@ -1909,6 +1909,11 @@ std::string editor_mode::track_readout() const
text += " " + curve_label(curve, laying_norms());
if (false == alignment::meets_norms(curve, plan.design))
text += "\n" + STR("this curve doesn't meet the norms, it's laid all the same");
// the curves next to the free ends going on from the curves of their tracks: the first and the last one
if (auto const on{curve_goes_on(plan.shape.curves.front())}; false == on.empty())
text += "\n" + on;
if (auto const on{curve_goes_on(curve)}; plan.shape.curves.size() > 1 && false == on.empty())
text += "\n" + on;
}
if (lay.mode == lay_mode::curve && false == plan.traces.empty() && false == plan.traces.back().offsets.empty())
{

View File

@@ -102,6 +102,22 @@ inline std::string curve_label(alignment::curve_report const &Curve, alignment::
return text + speed_of(curve_sharpest(Curve), Limits);
}
// how a curve laid goes on from the curve of the track it starts at, or into the one of the track it ends at; empty from a straight
inline std::string curve_goes_on(alignment::curve_report const &Curve)
{
std::vector<std::string> lines;
if (Curve.curvature_in > 0.0)
lines.push_back(Curve.transition_in < 1.0 ? format(STR_C("goes on from R %.0f m of the track it starts at"), 1.0 / Curve.curvature_in) :
format(STR_C("goes on from R %.0f m of the track it starts at, a transition of %.0f m between them"), 1.0 / Curve.curvature_in, Curve.transition_in));
if (Curve.curvature_out > 0.0)
lines.push_back(Curve.transition_out < 1.0 ? format(STR_C("goes into R %.0f m of the track it ends at"), 1.0 / Curve.curvature_out) :
format(STR_C("goes into R %.0f m of the track it ends at, a transition of %.0f m between them"), 1.0 / Curve.curvature_out, Curve.transition_out));
std::string text;
for (auto const &line : lines)
text += (text.empty() ? "" : "\n") + line;
return text;
}
// the transition curves and the cant the norms put in a layout, a compound curve where it goes on from an arc turning the same way
inline std::string eased_of(std::optional<alignment::lay_result> const &Eased)
{

View File

@@ -112,8 +112,18 @@ struct compound_curve
double shortened{1.0};
};
// the curvature and the cant a curve goes on from at its start and into at its end, those of the curve of the adjoining track turning
// its way; 0 on a straight
struct curve_ends
{
double from{0.0};
double to{0.0};
double from_cant{0.0};
double to_cant{0.0};
};
// Cants: of the arcs, the cants of the segments stay 0 without them
compound_curve build_compound(double const Deflection, std::vector<compound_arc> const &Arcs, double const Lengthin, double const Lengthout, std::vector<double> const &Cants = {})
compound_curve build_compound(double const Deflection, std::vector<compound_arc> const &Arcs, double const Lengthin, double const Lengthout, std::vector<double> const &Cants = {}, curve_ends const &Ends = {})
{
compound_curve result;
if (Arcs.empty())
@@ -122,7 +132,7 @@ compound_curve build_compound(double const Deflection, std::vector<compound_arc>
for (auto const &arc : Arcs)
curvatures.push_back(1.0 / std::max(1.0, arc.radius));
auto const cant = [&](std::size_t const Arc) { return Arc < Cants.size() ? Cants[Arc] : 0.0; };
auto transitions{curvatures.front() * Lengthin + curvatures.back() * Lengthout};
auto transitions{(Ends.from + curvatures.front()) * Lengthin + (curvatures.back() + Ends.to) * Lengthout};
for (std::size_t i = 1; i < Arcs.size(); ++i)
transitions += (curvatures[i - 1] + curvatures[i]) * std::max(0.0, Arcs[i].transition);
transitions *= 0.5;
@@ -133,7 +143,7 @@ compound_curve build_compound(double const Deflection, std::vector<compound_arc>
double total{0.0};
for (auto const &arc : Arcs)
total += std::max(0.0, arc.share);
std::vector<std::array<double, 5>> segments{{0.0, curvatures.front(), Lengthin * f, 0.0, cant(0)}};
std::vector<std::array<double, 5>> segments{{Ends.from, curvatures.front(), Lengthin * f, Ends.from_cant, cant(0)}};
for (std::size_t i = 0; i < Arcs.size(); ++i)
{
if (i > 0)
@@ -141,7 +151,7 @@ compound_curve build_compound(double const Deflection, std::vector<compound_arc>
auto const share{total > 0.0 ? std::max(0.0, Arcs[i].share) / total : 1.0 / static_cast<double>(Arcs.size())};
segments.push_back({curvatures[i], curvatures[i], share * arcs / curvatures[i], cant(i), cant(i)});
}
segments.push_back({curvatures.back(), 0.0, Lengthout * f, cant(Arcs.size() - 1), 0.0});
segments.push_back({curvatures.back(), Ends.to, Lengthout * f, cant(Arcs.size() - 1), Ends.to_cant});
double x{0.0};
double y{0.0};
double heading{0.0};
@@ -375,6 +385,20 @@ double spirals_length(transition_shape const Shape, double const Deflection, dou
return Shape == transition_shape::clothoid ? 2.0 * Radius * half : 2.0 * Radius * std::tan(half);
}
namespace
{
// the transition between the curve of an adjoining track and the arc next to it, as between two arcs; none where the curvature differs
// by 5 % at most and the cant by less than 5 mm, the curve going on at the radius of the adjoining one
double between_curves(double const Speed, double const Curvature0, double const Cant0, double const Curvature1, double const Cant1, limits const &Limits)
{
if (std::abs(Curvature1 - Curvature0) <= 0.05 * std::max(std::abs(Curvature0), std::abs(Curvature1)) && std::abs(Cant1 - Cant0) < 5.0)
return 0.0;
return transition_between(Speed, Curvature0, Cant0, Curvature1, Cant1, Limits);
}
} // namespace
bool meets_norms(curve_report const &Curve, design const &Design)
{
auto const &norms{Design.norms};
@@ -385,6 +409,16 @@ bool meets_norms(curve_report const &Curve, design const &Design)
return false;
if (false == Curve.spirals && sharpest < Curve.recommended.radius_min - 1e-6)
return false;
// going on from the curve of an adjoining track, or into it: the transition there between its curvature and cant and those of the
// arc next to it
if ((Curve.curvature_in > 0.0 || Curve.curvature_out > 0.0) && false == Curve.radii.empty() && Curve.cants.size() == Curve.radii.size())
{
auto const required = [&](double const Curvature, double const Cant, double const Radius, double const Arccant) {
return Curvature > 0.0 ? between_curves(Design.speed, Curvature, Cant, 1.0 / Radius, Arccant, norms) : transition_length(Design.speed, Radius, Arccant, norms);
};
return Curve.transition_in + 1e-6 >= required(Curve.curvature_in, Curve.cant_in, Curve.radii.front(), Curve.cants.front()) &&
Curve.transition_out + 1e-6 >= required(Curve.curvature_out, Curve.cant_out, Curve.radii.back(), Curve.cants.back());
}
if (Curve.radii.size() > 1 && Curve.cants.size() == Curve.radii.size())
// a compound curve ramps the cant of its first arc in and the one of its last arc out
return Curve.transition_in + 1e-6 >= transition_length(Design.speed, Curve.radii.front(), Curve.cants.front(), norms) &&
@@ -439,6 +473,26 @@ void make_cants(design const &Design, result &R)
auto const exit{tail.kind == element_kind::transition_out || (tail.kind == element_kind::spiral && tail.curvature_end == 0.0 && tail.curvature_start > 0.0) ? tail.length : 0.0};
auto const cantin{element_cant(head, entry)};
auto const cantout{element_cant(tail, tail.length - exit)};
// a curve going on from the curve of the adjoining track, or into it, has its cant there from the first: no ramp at that end
auto const continuedin{head.kind == element_kind::spiral && head.curvature_start > 0.0};
auto const continuedout{tail.kind == element_kind::spiral && tail.curvature_end > 0.0};
if (continuedin || continuedout)
{
push(start, continuedin ? turn * cantin : 0.0);
for (auto i{first}; i <= last; ++i)
{
auto const &e{R.elements[i]};
for (auto const distance : {0.0, e.length})
push(e.chainage + distance, turn * element_cant(e, distance));
}
push(end, continuedout ? turn * element_cant(tail, tail.length) : 0.0);
if (false == continuedin && cantin >= 0.5)
R.ramps.push_back({static_cast<int>(k), start, start + entry, cantin, false});
if (false == continuedout && cantout >= 0.5)
R.ramps.push_back({static_cast<int>(k), end - exit, end, cantout, false});
reached = end;
continue;
}
if (std::max(cantin, cantout) < 0.5)
{
reached = end;
@@ -621,6 +675,9 @@ result fit_between(design const &Design)
double cant{0.0};
bool compound{false};
bool spirals{false};
// going on from the curve of the adjoining track: its size is fixed by the start or by the end, the curves next to it make room
bool curved{false};
curve_ends ends;
std::vector<compound_arc> arcs;
std::function<void(double, double &, double &)> tangents;
};
@@ -654,6 +711,116 @@ result fit_between(design const &Design)
r.errors.emplace_back(format(STR_C("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex"), k + 1));
return r;
}
// the curve going on from the curve of the adjoining track turning its way, at the start or at the end: a compound curve of the
// arcs it has, one or more, as large as begin it right at the start, or end it at the end
auto const from{k == 0 && Design.start_curved && Design.start_radius > 0.0 && Design.start_turn == side};
auto const to{k + 1 == count && Design.end_curved && Design.end_radius > 0.0 && Design.end_turn == side && false == from};
if ((from || to) && false == reverse && false == vertex.spirals)
{
fit.compound = true;
fit.curved = true;
fit.deflection = deflection;
fit.arcs = {{fit.radius, 0.0, 1.0, -1.0}};
if (vertex.compound && false == vertex.arcs.empty())
{
fit.arcs.front() = {fit.radius, 0.0, vertex.share, vertex.first_cant};
for (auto const &arc : vertex.arcs)
fit.arcs.push_back({std::max(1.0, arc.radius), std::max(0.0, arc.transition), arc.share, arc.cant});
}
if (from)
{
fit.ends.from = 1.0 / Design.start_radius;
fit.ends.from_cant = std::max(0.0, Design.start_cant);
// with no transition between them the cant of the adjoining track goes on into the arc
if (fit.transition_in < 1.0)
fit.arcs.front().cant = fit.ends.from_cant;
}
else
{
fit.ends.to = 1.0 / Design.end_radius;
fit.ends.to_cant = std::max(0.0, Design.end_cant);
if (fit.transition_out < 1.0)
fit.arcs.back().cant = fit.ends.to_cant;
}
auto const normalin{perpendicular(directionin) * static_cast<double>(side)};
auto const tangents_at = [&](double const Scale, double &Tangentin, double &Tangentout) {
auto arcs{fit.arcs};
for (auto &arc : arcs)
arc.radius *= Scale;
auto const shape{build_compound(deflection, arcs, fit.transition_in, fit.transition_out, {}, fit.ends)};
auto const end{directionin * shape.end.x + normalin * shape.end.y};
Tangentin = cross(directionout, end) / cross(directionout, directionin);
Tangentout = glm::dot(end - directionin * Tangentin, directionout);
};
// the straight it begins at, or ends at, all along the tangent: no straight between
auto const wanted{from ? glm::distance(polygon[0], polygon[1]) : glm::distance(polygon[count], polygon[count + 1])};
auto const miss = [&](double const Scale) {
double in;
double out;
tangents_at(Scale, in, out);
return (from ? in : out) - wanted;
};
// the radii grow with the tangent: from those given the way the miss says, doubled or halved until it changes its sign, then
// the root between, by the regula falsi of Illinois
auto low{1.0};
auto lowmiss{miss(low)};
auto high{low};
auto highmiss{lowmiss};
auto const smallest{5.0 / fit.radius};
auto const largest{1e6 / fit.radius};
while ((lowmiss < 0.0) == (highmiss < 0.0) && high < largest && low > smallest)
{
if (lowmiss < 0.0)
{
low = high;
lowmiss = highmiss;
high *= 2.0;
highmiss = miss(high);
}
else
{
high = low;
highmiss = lowmiss;
low *= 0.5;
lowmiss = miss(low);
}
}
if ((lowmiss < 0.0) == (highmiss < 0.0))
{
r.errors.emplace_back(from ? format(STR_C("Vertex %zu: no curve goes on from the curve of the adjoining track (R %.0f m) to the straight after it"), k + 1, Design.start_radius) :
format(STR_C("Vertex %zu: no curve goes on from the straight before it into the curve of the adjoining track (R %.0f m)"), k + 1, Design.end_radius));
return r;
}
auto scale{low};
for (int i = 0, kept = 0; i < 60; ++i)
{
scale = (low * highmiss - high * lowmiss) / (highmiss - lowmiss);
auto const value{miss(scale)};
if (std::abs(value) < 1e-7 || high - low < 1e-12 * high)
break;
if ((value < 0.0) == (lowmiss < 0.0))
{
low = scale;
lowmiss = value;
if (kept == -1)
highmiss *= 0.5;
kept = -1;
}
else
{
high = scale;
highmiss = value;
if (kept == 1)
lowmiss *= 0.5;
kept = 1;
}
}
for (auto &arc : fit.arcs)
arc.radius *= scale;
fit.radius = fit.arcs.front().radius;
tangents_at(1.0, fit.tangent_in, fit.tangent_out);
continue;
}
if (vertex.compound && false == vertex.arcs.empty())
{
fit.compound = true;
@@ -713,13 +880,27 @@ result fit_between(design const &Design)
for (std::size_t leg = 0; leg + 1 < polygon.size(); ++leg)
{
auto const available{glm::distance(polygon[leg], polygon[leg + 1]) + (leg == 0 ? startreserve : 0.0) + (leg == count ? endreserve : 0.0)};
auto const required{(leg > 0 ? fits[leg - 1].tangent_out : 0.0) + (leg < count ? fits[leg].tangent_in : 0.0)};
if (required <= available + 1e-6)
auto const before{leg > 0 ? fits[leg - 1].tangent_out : 0.0};
auto const after{leg < count ? fits[leg].tangent_in : 0.0};
if (before + after <= available + 1e-6)
continue;
auto const scale{available / required * (1.0 - 1e-9)};
if (leg > 0)
// a curve going on from the adjoining track keeps its size, the one on the other side of the straight makes room for it
auto const fixedbefore{leg > 0 && fits[leg - 1].curved};
auto const fixedafter{leg < count && fits[leg].curved};
if (fixedbefore != fixedafter)
{
auto const other{fixedbefore ? after : before};
if (other > 1e-9)
{
auto &kept{scales[fixedbefore ? leg : leg - 1]};
kept = std::min(kept, std::clamp((available - (fixedbefore ? before : after)) / other, 1e-3, 1.0) * (1.0 - 1e-9));
}
continue;
}
auto const scale{available / (before + after) * (1.0 - 1e-9)};
if (leg > 0 && false == fits[leg - 1].curved)
scales[leg - 1] = std::min(scales[leg - 1], scale);
if (leg < count)
if (leg < count && false == fits[leg].curved)
scales[leg] = std::min(scales[leg], scale);
}
for (std::size_t k = 0; k < count; ++k)
@@ -838,7 +1019,7 @@ result fit_between(design const &Design)
if (fit.compound)
{
auto const cants{arc_cants(fit.arcs, fit.cant)};
auto const shape{build_compound(deflection, fit.arcs, fit.transition_in, fit.transition_out, cants)};
auto const shape{build_compound(deflection, fit.arcs, fit.transition_in, fit.transition_out, cants, fit.ends)};
if (shape.shortened < 1.0)
r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shortened to fit the deflection angle"), k + 1));
auto const normalin{perpendicular(directionin) * static_cast<double>(side)};
@@ -893,6 +1074,10 @@ result fit_between(design const &Design)
report.tangent_in = glm::distance(position, curvestart);
report.tangent_out = glm::distance(position, cursor);
report.cants = cants;
report.curvature_in = fit.ends.from;
report.cant_in = fit.ends.from_cant;
report.curvature_out = fit.ends.to;
report.cant_out = fit.ends.to_cant;
report.unbalanced = -std::numeric_limits<double>::max();
for (std::size_t i = 0; i < cants.size(); ++i)
{
@@ -913,10 +1098,22 @@ result fit_between(design const &Design)
// each transition long enough for the change of the curvature and of the cant along it, at the speed the sharpest arc
// allows, as the transitions of the norms are set between the arcs
auto const allowed{std::max(1.0, std::min(Design.speed, speed_limit(sharpest, norms)))};
auto const requiredin{transition_length(allowed, fit.arcs.front().radius, cants.front(), norms)};
auto const requiredout{transition_length(allowed, fit.arcs.back().radius, cants.back(), norms)};
if (report.transition_in + 1e-6 < requiredin || report.transition_out + 1e-6 < requiredout)
r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shorter than the required %.0f m"), k + 1, std::max(requiredin, requiredout)));
// from the curve of an adjoining track, or into it, as between two arcs, at the speed the sharper of them allows
auto const slower = [&](double const Curvature) { return Curvature > 0.0 ? std::max(1.0, std::min(allowed, speed_limit(1.0 / Curvature, norms))) : allowed; };
auto const requiredin{fit.ends.from > 0.0 ? between_curves(slower(fit.ends.from), fit.ends.from, fit.ends.from_cant, 1.0 / fit.arcs.front().radius, cants.front(), norms) :
transition_length(allowed, fit.arcs.front().radius, cants.front(), norms)};
auto const requiredout{fit.ends.to > 0.0 ? between_curves(slower(fit.ends.to), 1.0 / fit.arcs.back().radius, cants.back(), fit.ends.to, fit.ends.to_cant, norms) :
transition_length(allowed, fit.arcs.back().radius, cants.back(), norms)};
// the transition between the curve of the adjoining track and the arc told apart from the one to the straight
auto const shortin{report.transition_in + 1e-6 < requiredin};
auto const shortout{report.transition_out + 1e-6 < requiredout};
auto const betweenin{fit.ends.from > 0.0};
auto const betweenout{fit.ends.to > 0.0};
if ((shortin && betweenin) || (shortout && betweenout))
r.warnings.emplace_back(format(STR_C("Vertex %zu: the transition curve between the curve of the adjoining track and R %.0f m is shorter than the required %.0f m"), k + 1,
shortin && betweenin ? fit.arcs.front().radius : fit.arcs.back().radius, shortin && betweenin ? requiredin : requiredout));
if ((shortin && false == betweenin) || (shortout && false == betweenout))
r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shorter than the required %.0f m"), k + 1, std::max(shortin && false == betweenin ? requiredin : 0.0, shortout && false == betweenout ? requiredout : 0.0)));
for (std::size_t i = 1; i < fit.arcs.size(); ++i)
{
auto const required{transition_between(allowed, 1.0 / fit.arcs[i - 1].radius, cants[i - 1], 1.0 / fit.arcs[i].radius, cants[i], norms)};
@@ -1116,9 +1313,11 @@ result fit_between(design const &Design)
auto const theirs{(Turn != 0 ? Turn : Own < 0.0 ? -1 : 1) / Radius};
return std::abs(theirs - Own) > 0.05 * std::max(1.0 / Radius, std::abs(Own));
};
if (abrupt(Design.start_radius, Design.start_turn, curvature_at(false)))
// a design asked to go on from the curve where its curve turns its way leaves the end where it doesn't to the one laying it: it eases
// out of the curve there, or keeps the jump at the end of the diverging track of a switch, as the switch has it at its start
if (false == Design.start_curved && abrupt(Design.start_radius, Design.start_turn, curvature_at(false)))
r.warnings.emplace_back(format(STR_C("The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly"), Design.start_radius));
if (abrupt(Design.end_radius, Design.end_turn, curvature_at(true)))
if (false == Design.end_curved && abrupt(Design.end_radius, Design.end_turn, curvature_at(true)))
r.warnings.emplace_back(format(STR_C("The adjoining track at the end is curved (R %.0f m), the curvature changes abruptly"), Design.end_radius));
make_cants(Design, r);
@@ -1150,6 +1349,9 @@ result compute(design const &Design)
extended.end = along_end(Design.end, Design.end_direction, Design.end_grade, endextension);
extended.start_reserve = 0.0;
extended.end_reserve = 0.0;
// a curve going on from the adjoining track begins right at the end, which isn't moved; an end moved is on a straight
extended.start_curved = Design.start_curved && startextension == 0.0;
extended.end_curved = Design.end_curved && endextension == 0.0;
if (extended.vertices.size() >= 2)
{
extended.vertices.front().offset += startextension;
@@ -2861,11 +3063,90 @@ result ease_bends(design &Design, std::vector<char> const &Corners, corner_radii
auto const low{std::max(10.0, std::min(Radii.low, Radii.high))};
auto const high{std::max(low, Radii.high)};
// a bend at the start, or at the end, turning the way of the curve of the adjoining track goes on from it: its curve begins right
// there, as large as the bend makes it, with the transitions and the cant of the norms between it and the adjoining curve and out of
// it to the straight. Its size fixed so, the curves next to it share the straight with it as with a curve left as it is
std::vector<char> fixed(count, 0);
for (std::size_t k = 0; k < count; ++k)
{
if (false == corner(k) || (k > 0 && k + 1 < count))
continue;
auto const in{polygon[k + 1] - polygon[k]};
auto const out{polygon[k + 2] - polygon[k + 1]};
if (glm::length(in) < 1e-6 || glm::length(out) < 1e-6)
continue;
int const side{cross(in, out) > 0.0 ? 1 : -1};
auto const from{k == 0 && Design.start_curved && Design.start_radius > 0.0 && Design.start_turn == side};
auto const to{k + 1 == count && Design.end_curved && Design.end_radius > 0.0 && Design.end_turn == side && false == from};
if (false == from && false == to)
continue;
// the bend alone between the end and the point after it, or before it, the straight there as long as the curve wants
auto single{Design};
single.vertices.assign(1, vertex{});
if (from)
{
single.end = {polygon[k + 2].x, Design.end.y, polygon[k + 2].y};
single.end_direction = glm::normalize(out);
single.end_curved = false;
single.end_reserve = 1e4;
}
else
{
single.start = {polygon[k].x, Design.start.y, polygon[k].y};
single.start_direction = glm::normalize(in);
single.start_curved = false;
single.start_reserve = 1e4;
}
auto const adjoining{from ? 1.0 / Design.start_radius : 1.0 / Design.end_radius};
auto const adjoiningcant{std::max(0.0, from ? Design.start_cant : Design.end_cant)};
auto &shape{single.vertices.front()};
shape.radius = std::max(1.0, Design.vertices[k].radius);
std::optional<curve_report> made;
// the transitions and the cant follow the radius, which the transitions move a little
for (int pass = 0; pass < 6; ++pass)
{
auto const allowed{std::max(1.0, std::min(speed, speed_limit(shape.radius, norms)))};
auto const recommended{recommend(allowed, shape.radius, norms)};
auto const slower{std::max(1.0, std::min(allowed, speed_limit(1.0 / adjoining, norms)))};
auto const along{recommended.transition};
auto const cant{ramped_cant(recommended.cant, along, allowed, norms)};
auto const between{between_curves(slower, adjoining, adjoiningcant, 1.0 / shape.radius, cant, norms)};
(from ? shape.transition_in : shape.transition_out) = between;
(from ? shape.transition_out : shape.transition_in) = along;
shape.cant = cant;
auto const laid{compute(single)};
if (false == laid.valid || laid.curves.size() != 1)
{
made.reset();
break;
}
auto const previous{shape.radius};
made = laid.curves.front();
shape.radius = made->radius;
if (std::abs(shape.radius - previous) < 0.01)
break;
}
if (false == made.has_value())
continue;
auto &vertex{Design.vertices[k]};
vertex.spirals = false;
vertex.radius = shape.radius;
vertex.transition_in = shape.transition_in;
vertex.transition_out = shape.transition_out;
vertex.cant = shape.cant;
vertex.reverse_turn = false;
vertex.compound = false;
vertex.share = 1.0;
vertex.arcs.clear();
fixed[k] = 1;
kept[k] = {made->tangent_in, made->tangent_out};
}
// the shapes each bend can take, the best first
std::vector<std::vector<corner_shape>> ladders(count);
for (std::size_t k = 0; k < count; ++k)
{
if (false == corner(k))
if (false == corner(k) || fixed[k] != 0)
continue;
auto const in{polygon[k + 1] - polygon[k]};
auto const out{polygon[k + 2] - polygon[k + 1]};
@@ -2955,7 +3236,7 @@ result ease_bends(design &Design, std::vector<char> const &Corners, corner_radii
auto const reach_of = [&](std::size_t const K, bool const Out) {
if (false == ladders[K].empty())
return ladders[K][picks[K]].reach;
return corner(K) ? 0.0 : kept[K][Out ? 1 : 0];
return corner(K) && fixed[K] == 0 ? 0.0 : kept[K][Out ? 1 : 0];
};
auto const best_within = [&](std::size_t const K, double const Room) {
auto const &ladder{ladders[K]};
@@ -3097,6 +3378,9 @@ result ease_corners(design &Design, std::vector<char> &Corners, corner_radii con
base.end_direction = glm::normalize(polygon[m + 2] - polygon[m + 1]);
base.start_reserve = 0.0;
base.end_reserve = 0.0;
// the curve of the row goes on from the curve of the adjoining track where the row begins at the start, or ends at the end
base.start_curved = Design.start_curved && k == 0;
base.end_curved = Design.end_curved && m + 1 == static_cast<int>(Design.vertices.size());
std::vector<glm::dvec2> points(polygon.begin() + k + 1, polygon.begin() + m + 2);
// two bends don't show the curve between them: the curve goes through the line their two curves laid, with no
// straight between them, from where the first begins to where the second ends, the straights either side its room
@@ -3120,6 +3404,8 @@ result ease_corners(design &Design, std::vector<char> &Corners, corner_radii con
base.end_direction = glm::normalize(plan_of(after.direction));
base.start_reserve = from - (first > 0 ? curves[first - 1].to : 0.0);
base.end_reserve = (last + 1 < curves.size() ? curves[last + 1].from : line.length) - to;
base.start_curved = base.start_curved && from < 1e-6;
base.end_curved = base.end_curved && to > line.length - 1e-6;
within = kPointsWithin;
}
auto const fit{Fit ? Fit(base, points) : std::optional<trace_stretch>{fit_trace_stretch(base, points, 0, false)}};
@@ -3185,6 +3471,16 @@ std::optional<trace_stretch> stretch_of(design const &Design, std::vector<glm::d
stretch.vertices = Design.vertices;
for (std::size_t k = 0; k < stretch.vertices.size() && k < laid.vertices.size(); ++k)
stretch.vertices[k].position = laid.vertices[k];
// a curve going on from an adjoining one with the radii it was laid with, which begin it at the end
for (auto const &curve : laid.curves)
if ((curve.curvature_in > 0.0 || curve.curvature_out > 0.0) && curve.vertex >= 0 && curve.vertex < static_cast<int>(stretch.vertices.size()))
{
auto &vertex{stretch.vertices[curve.vertex]};
auto const scale{curve.radius / std::max(1.0, vertex.radius)};
vertex.radius = curve.radius;
for (auto &arc : vertex.arcs)
arc.radius *= scale;
}
double sum{0.0};
for (auto const &projection : project(laid, Points))
{
@@ -3196,15 +3492,12 @@ std::optional<trace_stretch> stretch_of(design const &Design, std::vector<glm::d
return stretch;
}
// how far the points are from the line the design lays, a point clicked by mistake far off pulling it less than the square of its
// departure would
bool stretch_residuals(design Trial, std::vector<glm::dvec2> const &Points, std::vector<double> &Residuals)
// how far the points are from the line laid, a point clicked by mistake far off pulling it less than the square of its departure would
bool laid_residuals(result const &Laid, std::vector<glm::dvec2> const &Points, std::vector<double> &Residuals)
{
Trial.norms.checked = false;
auto const laid{compute(Trial)};
if (false == laid.valid)
if (false == Laid.valid)
return false;
auto const line{sample_plan(laid, 0.0, laid.length, std::clamp(laid.length / 300.0, 0.5, 4.0))};
auto const line{sample_plan(Laid, 0.0, Laid.length, std::clamp(Laid.length / 300.0, 0.5, 4.0))};
Residuals.clear();
for (auto const &point : Points)
{
@@ -3215,11 +3508,19 @@ bool stretch_residuals(design Trial, std::vector<glm::dvec2> const &Points, std:
return true;
}
// the same for the line the design lays
bool stretch_residuals(design Trial, std::vector<glm::dvec2> const &Points, std::vector<double> &Residuals)
{
Trial.norms.checked = false;
return laid_residuals(compute(Trial), Points, Residuals);
}
// the same for a design of one compound curve, its shape made straight away as fit_between makes it, many times quicker than the whole
// design laid: the fits of compound curves try it some thousand times
bool compound_residuals(design const &Trial, std::vector<glm::dvec2> const &Points, std::vector<double> &Residuals)
{
if (Trial.vertices.size() != 1 || false == Trial.vertices.front().compound || Trial.vertices.front().arcs.empty())
// a curve going on from an adjoining one is placed by its end, not by the vertex: laid as a whole
if (Trial.vertices.size() != 1 || false == Trial.vertices.front().compound || Trial.vertices.front().arcs.empty() || Trial.start_curved || Trial.end_curved)
return stretch_residuals(Trial, Points, Residuals);
glm::dvec2 corner;
if (false == tangent_intersection(Trial, corner))
@@ -3300,6 +3601,113 @@ void plain_curve(vertex &Vertex, double const Radius, double const Transition, d
Vertex.cant = Transition > 0.0 ? ramped_cant(recommend(allowed, Radius, Norms).cant, Transition, allowed, Norms) : 0.0;
}
// the cant of the norms for a curve going on from the curve of an adjoining track, or into it, as much as its transitions ramp: the one
// to the straight from 0, the one between it and the adjoining curve from the cant of that one, which goes on into the arc with none
double curved_cant(double const Radius, double const Between, double const Out, double const Adjoining, double const Speed, limits const &Norms)
{
if (Between < 1.0)
return Adjoining;
auto const allowed{std::max(1.0, std::min(Speed, speed_limit(Radius, Norms)))};
auto const cant{ramped_cant(recommend(allowed, Radius, Norms).cant, Out, allowed, Norms)};
auto const reach{std::min(Between * 1000.0 / (Norms.ramp_factor * allowed), Norms.cant_rate * 3.6 * Between / allowed)};
if (std::abs(cant - Adjoining) <= reach)
return cant;
auto const ramped{cant > Adjoining ? Adjoining + std::floor(reach / 5.0) * 5.0 : Adjoining - std::floor(reach / 5.0) * 5.0};
return std::max(0.0, ramped);
}
// the transitions of the curve of the vertex going on from the curve of the adjoining track at the start, or into the one at the end,
// following the points best: its radius the one which begins it there, its cant that of the norms as far as the transitions ramp it
void fit_curved(design &Design, std::size_t const Vertex, std::vector<glm::dvec2> const &Points, bool const Fromstart)
{
auto const speed{std::max(1.0, Design.speed)};
auto const fromstart{Fromstart};
auto const adjoining{std::max(kTraceRadiusMin, fromstart ? Design.start_radius : Design.end_radius)};
auto const adjoiningcant{std::max(0.0, fromstart ? Design.start_cant : Design.end_cant)};
auto const longest{longest_transition(speed, adjoining, Design.norms)};
// the radius is sought from the one laid last, a step of the fit moving it little
auto radius{adjoining};
// the square roots of the lengths of the transitions, between the curves and to the straight, a 30th of them: the step the fit takes
// its derivatives with moving the curve more than the chords the points are measured against do; one parameter: none between them,
// the curve going on at the radius of the adjoining one
constexpr double kScale{30.0};
auto const length_of = [](double const Parameter) { return kScale * kScale * Parameter * Parameter; };
auto const parameter_of = [](double const Length) { return std::sqrt(Length) / kScale; };
auto const shaped = [&](std::vector<double> const &Parameters, design &Trial) {
Trial = Design;
auto &curve{Trial.vertices[Vertex]};
curve = {curve.position, curve.offset, radius};
auto const between{Parameters.size() > 1 ? std::min(length_of(Parameters[0]), longest) : 0.0};
auto const out{std::min(length_of(Parameters.back()), longest)};
curve.transition_in = fromstart ? between : out;
curve.transition_out = fromstart ? out : between;
curve.cant = 0.0;
};
auto const laid_with = [&](result const &Laid) {
for (auto const &curve : Laid.curves)
if (curve.vertex == static_cast<int>(Vertex))
radius = curve.radius;
};
residuals_of const residuals{[&](std::vector<double> const &Parameters, std::vector<double> &Residuals) {
design trial;
shaped(Parameters, trial);
trial.norms.checked = false;
auto const laid{compute(trial)};
laid_with(laid);
return laid_residuals(laid, Points, Residuals);
}};
// the transition between the curves short or none, as where the curve goes on at the radius of the adjoining one, or as long as the
// one to the straight; this one as long as the norms ask for at the radius of the adjoining curve. The square roots of the lengths,
// a length of nought holding the step at nought
auto const allowed{std::max(1.0, std::min(speed, speed_limit(adjoining, Design.norms)))};
auto const out{std::clamp(recommend(allowed, adjoining, Design.norms).transition, 10.0, 0.9 * longest)};
std::vector<double> best;
auto lowest{std::numeric_limits<double>::max()};
for (auto const between : {2.0, 0.5 * out})
{
radius = adjoining;
std::vector<double> parameters{parameter_of(between), parameter_of(out)};
if (auto const cost{least_squares(parameters, residuals)}; cost < lowest)
{
lowest = cost;
best = parameters;
}
}
// the curve going on at the radius of the adjoining one, with no transition between them, where it keeps to the points about as well:
// a short transition the fit leaves only follows points clicked a little off
{
radius = adjoining;
std::vector<double> parameters{parameter_of(out)};
auto const cost{least_squares(parameters, residuals)};
auto const points{static_cast<double>(std::max<std::size_t>(1, Points.size()))};
// the radius it begins with the one of the adjoining curve, within 5 %, else the curvature would jump between them
design trial;
shaped(parameters, trial);
trial.norms.checked = false;
laid_with(compute(trial));
auto const along{std::abs(radius - adjoining) <= 0.05 * adjoining};
if (along && cost < std::numeric_limits<double>::max() && (best.empty() || std::sqrt(cost / points) <= std::sqrt(lowest / points) + 0.01))
{
lowest = cost;
best = parameters;
}
}
if (best.empty())
return;
design fitted;
shaped(best, fitted);
auto &curve{fitted.vertices[Vertex]};
curve.transition_in = std::round(curve.transition_in);
curve.transition_out = std::round(curve.transition_out);
auto const laid{compute(fitted)};
if (false == laid.valid)
return;
laid_with(laid);
curve.radius = radius;
curve.cant = fromstart ? curved_cant(radius, curve.transition_in, curve.transition_out, adjoiningcant, speed, fitted.norms) : curved_cant(radius, curve.transition_out, curve.transition_in, adjoiningcant, speed, fitted.norms);
Design = fitted;
}
// the radius and the length of the transitions of the curve of the vertex following the points best, the rest of the design kept: the
// transitions of a line on the photo are what they are, the norms may have asked for others
void fit_curve(design &Design, std::size_t const Vertex, std::vector<glm::dvec2> const &Points)
@@ -3572,7 +3980,29 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
single.vertices.assign(1, vertex{});
auto &curve{single.vertices.front()};
curve.transition_in = curve.transition_out = curve.cant = 0.0;
if (auto const arc{fit_trace(single, 0, Points, 1)}; arc.valid)
// a curve going on from the curve of the adjoining track at the start, or into the one at the end, turning its way: the
// transitions the points show, the radius the one which begins it there; the radii of a compound curve where they're asked for
// or one doesn't keep to the points
int const side{cross(startdirection, enddirection) > 0.0 ? 1 : -1};
auto const fromstart{Base.start_curved && Base.start_radius > 0.0 && Base.start_turn == side};
auto const curved{fromstart || (Base.end_curved && Base.end_radius > 0.0 && Base.end_turn == side)};
if (curved)
{
curve.radius = fromstart ? Base.start_radius : Base.end_radius;
fit_curved(single, 0, Points, fromstart);
one = stretch_of(single, Points);
auto const loose{one.has_value() && (one->rms > kStretchClose || one->worst > kPointsWithin)};
if (one.has_value() && Radii != 1 && (Radii > 1 || loose))
if (auto const fit{fit_trace(single, 0, Points, Radii > 1 ? Radii : 2)}; fit.valid && fit.fitted.compound)
{
auto several{single};
several.vertices.front() = fit.fitted;
fit_compound(several, 0, Points);
if (auto const compound{stretch_of(several, Points)}; compound.has_value() && (Radii > 1 || compound->rms < 0.75 * one->rms))
one = compound;
}
}
else if (auto const arc{fit_trace(single, 0, Points, 1)}; arc.valid)
{
curve = arc.fitted;
fit_curve(single, 0, Points);
@@ -3695,8 +4125,10 @@ trace_stretch fit_trace_stretch(design const &Base, std::vector<glm::dvec2> cons
// none where a curve ran to the end of the radii tried, turns hardly at all, or doesn't fit between the straights: the
// points made the fit find two curves where they show one
auto const laid{compute(two)};
// a curve of the two going on from the curve of an adjoining track has its transitions tied together here, to the straight
// and between the curves alike: none such, the one curve going on from it is fitted on its own
auto const sound{laid.valid && laid.radius_warnings.empty() && laid.curves.size() == 2 && std::all_of(laid.curves.begin(), laid.curves.end(), [](curve_report const &Curve) {
return Curve.radius > 1.05 * kTraceRadiusMin && Curve.radius < 0.95 * kTraceRadiusMax && Curve.deflection > 0.25 * kPi / 180.0;
return Curve.radius > 1.05 * kTraceRadiusMin && Curve.radius < 0.95 * kTraceRadiusMax && Curve.deflection > 0.25 * kPi / 180.0 && Curve.curvature_in <= 0.0 && Curve.curvature_out <= 0.0;
})};
// one compound curve rather than two turning the same way with a short straight between them; two such only where the
// straight is long and they keep to the points much closer

View File

@@ -98,6 +98,16 @@ struct design
// known; a design going on at the curvature of the adjoining track doesn't change it abruptly
int start_turn{0};
int end_turn{0};
// the curve of the first vertex goes on from the curve of the adjoining track at the start, where it turns its way, as a track laid on
// from a curved free end: it begins right at the start, with no straight before it, its transition in going from the curvature and
// the cant of the adjoining track (start_radius, start_turn, start_cant) to those of its first arc. The radii of its arcs are the
// ones which begin it there, the transitions and the shares as given. The same for the curve of the last vertex at the end; a curve
// of the only vertex goes on from the start alone
bool start_curved{false};
bool end_curved{false};
// mm, the cant of the adjoining track at the start and at the end, leaning into its turn
double start_cant{0.0};
double end_cant{0.0};
// length of the straight beyond the start and the end, which the curves can take over
double start_reserve{0.0};
double end_reserve{0.0};
@@ -175,6 +185,11 @@ struct curve_report
recommendation recommended;
bool spirals{false}; // two transition curves with no arc between them
double cant{0.0};
// the curvature and the cant of the curve of the adjoining track it goes on from at its start, and into at its end; 0 from a straight
double curvature_in{0.0};
double cant_in{0.0};
double curvature_out{0.0};
double cant_out{0.0};
};
struct profile_point

View File

@@ -130,7 +130,13 @@ void mark(scene::layer_handle const Layer)
lines.push_back(format("source %s origin %.4f shape %s speed %.1f", entry.source.c_str(), entry.origin, shape_name(design.shape), design.speed) + (entry.fit > 0.0 ? format(" fit %.3f", entry.fit) : std::string{}));
auto const startdirection{local_direction(context, design.start, design.start_direction)};
auto const enddirection{local_direction(context, design.end, design.end_direction)};
lines.push_back(format("ends %.9f %.9f %.9f %.9f %.6f %.6f %.3f %.3f", startdirection.x, startdirection.y, enddirection.x, enddirection.y, design.start_grade, design.end_grade, design.start_radius, design.end_radius));
auto ends{format("ends %.9f %.9f %.9f %.9f %.6f %.6f %.3f %.3f", startdirection.x, startdirection.y, enddirection.x, enddirection.y, design.start_grade, design.end_grade, design.start_radius, design.end_radius)};
// the sides the adjoining tracks turn to, and the curves going on from them with their cants, written since there are such
if (design.start_turn != 0 || design.end_turn != 0)
ends += format(" turns %d %d", design.start_turn, design.end_turn);
if (design.start_curved || design.end_curved)
ends += format(" curved %d %d %.1f %.1f", design.start_curved ? 1 : 0, design.end_curved ? 1 : 0, design.start_cant, design.end_cant);
lines.push_back(ends);
auto const &norms{design.norms};
lines.push_back(format("norms %.1f %.3f %.1f %.1f %.2f %.3f %.2f %.2f %.3f", norms.gauge, norms.unbalanced, norms.cant_max, norms.cant_rate, norms.ramp_factor, norms.jerk, norms.tangent_min_time, norms.arc_min, norms.arc_min_factor));
lines.push_back(format("pieces %d %.3f", design.transition_pieces, design.arc_piece_angle));
@@ -378,6 +384,16 @@ void read_part(record &Entry, std::string const &Keyword, std::istringstream &Li
Line >> startdirection.x >> startdirection.y >> enddirection.x >> enddirection.y >> design.start_grade >> design.end_grade >> design.start_radius >> design.end_radius;
design.start_direction = world_direction(Context, Context.to_local(design.start), startdirection);
design.end_direction = world_direction(Context, Context.to_local(design.end), enddirection);
for (std::string key; Line >> key;)
{
if (key == "turns")
Line >> design.start_turn >> design.end_turn;
if (int start{0}, end{0}; key == "curved" && Line >> start >> end >> design.start_cant >> design.end_cant)
{
design.start_curved = (start != 0);
design.end_curved = (end != 0);
}
}
}
else if (Keyword == "norms")
{

View File

@@ -12152,3 +12152,39 @@ msgstr "Esc dwa razy przy układaniu"
msgid "Drop the points clicked and not laid yet"
msgstr "Porzuca kliknięte, jeszcze nieułożone punkty"
# laying on from a curved free end: the curve next to it goes on from its curvature
msgid "Vertex %zu: no curve goes on from the curve of the adjoining track (R %.0f m) to the straight after it"
msgstr "Wierzchołek %zu: żaden łuk nie przejdzie od łuku przylegającego toru (R %.0f m) do prostej za nim"
msgid "Vertex %zu: no curve goes on from the straight before it into the curve of the adjoining track (R %.0f m)"
msgstr "Wierzchołek %zu: żaden łuk nie przejdzie od prostej przed nim w łuk przylegającego toru (R %.0f m)"
msgid "Goes on from the curve of the track at the start (R %.0f m)"
msgstr "Kontynuuje łuk toru na początku (R %.0f m)"
msgid "Goes into the curve of the track at the end (R %.0f m)"
msgstr "Przechodzi w łuk toru na końcu (R %.0f m)"
msgid "The curve begins right at the end of the fragment, at the curvature of the track adjoining there, where it turns the same way; its radius is the one which begins it there, the transition at that end goes between the two curves"
msgstr "Łuk zaczyna się dokładnie na końcu odcinka, z krzywizną przylegającego tam toru, jeśli skręca w tę samą stronę; jego promień to ten, przy którym zaczyna się właśnie tam, a krzywa przejściowa na tym końcu łączy oba łuki"
msgid "goes on from R %.0f m of the track it starts at"
msgstr "kontynuuje R %.0f m toru, od którego się zaczyna"
msgid "goes on from R %.0f m of the track it starts at, a transition of %.0f m between them"
msgstr "kontynuuje R %.0f m toru, od którego się zaczyna, z krzywą przejściową %.0f m między nimi"
msgid "goes into R %.0f m of the track it ends at"
msgstr "przechodzi w R %.0f m toru, na którym się kończy"
msgid "goes into R %.0f m of the track it ends at, a transition of %.0f m between them"
msgstr "przechodzi w R %.0f m toru, na którym się kończy, z krzywą przejściową %.0f m między nimi"
msgid "Curve %d %s"
msgstr "Łuk %d %s"
msgid "Vertex %zu: the transition curve between the curve of the adjoining track and R %.0f m is shorter than the required %.0f m"
msgstr "Wierzchołek %zu: krzywa przejściowa między łukiem przylegającego toru a R %.0f m jest krótsza niż wymagane %.0f m"

View File

@@ -3,6 +3,7 @@
#include "editor/editorAlignment.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <string>
@@ -67,6 +68,56 @@ std::vector<glm::dvec2> traced(alignment::result const &Line, double const Spaci
return points;
}
// a line from the origin along x through segments of the curvature going evenly from the first to the second (to the left positive) over
// the length, as the end it comes to and the heading there; small steps of Simpson
struct line_end {
glm::dvec2 position{0.0};
double heading{0.0};
};
line_end walked(std::vector<std::array<double, 3>> const &Segments) {
line_end end;
for (auto const &[from, to, length] : Segments) {
int const steps{std::max(2, static_cast<int>(std::ceil(length / 0.25)) * 2)};
auto const h{length / steps};
auto const angle = [&, from = from, to = to, length = length](double const S) { return end.heading + from * S + (to - from) * S * S / (2.0 * length); };
glm::dvec2 sum{0.0};
for (int i = 0; i <= steps; ++i) {
double const weight = (i == 0 || i == steps) ? 1.0 : (i % 2 ? 4.0 : 2.0);
sum += weight * glm::dvec2{std::cos(angle(i * h)), std::sin(angle(i * h))};
}
end.position += sum * (h / 3.0);
end.heading = angle(length);
}
return end;
}
// the curvature of the line at the chainage, positive to the left, from the headings half a metre either side
double curvature_of(alignment::result const &Line, double const Chainage) {
auto const a{std::clamp(Chainage - 0.25, 0.0, Line.length)};
auto const b{std::clamp(Chainage + 0.25, 0.0, Line.length)};
auto const from{alignment::evaluate(Line, a).direction};
auto const to{alignment::evaluate(Line, b).direction};
return (std::atan2(to.z, to.x) - std::atan2(from.z, from.x)) / (b - a);
}
// a design from the origin along x, on from a curve to the left of the radius, to the straight the segments end at, 200 m along it; one
// vertex where the straights meet
alignment::design curved_from(double const Radius, std::vector<std::array<double, 3>> const &Segments, double const Speed) {
auto const end{walked(Segments)};
alignment::design design;
design.speed = Speed;
design.start = {0.0, 0.0, 0.0};
design.start_direction = heading(0.0);
auto const beyond{end.position + heading(end.heading) * 200.0};
design.end = {beyond.x, 0.0, beyond.y};
design.end_direction = heading(end.heading);
design.start_radius = Radius;
design.start_turn = 1;
design.start_curved = true;
design.vertices.emplace_back();
return design;
}
// the stretch traced as the lay tool asks for it: from the start of the line along its first straight, to the last point along the
// straight through the last two
alignment::design stretch_base(alignment::design const &Line, std::vector<glm::dvec2> const &Points) {
@@ -468,3 +519,231 @@ TEST_CASE("alignment: a cant ramp moved off the transition curve onto the straig
auto const turned{std::max_element(paths.begin(), paths.end(), [](segment_data const &A, segment_data const &B) { return std::abs(A.rolls[1]) < std::abs(B.rolls[1]); })};
CHECK(std::abs(turned->rolls[1]) == doctest::Approx(glm::degrees(std::asin(60.0 / design.norms.gauge))).epsilon(1e-3));
}
TEST_CASE("alignment: a curve laid on from the diverging track of a switch begins at its end with its curvature") {
auto const adjoining = [](alignment::result const &Line) {
return std::any_of(Line.warnings.begin(), Line.warnings.end(), [](std::string const &Warning) { return Warning.find("adjoining track at the start") != std::string::npos; });
};
// R 300 on for 100 m, then a transition of 40 m out to the straight, at 40 km/h
auto design{curved_from(300.0, {{1.0 / 300.0, 1.0 / 300.0, 100.0}, {1.0 / 300.0, 0.0, 40.0}}, 40.0)};
auto &vertex{design.vertices.front()};
vertex.radius = 1000.0;
vertex.transition_in = 0.0;
vertex.transition_out = 40.0;
auto const line{alignment::compute(design)};
REQUIRE(line.valid);
REQUIRE(line.curves.size() == 1);
auto const &curve{line.curves.front()};
// the radius is the one which begins the curve at the start, whatever was given
CHECK(curve.radius == doctest::Approx(300.0).epsilon(1e-3));
CHECK(curve.curvature_in == doctest::Approx(1.0 / 300.0));
// no straight before it: the curvature of the switch from the very start, which is where it was
CHECK(line.elements.front().kind != alignment::element_kind::straight);
CHECK(curvature_of(line, 0.25) == doctest::Approx(1.0 / 300.0).epsilon(0.01));
auto const start{alignment::evaluate(line, 0.0)};
CHECK(std::abs(start.position.x) < 1e-6);
CHECK(std::abs(start.position.z) < 1e-6);
CHECK(std::abs(start.direction.z) < 1e-6);
CHECK_FALSE(adjoining(line));
CHECK(alignment::meets_norms(curve, design));
// the line through the end as given
auto const end{alignment::evaluate(line, line.length)};
CHECK(glm::distance(glm::dvec2{end.position.x, end.position.z}, glm::dvec2{design.end.x, design.end.z}) < 1e-3);
// the track before turning the other way: the curve from a straight, the jump of the curvature left to the one laying it, which eases
// out of the curve there; a design not asked to go on from it, as the plan of a line laid before, warns of it
auto other{design};
other.start_turn = -1;
auto const jump{alignment::compute(other)};
REQUIRE(jump.valid);
CHECK(jump.curves.front().curvature_in == doctest::Approx(0.0));
CHECK_FALSE(adjoining(jump));
other.start_curved = false;
CHECK(adjoining(alignment::compute(other)));
}
TEST_CASE("alignment: the cant of the curved track adjoining goes on into the curve with no transition between them") {
auto design{curved_from(600.0, {{1.0 / 600.0, 1.0 / 600.0, 150.0}, {1.0 / 600.0, 0.0, 80.0}}, 100.0)};
design.start_cant = 60.0;
auto &vertex{design.vertices.front()};
vertex.radius = 600.0;
vertex.transition_out = 80.0;
vertex.cant = 100.0;
auto const line{alignment::compute(design)};
REQUIRE(line.valid);
// the cant of the track adjoining from the very start, ramped out along the transition to the straight
CHECK(alignment::evaluate(line, 0.0).cant == doctest::Approx(60.0));
CHECK(alignment::evaluate(line, 50.0).cant == doctest::Approx(60.0));
auto const out{line.elements.back().kind == alignment::element_kind::straight ? line.elements[line.elements.size() - 2] : line.elements.back()};
CHECK(alignment::evaluate(line, out.chainage + out.length * 0.5).cant == doctest::Approx(30.0).epsilon(0.02));
CHECK(alignment::evaluate(line, out.chainage + out.length + 1.0).cant == doctest::Approx(0.0));
REQUIRE(line.ramps.size() == 1);
CHECK(line.ramps.front().from == doctest::Approx(out.chainage));
// the paths roll with the cant from the first one
auto const paths{alignment::pieces(line, design, alignment::minimum_pieces(line, design))};
REQUIRE_FALSE(paths.empty());
CHECK(paths.front().rolls[0] == doctest::Approx(glm::degrees(std::asin(60.0 / design.norms.gauge))).epsilon(1e-3));
}
TEST_CASE("alignment: a curve traced on from the diverging track of a switch keeps its curvature from the start") {
// the curve on the photo: R 300 from the end of the switch for 80 m, a transition of 25 m out, then the straight, at 40 km/h
auto truth{curved_from(300.0, {{1.0 / 300.0, 1.0 / 300.0, 80.0}, {1.0 / 300.0, 0.0, 25.0}}, 40.0)};
auto &vertex{truth.vertices.front()};
vertex.radius = 300.0;
vertex.transition_out = 25.0;
auto const line{alignment::compute(truth)};
REQUIRE(line.valid);
auto const points{traced(line, 10.0, 0.02)};
auto const base{stretch_base(truth, points)};
auto const fit{alignment::fit_trace_stretch(base, points, 0, true)};
REQUIRE(fit.valid);
REQUIRE(fit.vertices.size() == 1);
auto const &curve{fit.vertices.front()};
CHECK(curve.radius == doctest::Approx(300.0).epsilon(0.02));
CHECK(curve.transition_in < 8.0);
CHECK(curve.transition_out == doctest::Approx(25.0).epsilon(0.2));
CHECK(fit.rms < 0.03);
// clicked exactly, the diverging track read as R 300.9 as the editor reads a switch: the curve goes on at its radius with no transition
// between them and with the cant of the switch, none
auto const exact{traced(line, 10.0, 0.0)};
auto exactbase{stretch_base(truth, exact)};
exactbase.start_radius = 300.9;
auto const along{alignment::fit_trace_stretch(exactbase, exact, 0, true)};
REQUIRE(along.valid);
REQUIRE(along.vertices.size() == 1);
CHECK(along.vertices.front().transition_in == doctest::Approx(0.0));
CHECK(along.vertices.front().transition_out == doctest::Approx(25.0).epsilon(0.05));
CHECK(along.vertices.front().radius == doctest::Approx(300.0).epsilon(0.005));
CHECK(along.vertices.front().cant == doctest::Approx(0.0));
CHECK(along.rms < 0.005);
// laid as fitted: the curvature of the switch at the very start
auto laid{base};
laid.vertices = fit.vertices;
auto const shape{alignment::compute(laid)};
REQUIRE(shape.valid);
CHECK(shape.elements.front().kind != alignment::element_kind::straight);
CHECK(curvature_of(shape, 0.25) == doctest::Approx(1.0 / 300.0).epsilon(0.02));
// from a track turning the other way the curve is fitted from a straight, as before
auto other{base};
other.start_turn = -1;
auto const straight{alignment::fit_trace_stretch(other, points, 0, true)};
REQUIRE(straight.valid);
REQUIRE_FALSE(straight.vertices.empty());
auto apart{other};
apart.vertices = straight.vertices;
auto const from{alignment::compute(apart)};
REQUIRE(from.valid);
CHECK(from.curves.front().curvature_in == doctest::Approx(0.0));
}
TEST_CASE("alignment: a curve traced on from a curved free end through a transition into a larger radius") {
// R 300 at the free end, a transition of 30 m into R 500 for 60 m, 40 m out to the straight, at 60 km/h
auto truth{curved_from(300.0, {{1.0 / 300.0, 1.0 / 500.0, 30.0}, {1.0 / 500.0, 1.0 / 500.0, 60.0}, {1.0 / 500.0, 0.0, 40.0}}, 60.0)};
auto &vertex{truth.vertices.front()};
vertex.radius = 500.0;
vertex.transition_in = 30.0;
vertex.transition_out = 40.0;
auto const line{alignment::compute(truth)};
REQUIRE(line.valid);
REQUIRE(line.curves.size() == 1);
CHECK(line.curves.front().radius == doctest::Approx(500.0).epsilon(1e-3));
// clicked exactly, the curve found again
auto const exact{traced(line, 10.0, 0.0)};
auto const fit{alignment::fit_trace_stretch(stretch_base(truth, exact), exact, 0, true)};
REQUIRE(fit.valid);
REQUIRE(fit.vertices.size() == 1);
auto const &curve{fit.vertices.front()};
CHECK(curve.radius == doctest::Approx(500.0).epsilon(0.01));
CHECK(curve.transition_in == doctest::Approx(30.0).epsilon(0.05));
CHECK(curve.transition_out == doctest::Approx(40.0).epsilon(0.05));
CHECK(fit.rms < 0.01);
// clicked 2 cm off: the straight after the curve, through the last two points, a little off too, the curve keeps to the points as well
auto const points{traced(line, 10.0, 0.02)};
auto const noisy{alignment::fit_trace_stretch(stretch_base(truth, points), points, 0, true)};
REQUIRE(noisy.valid);
REQUIRE(noisy.vertices.size() == 1);
CHECK(noisy.vertices.front().radius == doctest::Approx(500.0).epsilon(0.03));
CHECK(noisy.rms < 0.025);
}
TEST_CASE("alignment: a bend clicked ahead of a curved free end, turning its way, is eased with a curve going on from it") {
alignment::design design;
design.speed = 60.0;
design.start_direction = heading(0.0);
design.start_radius = 300.0;
design.start_turn = 1;
design.start_curved = true;
// the bend 150 m ahead, 20 degrees to the left, the end 300 m after it
alignment::vertex bend;
bend.position = {150.0, 0.0};
design.vertices.push_back(bend);
auto const end{bend.position + heading(20.0 * pi / 180.0) * 300.0};
design.end = {end.x, 0.0, end.y};
design.end_direction = heading(20.0 * pi / 180.0);
std::vector<char> corners{1};
auto const line{alignment::ease_corners(design, corners, {3000.0, 6000.0})};
REQUIRE(line.valid);
REQUIRE(line.curves.size() == 1);
auto const &curve{line.curves.front()};
CHECK(line.elements.front().kind != alignment::element_kind::straight);
CHECK(curvature_of(line, 0.25) == doctest::Approx(1.0 / 300.0).epsilon(0.02));
// all of the straight before the bend taken by the curve, larger than the one it goes on from, through a transition between them
CHECK(curve.tangent_in == doctest::Approx(150.0).epsilon(1e-4));
CHECK(curve.radius > 400.0);
CHECK(curve.transition_in > 5.0);
CHECK(alignment::meets_norms(curve, design));
CHECK(line.vertices.front().x == doctest::Approx(150.0));
CHECK(std::abs(line.vertices.front().y) < 1e-9);
// the design keeps the radius it's laid with
CHECK(design.vertices.front().radius == doctest::Approx(curve.radius).epsilon(1e-6));
// a second bend after it: the curve going on from the free end keeps its size, the next one makes room for it
auto two{design};
two.vertices = {bend, bend};
two.vertices[1].position = bend.position + heading(20.0 * pi / 180.0) * 220.0;
auto const after{two.vertices[1].position + heading(-5.0 * pi / 180.0) * 300.0};
two.end = {after.x, 0.0, after.y};
two.end_direction = heading(-5.0 * pi / 180.0);
two.vertices.front().offset = 150.0;
two.vertices.back().offset = 300.0;
std::vector<char> both{1, 1};
auto const pair{alignment::ease_corners(two, both, {3000.0, 6000.0})};
REQUIRE(pair.valid);
REQUIRE(pair.curves.size() == 2);
CHECK(pair.curves[0].radius == doctest::Approx(curve.radius).epsilon(1e-3));
CHECK(pair.curves[0].tangent_out + pair.curves[1].tangent_in <= 220.0 + 1e-3);
CHECK(curvature_of(pair, 0.25) == doctest::Approx(1.0 / 300.0).epsilon(0.02));
}
TEST_CASE("alignment: a curve laid into a curved free end ends at it with its curvature") {
// a straight of 100 m, a transition of 40 m into R 400 to the right, the arc 60 m on to the free end
auto const end{walked({{0.0, 0.0, 100.0}, {0.0, -1.0 / 400.0, 40.0}, {-1.0 / 400.0, -1.0 / 400.0, 60.0}})};
alignment::design design;
design.speed = 80.0;
design.start_direction = heading(0.0);
design.end = {end.position.x, 0.0, end.position.y};
design.end_direction = heading(end.heading);
design.end_radius = 400.0;
design.end_turn = -1;
design.end_curved = true;
alignment::vertex vertex;
vertex.radius = 2000.0;
vertex.transition_in = 40.0;
design.vertices.push_back(vertex);
auto const line{alignment::compute(design)};
REQUIRE(line.valid);
REQUIRE(line.curves.size() == 1);
CHECK(line.curves.front().radius == doctest::Approx(400.0).epsilon(1e-3));
CHECK(line.curves.front().curvature_out == doctest::Approx(1.0 / 400.0));
CHECK(line.elements.back().kind != alignment::element_kind::straight);
CHECK(curvature_of(line, line.length - 0.25) == doctest::Approx(-1.0 / 400.0).epsilon(0.01));
auto const last{alignment::evaluate(line, line.length)};
CHECK(glm::distance(glm::dvec2{last.position.x, last.position.z}, end.position) < 1e-3);
CHECK(std::none_of(line.warnings.begin(), line.warnings.end(), [](std::string const &Warning) { return Warning.find("adjoining track at the end") != std::string::npos; }));
}