17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-26 07:59:30 +01:00

feat(editor): laying from the photo in two modes, Tab switching them in one row of points: the points clicked as bends of straights, eased with two transition curves meeting with no cant, of 3000 to 6000 m set in the panel, or with an arc of the norms where they don't fit; or the points along the middle of a curve seen on the photo, the last two on the straight after it, a curve, compound or two with a straight between them, fitted to them with its radii and its transitions. The points clicked are kept where they are, a curve below the norms laid with its warnings in the colour of one; the line shown as the points are clicked, laid by Enter in one step of the undo and going on from its end, Backspace taking the last point back, Esc dropping the points not laid. A bend after a free end or a curve traced goes along its straight, dashed; the curves labelled with their radii, transitions, cant and speed, the straights with their lengths, the points traced in the colour of how far the line is from them, the curves of the line listed in the panel. The Plan sets two transition curves with no arc for a vertex. Selftests laymode, layradius, laytrace, layp, layreport, layenter, layback, laycancel, laysample

This commit is contained in:
maj00r
2026-10-24 21:12:27 +02:00
parent 500f068297
commit 6bd3d4d19f
9 changed files with 1012 additions and 210 deletions

View File

@@ -5570,15 +5570,16 @@ void editor_mode::render_shortcuts_window()
{"Shift+LMB", "Add a track to the selection, drag: a box"},
{"RMB on a track", "What can be done with it"},
{"K", "Split the selected track at the cursor, a break of the grade at the height of the ground"},
{"L, LMB, LMB...", "Lay track: each click lays a piece and goes on from its end"},
{"L, LMB, LMB...", "Lay track: each click a bend of the straights, eased with a large curve; Enter lays them and goes on from the end"},
{"Tab while laying", "The points clicked next go along the middle of a curve seen on the photo, the last two on the straight after it; Tab again: back to the bends"},
{"Drag from a free end", "Lead a track out of it; onto another free end: join the two"},
{"Ctrl+wheel", "Laying: radius"},
{"Shift+wheel", "Laying: arc then straight, straight then arc, arc, straight; at a free end: another way to join"},
{"0-9 while laying", "Type the length (or the radius) of the next piece, Enter lays it"},
{"0-9 while laying", "Type the length of the next straight, Enter puts the point"},
{"Alt held while laying", "The direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track; the first click with Alt goes onto the axis of the track"},
{"N", "Parallel track: LMB on a track, then the side and the spacing"},
{"Shift+LMB on a track in L", "The Parallel tool (N) with it"},
{"Enter / Backspace", "Laying track: lay up to the cursor / take the last piece back"},
{"Enter / Backspace", "Laying track: lay the points clicked / take back the last point, then the last piece laid"},
{"Del", "Delete the selected tracks"}}},
{"Plan of a curve",
{{"G, LMB on a curve", "Its design: the vertex and the radius to drag in the view, the curvature in the panel"},
@@ -6545,6 +6546,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
}
break;
case GLFW_KEY_TAB:
// laying: the bends of straights or a curve traced, for the points clicked from now on
if (is_press(Action) && false == anyModifier && ui()->mode() == nodebank_panel::TRACK && m_lay.active && false == ImGui::GetIO().WantTextInput)
lay_switch_mode();
break;
case GLFW_KEY_BACKSPACE:
// area fill: remove the last outline point
if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty())

View File

@@ -1384,11 +1384,42 @@ class editor_mode : public application_mode, private editor_track::observer, pri
std::vector<TTrack *> line;
};
extend_tool m_extend;
// what a point clicked while laying is: a bend of the straights, eased with a large curve, or a point along the middle of a curve
// seen on the photo, the last two of these on the straight after it
enum class lay_mode
{
straights,
curve
};
// a curve traced on the photo as it's laid, how far the points clicked along it are from it
struct lay_trace
{
std::vector<glm::dvec3> points;
std::vector<double> offsets;
double rms{0.0};
double worst{0.0};
std::size_t curves{0};
};
// the line the points clicked make: its design, the bends among its vertices, the curves traced
struct lay_line_plan
{
alignment::design design;
alignment::result shape;
std::vector<char> corners;
std::vector<lay_trace> traces;
// the straights of a fixed direction the next bend slides along: from a free end, out of a curve traced
std::vector<std::pair<glm::dvec3, glm::dvec3>> guides;
};
// lays new track through the clicked points, with no path needed to start from
struct lay_tool
{
bool active{false};
std::vector<glm::dvec3> points;
std::vector<lay_mode> modes; // of the points, as each was clicked
lay_mode mode{lay_mode::straights};
int radii{0}; // of a curve traced: 0 as many as the points need, up to three
bool straights_between{true}; // a curve traced may be two with a straight between them
lay_line_plan plan; // of the line shown
editor_track::snap_target start; // free end the track starts from, if any
glm::dvec3 mouse{0.0};
editor_track::snap_target mouse_snap;
@@ -1424,8 +1455,20 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void lay_finish(editor_track::snap_target const &End);
void lay_cancel();
void lay_enter();
std::vector<segment_data> lay_pieces(std::vector<glm::dvec3> const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const;
alignment::result lay_shape(std::vector<glm::dvec3> const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const;
// Tab: the bends of straights or the curve traced, for the points clicked from now on
void lay_switch_mode();
// the points the same in number as their modes, a point with none a bend of straights
void lay_trim();
// 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;
// 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
std::optional<alignment::trace_stretch> traced_stretch(alignment::design const &Base, std::vector<glm::dvec2> const &Points) const;
mutable std::vector<std::pair<std::vector<double>, alignment::trace_stretch>> m_stretch_cache;
// the stretch the cursor moves along is fitted on a thread, the frames drawn meanwhile with the fit done last
mutable alignment::stretch_fitter m_stretch_fitter;
void render_switch_ui();
void render_switch_timbers();
void render_rail_profile_hint(TTrack &Track);

View File

@@ -18,6 +18,7 @@ http:
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorGroundFollow.hpp"
#include "editor/editorIncludeInfo.hpp"
#include "utilities/Globals.h"
@@ -89,20 +90,30 @@ void editor_mode::update_build_tools()
lay.mouse = lay_bed(lay.mouse);
if (false == m_extend.active)
joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap);
lay_trim();
lay.plan = {};
if (lay.points.empty())
return;
// the cursor is the next point, in the mode it would be clicked in
auto points{lay.points};
auto modes{lay.modes};
auto end{lay.mouse_snap};
if (end.track != nullptr && glm::distance(end.position, points.front()) > 1.0)
{
points.push_back(end.position);
modes.push_back(lay.mode);
}
else
{
end = {};
if (plan_distance(lay.mouse, points.back()) > 1.0)
{
points.push_back(lay.mouse);
modes.push_back(lay.mode);
}
}
m_follow_preview = true;
lay.preview = lay_pieces(points, end, lay.preview_length, lay.preview_error);
lay.preview = lay_pieces(points, modes, end, lay.preview_length, lay.preview_error, &lay.plan);
m_follow_preview = false;
}
@@ -910,41 +921,32 @@ bool editor_mode::track_wheel(double const Offset)
return true;
}
std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const
std::vector<segment_data> editor_mode::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) const
{
auto const &tool{m_lay};
Length = 0.0;
m_eased_last.reset();
if (Points.size() < 2)
return {};
if (Points.size() == 2)
auto const unknown{std::numeric_limits<double>::quiet_NaN()};
if (Points.size() == 2 && End.track != nullptr)
{
auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])};
if (glm::length(leaving) < 1e-6)
{
Error = STR_C("Two points lie on one another");
return {};
}
if (End.track != nullptr && tool.start.track != nullptr)
return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error);
m_join_last = {};
std::vector<segment_data> pieces;
// two free ends: the curve between them is the one the join finds
if (tool.start.track != nullptr)
{
auto const grade{-grade_of(tool.start.direction)};
auto const eased{eased_layout(tool.start.track, Points[0], glm::normalize(leaving), Points[1], tool.radius)};
auto const laid{eased.has_value() && false == eased->pieces.empty()};
pieces = laid ? eased->pieces : editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius);
Error = laid ? narrowed(m_track_layout, tool.radius, eased->radius) : narrowed(pieces, m_track_layout, tool.radius);
follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits<double>::quiet_NaN(), tool.start.track);
auto const leaving{-plan_of(tool.start.direction)};
if (glm::length(leaving) < 1e-6)
{
Error = STR_C("Two points lie on one another");
return {};
}
return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error);
}
else if (End.track == nullptr)
{
auto const length{glm::length(leaving)};
pieces = straight_piece(Points[0], glm::normalize(leaving), (Points[1].y - Points[0].y) / length, length);
follow_ground(pieces, Points[0].y, std::numeric_limits<double>::quiet_NaN(), Points[1].y, std::numeric_limits<double>::quiet_NaN(), tool.copy_selected ? selected_track() : nullptr);
}
else if (glm::length(plan_of(End.direction)) > 1e-6)
// from a point to a free end: the shape of the layout laid back from the end
m_join_last = {};
std::vector<segment_data> pieces;
if (glm::length(plan_of(End.direction)) > 1e-6)
{
auto const eased{eased_layout(End.track, End.position, glm::normalize(-plan_of(End.direction)), Points[0], tool.radius)};
auto const laid{eased.has_value() && false == eased->pieces.empty()};
@@ -952,7 +954,7 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
for (auto piece{back.rbegin()}; piece != back.rend(); ++piece)
pieces.push_back(reversed(*piece));
Error = laid ? narrowed(m_track_layout, tool.radius, eased->radius) : narrowed(pieces, m_track_layout, tool.radius);
follow_ground(pieces, Points[0].y, std::numeric_limits<double>::quiet_NaN(), End.position.y, grade_of(End.direction), End.track);
follow_ground(pieces, Points[0].y, unknown, End.position.y, grade_of(End.direction), End.track);
}
for (auto const &piece : pieces)
Length += bezier{piece}.plan_length();
@@ -960,24 +962,17 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
Error = STR_C("The track can't be laid through these points");
return pieces;
}
auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])};
auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])};
if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3)
{
Error = STR_C("Two points lie on one another");
m_join_last = {};
lay_line_plan plan;
if (false == lay_line(Points, Modes, End, plan, Error))
return {};
}
alignment::design design;
auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)};
if (false == shape.valid)
{
Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front();
return {};
}
auto const &design{plan.design};
auto const &shape{plan.shape};
Error = lines_of(shape.warnings);
auto const count = [&](alignment::result const &Shape, alignment::design const &Design) {
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
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) {
@@ -993,88 +988,376 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
auto const out{eased(End, shape.length, -1.0)};
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 points{Points};
auto starting{design.start_direction};
auto ending{design.end_direction};
auto easeddesign{design};
if (false == in.pieces.empty())
{
points.front() = in.end;
starting = in.direction;
easeddesign.start = in.end;
easeddesign.start_direction = in.direction;
}
if (false == out.pieces.empty())
{
points.back() = out.end;
ending = -out.direction;
easeddesign.end = out.end;
easeddesign.end_direction = -out.direction;
}
alignment::design easeddesign;
auto const easedshape{lay_shape(points, starting, ending, easeddesign)};
// the bends stay where they are, measured from the ends moved
if (easeddesign.vertices.size() >= 2 && shape.vertices.size() == easeddesign.vertices.size())
{
easeddesign.vertices.front().offset = std::max(0.1, glm::dot(shape.vertices.front() - plan_of(easeddesign.start), glm::normalize(easeddesign.start_direction)));
easeddesign.vertices.back().offset = std::max(0.1, glm::dot(plan_of(easeddesign.end) - shape.vertices.back(), glm::normalize(easeddesign.end_direction)));
}
auto const easedshape{alignment::ease_corners(easeddesign, plan.corners, {EditorSettings.corner_low(), EditorSettings.corner_high()})};
if (easedshape.valid)
{
Length = editor_track::eased_length(in, easedshape.length, out);
Error = lines_of(easedshape.warnings);
return editor_track::eased_pieces(in, alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign)), out);
pieces = editor_track::eased_pieces(in, alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign)), out);
plan.design = easeddesign;
plan.shape = easedshape;
}
Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front());
else
Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front());
}
Length = shape.length;
return alignment::pieces(shape, design, count(shape, design));
if (pieces.empty())
{
Length = shape.length;
pieces = alignment::pieces(shape, design, count(shape, design));
}
// 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};
auto const grade0{tool.start.track != nullptr ? -grade_of(tool.start.direction) : unknown};
auto const grade1{End.track != nullptr ? grade_of(End.direction) : unknown};
if (EditorSettings.heights() == editorSettings::lay_heights::fitted)
follow_ground(pieces, Points.front().y, grade0, Points.back().y, grade1, style);
else
ground_follow::follow_points(pieces, std::vector<glm::dvec3>(Points.begin() + 1, Points.end() - 1), {Points.front().y, grade0, Points.back().y, grade1});
if (Plan != nullptr)
*Plan = std::move(plan);
return pieces;
}
alignment::result editor_mode::lay_shape(std::vector<glm::dvec3> const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &design) 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) const
{
auto const &tool{m_lay};
design = {};
Plan = {};
auto &design{Plan.design};
design.norms = laying_norms();
design.shape = m_route.design.shape;
design.speed = design_speed(tool.start.track, nullptr);
design.speed = design_speed(tool.start.track, End.track);
design.start = Points.front();
design.end = Points.back();
design.start_direction = Startdirection;
design.end_direction = Enddirection;
auto const joined{End.track != nullptr && glm::length(plan_of(End.direction)) > 1e-6};
// 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; };
auto const lifted = [](glm::dvec2 const &Point, double const Height) { return glm::dvec3{Point.x, Height, Point.y}; };
auto const apart = [&](glm::dvec2 const &Direction) {
if (glm::length(Direction) > 1e-6)
return true;
Error = STR_C("Two points lie on one another");
return false;
};
alignment::vertex corner;
corner.radius = std::max(1.0, tool.radius);
if (Points.size() == 2)
// the straight the line goes on along: a point of it and, where it's fixed, its direction
auto anchor{Points.front()};
std::optional<glm::dvec2> along;
if (tool.start.track != nullptr && glm::length(plan_of(tool.start.direction)) > 1e-6)
along = glm::normalize(-plan_of(tool.start.direction));
auto const startdirection{along};
struct bend
{
if (false == alignment::collinear(design))
glm::dvec3 position{0.0};
alignment::vertex vertex;
bool corner{true};
};
std::vector<bend> bends;
// a bend clicked after a straight of a fixed direction goes onto it, as far along it as it was clicked
auto const onto = [&](glm::dvec3 const &Point) {
auto const distance{std::max(1.0, glm::dot(plan_of(Point - anchor), *along))};
Plan.guides.emplace_back(anchor, lifted(plan_of(anchor) + *along * (distance + std::max(30.0, 0.2 * distance)), anchor.y));
return lifted(plan_of(anchor) + *along * distance, Point.y);
};
// the straights meet where the two lines cross, ahead of both points
auto const meeting = [](glm::dvec2 const &From, glm::dvec2 const &Along, glm::dvec2 const &To, glm::dvec2 const &Arriving, glm::dvec2 &Point) {
auto const determinant{cross(Along, Arriving)};
if (std::abs(determinant) < 1e-9)
return false;
auto const t{cross(To - From, Arriving) / determinant};
auto const u{cross(To - From, Along) / determinant};
if (t <= 0.0 || u <= 0.0)
return false;
Point = From + Along * t;
return true;
};
auto const straight_bend = [&](glm::dvec3 Point) {
if (along.has_value())
{
glm::dvec2 intersection;
if (alignment::tangent_intersection(design, intersection))
{
design.vertices.push_back(corner);
}
else
{
corner.offset = plan_distance(design.start, design.end) / 3.0;
design.vertices = {corner, corner};
}
Point = onto(Point);
along.reset();
}
bends.push_back({Point, {}, true});
anchor = Point;
};
std::size_t i{1};
while (i < inner)
{
auto last{i};
while (last < inner && mode_of(last) == lay_mode::curve)
++last;
auto const reaching{last == inner && joined};
// a bend of straights, or a point of a curve too alone to make one
if (last == i || (last - i < 2 && false == reaching))
{
straight_bend(Points[i]);
++i;
continue;
}
lay_trace trace;
std::vector<glm::dvec2> traced;
for (auto k{i}; k < last; ++k)
{
trace.points.push_back(Points[k]);
traced.push_back(plan_of(Points[k]));
}
i = last;
// 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};
glm::dvec2 entering;
// how far back the curve may begin before the point it comes from: up to the bend or the start before it, half the way back
// to a curve traced before, nothing before a free end
double behind{0.0};
if (along.has_value())
{
entering = *along;
if (false == bends.empty())
behind = 0.5 * plan_distance(entry, bends.back().position);
}
else if (false == bends.empty() && bends.back().corner)
{
entry = bends.back().position;
bends.pop_back();
auto const before{bends.empty() ? Points.front() : bends.back().position};
entering = plan_of(entry - before);
behind = plan_distance(entry, before) * (bends.empty() || bends.back().corner ? 1.0 : 0.5);
}
else
entering = traced.front() - plan_of(anchor);
if (false == apart(entering))
return false;
entering = glm::normalize(entering);
// the straight it goes on along: through the last two points, or the one of the free end the line finishes at
auto exit{trace.points.back()};
glm::dvec2 leaving;
if (reaching)
{
exit = End.position;
leaving = glm::normalize(plan_of(End.direction));
}
else
{
leaving = traced.back() - traced[traced.size() - 2];
if (false == apart(leaving))
return false;
leaving = glm::normalize(leaving);
}
if (trace.points.size() < 3 && false == reaching)
{
// no point along the curve itself: the bend where the two straights meet, eased the usual way
glm::dvec2 corner;
if (false == meeting(plan_of(entry), entering, plan_of(exit), -leaving, corner))
{
Error = STR_C("The straight before the curve and the one after it don't meet");
return false;
}
bends.push_back({lifted(corner, entry.y), {}, true});
}
else
{
auto base{design};
base.start = entry;
base.start_direction = entering;
base.end = exit;
base.end_direction = leaving;
// 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)};
if (false == fitted.has_value())
{
Error = STR_C("Fitting the curve to the points...");
return false;
}
auto const &fit{*fitted};
if (false == fit.valid)
{
Error = STR_C("No curve follows the points clicked along it");
return false;
}
for (auto const &vertex : fit.vertices)
bends.push_back({lifted(vertex.position, entry.y), vertex, false});
if (fit.vertices.empty())
{
// a straight: the two lines are one
exit = lifted(plan_of(entry) + entering * glm::dot(plan_of(exit - entry), entering), exit.y);
leaving = entering;
}
trace.offsets = fit.offsets;
trace.rms = fit.rms;
trace.worst = fit.worst;
trace.curves = fit.vertices.size();
}
Plan.traces.push_back(std::move(trace));
anchor = exit;
along = leaving;
}
// the end: the free end reached, the end of a curve traced, or the last point clicked
glm::dvec3 end;
glm::dvec2 ending;
if (joined)
{
end = End.position;
ending = glm::normalize(plan_of(End.direction));
if (along.has_value() && (false == bends.empty() || false == Plan.traces.empty()))
{
// from the straight out of a curve traced to the one of the free end: a bend where they meet
glm::dvec2 corner;
if (meeting(plan_of(anchor), *along, plan_of(end), -ending, corner))
bends.push_back({lifted(corner, anchor.y), {}, true});
}
}
else if (along.has_value())
{
end = anchor;
ending = *along;
}
else
{
for (std::size_t i = 1; i + 1 < Points.size(); ++i)
if (bends.empty())
{
corner.position = plan_of(Points[i]);
design.vertices.push_back(corner);
}
if (design.vertices.size() >= 2)
{
design.vertices.front().offset = std::max(1.0, glm::dot(plan_of(Points[1] - Points.front()), design.start_direction));
design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction));
Error = STR_C("Click at least two points");
return false;
}
end = bends.back().position;
bends.pop_back();
ending = plan_of(end) - plan_of(bends.empty() ? Points.front() : bends.back().position);
if (false == apart(ending))
return false;
ending = glm::normalize(ending);
}
return alignment::compute_recommended(design, true);
if (plan_distance(end, Points.front()) < 1.0)
{
Error = STR_C("Click at least two points");
return false;
}
design.end = end;
design.end_direction = ending;
if (startdirection.has_value())
design.start_direction = *startdirection;
else
{
auto const first{plan_of(bends.empty() ? end : bends.front().position) - plan_of(Points.front())};
if (false == apart(first))
return false;
design.start_direction = glm::normalize(first);
}
for (auto const &bend : bends)
{
auto vertex{bend.corner ? alignment::vertex{} : bend.vertex};
vertex.position = plan_of(bend.position);
design.vertices.push_back(vertex);
Plan.corners.push_back(bend.corner ? 1 : 0);
}
if (design.vertices.size() >= 2)
{
design.vertices.front().offset = glm::dot(plan_of(bends.front().position - design.start), design.start_direction);
design.vertices.back().offset = glm::dot(plan_of(end - bends.back().position), design.end_direction);
}
Plan.shape = alignment::ease_corners(design, Plan.corners, {EditorSettings.corner_low(), EditorSettings.corner_high()});
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 true;
}
std::optional<alignment::trace_stretch> editor_mode::traced_stretch(alignment::design const &Base, std::vector<glm::dvec2> const &Points) const
{
auto const &tool{m_lay};
// the stretches traced before the one the cursor moves along are the same from frame to frame: their fits are kept
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>(tool.radii), tool.straights_between ? 1.0 : 0.0, Base.start_reserve, Base.end_reserve};
for (auto const &point : Points)
{
key.push_back(point.x);
key.push_back(point.y);
}
auto &cache{m_stretch_cache};
for (auto entry{cache.begin()}; entry != cache.end(); ++entry)
if (entry->first == key)
{
std::rotate(cache.begin(), entry, std::next(entry));
return cache.front().second;
}
auto const keep = [&](alignment::trace_stretch const &Fit) {
cache.insert(cache.begin(), {key, Fit});
if (cache.size() > 16)
cache.pop_back();
};
alignment::trace_stretch fit;
if (false == m_follow_preview)
{
fit = alignment::fit_trace_stretch(Base, Points, tool.radii, tool.straights_between);
keep(fit);
return fit;
}
// shown while the cursor moves: fitted on the thread, the frames meanwhile drawn with the fit done last for the same points but
// the one at the cursor, or with no curve yet
if (m_stretch_fitter.done(key, fit))
{
keep(fit);
return fit;
}
m_stretch_fitter.ask({key, Base, Points, tool.radii, tool.straights_between});
std::vector<double> last;
auto const header{std::size_t{14}};
if (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))
{
auto const common{std::min(last.size(), key.size()) - 2};
if (std::equal(key.begin() + header, key.begin() + common, last.begin() + header))
return fit;
}
return {};
}
void editor_mode::lay_trim()
{
auto &tool{m_lay};
tool.modes.resize(tool.points.size(), lay_mode::straights);
}
void editor_mode::lay_switch_mode()
{
auto &tool{m_lay};
tool.mode = tool.mode == lay_mode::straights ? lay_mode::curve : lay_mode::straights;
tool.status = tool.mode == lay_mode::curve ? STR("Curve: click points along the middle of the track, the last two on the straight after the curve. Tab: back to the straights")
: STR("Straights: each point clicked is a bend of the straights, eased with a large curve. Tab: a curve traced on the photo");
tool.status_time = ImGui::GetTime();
}
void editor_mode::lay_click()
{
auto &tool{m_lay};
tool.status.clear();
lay_trim();
auto const snap{tool.mouse_snap};
if (snap.track != nullptr && false == tool.points.empty() && glm::distance(snap.position, tool.points.front()) > 1.0)
{
// reaching a free end finishes the track there, in line with the path it joins
tool.points.push_back(snap.position);
tool.modes.push_back(tool.mode);
lay_finish(snap);
return;
}
@@ -1092,53 +1375,35 @@ void editor_mode::lay_click()
point = from + (to - from) * along;
}
tool.points.push_back(point);
tool.modes.push_back(tool.mode);
return;
}
// the points only make the line shown: it's laid by Enter, or by a click on a free end
auto const &point{tool.mouse};
if (plan_distance(point, tool.points.back()) < 1.0)
return;
m_typed.clear();
if (tool.points.size() == 1)
{
lay_tool::step const previous{tool.start, tool.points.front()};
tool.points.push_back(tool.mouse);
lay_finish({});
if (false == tool.points.empty())
{
tool.points.pop_back();
return;
}
auto *last{dynamic_cast<TTrack *>(m_node)};
if (last == nullptr || last->m_paths.empty())
return;
auto const &path{last->m_paths.front()};
editor_track::snap_target next;
next.track = last;
next.point = {0, editor_track::point_kind::end};
next.position = path.points[segment_data::point::end];
auto const &control{path.points[segment_data::point::control2]};
next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position);
tool.chain.push_back(previous);
tool.start = next;
tool.points = {next.position};
return;
}
tool.points.push_back(point);
tool.modes.push_back(tool.mode);
}
void editor_mode::lay_finish(editor_track::snap_target const &End)
{
auto &tool{m_lay};
lay_trim();
double length;
std::string error;
auto const pieces{lay_pieces(tool.points, End, length, error)};
auto const pieces{lay_pieces(tool.points, tool.modes, End, length, error)};
auto const eased{m_eased_last};
tool.status_time = ImGui::GetTime();
if (pieces.empty())
{
tool.status = error.empty() ? std::string{STR_C("Click at least two points")} : error;
if (End.track != nullptr && false == tool.points.empty())
{
tool.points.pop_back();
lay_trim();
}
return;
}
auto const *style{tool.start.track != nullptr ? tool.start.track : End.track};
@@ -1191,6 +1456,7 @@ void editor_mode::lay_finish(editor_track::snap_target const &End)
m_nmt_state.laying = false;
WriteLog("Editor: " + tool.status, logtype::generic);
tool.points.clear();
tool.modes.clear();
tool.start = {};
straight_refresh();
}
@@ -1203,32 +1469,53 @@ void editor_mode::lay_enter()
lay_click();
return;
}
lay_trim();
if (tool.points.empty())
return;
auto const end{tool.mouse_snap};
if (end.track != nullptr && glm::distance(end.position, tool.points.front()) > 1.0)
{
tool.points.push_back(end.position);
tool.modes.push_back(tool.mode);
lay_finish(end);
return;
}
auto const point{placement_on_ground(tool.mouse)};
bool const appended{plan_distance(point, tool.points.back()) > 1.0};
if (appended)
tool.points.push_back(point);
auto const count{tool.points.size()};
// with no point clicked since the last Enter the laying of the chain ends
if (tool.points.size() < 2)
{
lay_cancel();
return;
}
lay_tool::step const previous{tool.start, tool.points.front()};
lay_finish({});
if (appended && tool.points.size() == count)
tool.points.pop_back();
if (false == tool.points.empty())
return;
// the laying goes on from the end of the track just laid, along it
auto *last{dynamic_cast<TTrack *>(m_node)};
if (last == nullptr || last->m_paths.empty())
return;
auto const &path{last->m_paths.front()};
editor_track::snap_target next;
next.track = last;
next.point = {0, editor_track::point_kind::end};
next.position = path.points[segment_data::point::end];
auto const &control{path.points[segment_data::point::control2]};
next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position);
tool.chain.push_back(previous);
tool.start = next;
tool.points = {next.position};
tool.modes = {tool.mode};
}
void editor_mode::lay_cancel()
{
auto &tool{m_lay};
tool.points.clear();
tool.modes.clear();
tool.start = {};
tool.chain.clear();
tool.preview.clear();
tool.plan = {};
tool.status.clear();
}
@@ -1251,6 +1538,7 @@ void editor_mode::lay_after_history(bool const Undo)
}
tool.start = tool.chain.back().start;
tool.points = {tool.chain.back().point};
tool.modes = {tool.mode};
tool.chain.pop_back();
}
}
@@ -1258,6 +1546,14 @@ void editor_mode::lay_after_history(bool const Undo)
void editor_mode::lay_back()
{
auto &tool{m_lay};
lay_trim();
// the points not laid yet go first, then the tracks laid in the chain
if (tool.points.size() > 1)
{
tool.points.pop_back();
tool.modes.pop_back();
return;
}
if (false == tool.chain.empty())
{
undo_last();
@@ -1265,10 +1561,9 @@ void editor_mode::lay_back()
tool.status_time = ImGui::GetTime();
return;
}
if (false == tool.points.empty())
tool.points.pop_back();
if (tool.points.empty())
tool.start = {};
tool.points.clear();
tool.modes.clear();
tool.start = {};
}
glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const
@@ -1367,11 +1662,71 @@ alignment::limits editor_mode::laying_norms() const
void editor_mode::render_lay_ui()
{
auto &tool{m_lay};
if (int mode{tool.mode == lay_mode::curve ? 1 : 0};
segmented("laymode", mode,
{{0, STR_C("Straights Tab"), "Each point clicked is a bend of the straights: the bend eased with two transition curves meeting, with no cant, of a large radius; where they don't fit, an arc with the transitions and the cant of the norms, as large as fits"},
{1, STR_C("Curve Tab"), "Click points along the middle of a curve seen on the photo, the last two on the straight after it: the curve, compound or with a straight in it, is fitted to them. It comes from the straight before it, of the points clicked before or of the free end"}}) &&
(mode == 1) != (tool.mode == lay_mode::curve))
lay_switch_mode();
ImGui::PushItemWidth(120.0f);
ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f");
item_tooltip("Drag to change, Ctrl+click to type the value. While laying: Ctrl+wheel");
value_chips("radii", tool.radius, {190.0, 300.0, 500.0, 800.0, 1200.0, 2000.0}, "%.0f");
tool.radius = std::max(10.0, tool.radius);
if (tool.mode == lay_mode::straights)
{
double low{EditorSettings.corner_low()};
double high{EditorSettings.corner_high()};
auto changed{ImGui::DragScalar(STR_C("Bends: R from (m)"), ImGuiDataType_Double, &low, 10.0f, nullptr, nullptr, "%.0f")};
item_tooltip("The radius the two transition curves of a bend meet at, at least; where they don't fit at it, an arc of the norms as large as fits");
changed |= ImGui::DragScalar(STR_C("to (m)"), ImGuiDataType_Double, &high, 10.0f, nullptr, nullptr, "%.0f");
item_tooltip("The radius tried first; a bend too slight for transitions long enough at the speed takes a larger one");
ImGui::PushID("bends");
auto first{true};
for (auto const &[from, to] : {std::pair{1000.0, 2000.0}, std::pair{2000.0, 4000.0}, std::pair{3000.0, 6000.0}, std::pair{4000.0, 8000.0}})
{
if (false == first)
ImGui::SameLine();
first = false;
auto const chosen{std::abs(low - from) < 0.5 && std::abs(high - to) < 0.5};
if (chosen)
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
if (ImGui::SmallButton(format("%.0f-%.0f", from, to).c_str()))
{
low = from;
high = to;
changed = true;
}
if (chosen)
ImGui::PopStyleColor();
}
ImGui::PopID();
if (changed)
{
low = std::max(10.0, low);
EditorSettings.corners(low, std::max(low, high));
EditorSettings.save();
}
}
else
{
ImGui::PopItemWidth();
segmented("layradii", tool.radii,
{{0, STR_C("Auto"), "As few radii as follow the points, up to three"}, {1, STR_C("1 R"), "One radius"}, {2, STR_C("2 R"), "A compound curve of two radii"}, {3, STR_C("3 R"), "A compound curve of three radii"}});
ImGui::Checkbox(STR_C("Two curves with a straight between them, where the points show one"), &tool.straights_between);
item_tooltip("The points along the curve may make two curves, turning the same way or not, with a straight between them, where these follow the points much closer than one curve");
if (auto const &traces{tool.plan.traces}; false == traces.empty() && false == traces.back().offsets.empty())
{
auto const &trace{traces.back()};
auto const colour = [](double const Departure) { return Departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : Departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255); };
metric_tiles({{format("%zu", trace.points.size()), STR_C("points"), ImGui::GetColorU32(ImGuiCol_Text)},
{format("%.0f cm", trace.rms * 100.0), STR_C("departure"), colour(trace.rms)},
{format("%.0f cm", trace.worst * 100.0), STR_C("at most"), colour(trace.worst)}});
}
else
{
ImGui::PushTextWrapPos(0.0f);
ImGui::TextDisabled("%s", STR_C("Click points along the middle of the track on the photo, the last two on the straight after the curve"));
ImGui::PopTextWrapPos();
}
ImGui::PushItemWidth(120.0f);
}
ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f");
item_tooltip("Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows");
value_chips("speeds", tool.style.velocity, {40.0, 60.0, 80.0, 100.0, 120.0, 160.0}, "%.0f");
@@ -1391,6 +1746,29 @@ void editor_mode::render_lay_ui()
EditorSettings.heights(static_cast<editorSettings::lay_heights>(heights));
EditorSettings.save();
}
// the curves of the line shown, those below the norms in the colour of a warning: they're laid all the same
if (auto const &plan{tool.plan}; tool.active && plan.shape.valid && false == plan.shape.curves.empty())
{
ImGui::SeparatorText(STR_C("Curves of the track laid"));
for (auto const &curve : plan.shape.curves)
{
auto const meets{alignment::meets_norms(curve, plan.design)};
auto const text{format("%d. %s", curve.vertex + 1, curve_label(curve, laying_norms()).c_str())};
if (meets)
ImGui::TextUnformatted(text.c_str());
else
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.15f, 1.0f), "%s", text.c_str());
if (ImGui::IsItemHovered())
{
std::string notes;
auto const tag{format(STR_C("Vertex %zu"), static_cast<std::size_t>(curve.vertex + 1))};
for (auto const &warning : plan.shape.warnings)
if (warning.rfind(tag + ":", 0) == 0)
notes += (notes.empty() ? "" : "\n") + warning;
ImGui::SetTooltip("%s", notes.empty() ? STR_C("Meets the norms for the speed") : notes.c_str());
}
}
}
if (auto *track{selected_track()}; track != nullptr)
{
std::string reason;
@@ -1402,6 +1780,13 @@ void editor_mode::render_lay_ui()
render_lay_status();
return;
}
ImGui::TextDisabled("%s", STR_C("Dragged from a free end, or laid from a point to one"));
ImGui::PushItemWidth(120.0f);
ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f");
item_tooltip("Drag to change, Ctrl+click to type the value. While dragging: Ctrl+wheel");
value_chips("radii", tool.radius, {190.0, 300.0, 500.0, 800.0, 1200.0, 2000.0}, "%.0f");
tool.radius = std::max(10.0, tool.radius);
ImGui::PopItemWidth();
render_layout_combo();
ImGui::Spacing();
ImGui::PushItemWidth(120.0f);

View File

@@ -1745,13 +1745,21 @@ bool editor_mode::render_route_vertex(int const Index)
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");
ImGui::PushItemWidth(110.0f);
if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound))
if (ImGui::Checkbox(STR_C("Two transition curves, no arc"), &vertex.spirals))
{
changed = true;
if (vertex.spirals)
vertex.compound = false;
}
item_tooltip("The bend eased the way the lines are drawn: two transition curves meeting at the radius, with no arc between them and no cant; their lengths follow from the radius and the angle");
if (false == vertex.spirals && ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound))
{
changed = true;
if (vertex.compound && vertex.arcs.empty())
vertex.arcs.push_back({std::max(10.0, std::round(vertex.radius * 0.6 / 10.0) * 10.0), 0.0, 1.0});
}
item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions");
if (false == vertex.spirals)
item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions");
if (vertex.compound)
{
auto total{std::max(0.0, vertex.share)};

View File

@@ -126,6 +126,13 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c
// layto <x> <z> <x> <z> <layout 0..3> <radius> from the free end at the first point towards the second, as a drag in the layout
// 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
// 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
// laysample <x> <z> <heading> <spacing> <noise> <deflection> <R> [<from the first vertex to the second, m> <deflection> <R>]
// a line of known curves, the points along them clicked as a curve traced, the line they make logged
// laychain <layout> <R> <x> <z> | b | u | r | e | t | p... the points clicked, Backspace, undo, redo, Enter, Tab, the line logged
// joints <x> <z>... the joints check over the scenery, the issues around the points logged, every issue with free ends without points
// route <x> <z> <single 0|1> the route design of the path nearest the point, a curve or the whole line
// routevertex <i> <R> <L in> <L out> <cant>, routearc <i> <R> <L> <share>, routeshare <i> <share>, routekm <km>

View File

@@ -180,6 +180,180 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
WriteLog(format("SELFTEST layspeed: %.0f km/h, own %d", m_lay.style.velocity, m_lay.own_speed ? 1 : 0));
return selftest_result::handled;
}
// the point clicked by the lay tool at x z, on the ground there, a free end within 3 m reached
auto const lay_point = [this](glm::dvec3 Point) {
Point.y = 5000.0;
std::vector<char> found;
auto const ground{ground_heights({Point}, true, &found)};
Point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y;
m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, Point, 3.0, {});
m_lay.mouse = m_lay.mouse_snap.track != nullptr ? Point : lay_bed(Point);
lay_click();
};
// the line the points clicked make now, as Enter would lay it: its vertices and curves, the warnings, the fits of the curves traced
auto const lay_report = [this](std::string const &Label) {
lay_trim();
lay_line_plan plan;
std::string error;
// timed as the first frame the points are clicked in, the fits kept for them dropped
m_stretch_cache.clear();
auto const started{std::chrono::steady_clock::now()};
auto const made{m_lay.points.size() >= 2 && lay_line(m_lay.points, m_lay.modes, {}, plan, error)};
auto const took{std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count()};
WriteLog(format("SELFTEST layplan %s: %s, %zu points, %zu vertices, L %.2f m, %.1f ms %s", Label.c_str(), made ? "valid" : "invalid", m_lay.points.size(), plan.design.vertices.size(), plan.shape.length, took, error.c_str()));
if (false == made)
return;
for (std::size_t k = 0; k < plan.design.vertices.size(); ++k)
{
auto const &vertex{plan.design.vertices[k]};
auto text{format("SELFTEST layplan vertex %zu: %s at %.3f %.3f", k, plan.corners[k] != 0 ? "bend" : "traced", k < plan.shape.vertices.size() ? plan.shape.vertices[k].x : 0.0, k < plan.shape.vertices.size() ? plan.shape.vertices[k].y : 0.0)};
for (auto const &curve : plan.shape.curves)
if (curve.vertex == static_cast<int>(k))
{
text += format(", %s R %.1f", curve.spirals ? "spirals" : "arc", curve.radius);
for (auto const radius : curve.radii)
text += format("/%.1f", radius);
text += format(", L %.1f/%.1f, cant %.0f, deflection %.3f deg, tangents %.1f/%.1f, arc %.1f m, norms %d", curve.transition_in, curve.transition_out, curve.cant, glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length,
alignment::meets_norms(curve, plan.design) ? 1 : 0);
}
WriteLog(text);
}
for (auto const &e : plan.shape.elements)
if (e.kind == alignment::element_kind::straight)
WriteLog(format("SELFTEST layplan straight at %.1f m: %.1f m", e.chainage, e.length));
for (auto const &warning : plan.shape.warnings)
WriteLog("SELFTEST layplan warning: " + warning);
for (auto const &trace : plan.traces)
WriteLog(format("SELFTEST layplan trace: %zu points, %zu curves, departure %.3f m, at most %.3f m", trace.points.size(), trace.curves, trace.rms, trace.worst));
};
if (command == "laymode")
{
std::string which;
words >> which;
if ((which == "curve") != (m_lay.mode == lay_mode::curve))
lay_switch_mode();
WriteLog("SELFTEST laymode: " + std::string{m_lay.mode == lay_mode::curve ? "curve" : "straights"});
return selftest_result::handled;
}
if (command == "layradius")
{
double low{3000.0};
double high{6000.0};
words >> low >> high;
EditorSettings.corners(low, high);
WriteLog(format("SELFTEST layradius: %.0f - %.0f m", EditorSettings.corner_low(), EditorSettings.corner_high()));
return selftest_result::handled;
}
if (command == "laytrace")
{
int straights{1};
words >> m_lay.radii >> straights;
m_lay.straights_between = straights != 0;
WriteLog(format("SELFTEST laytrace: radii %d, straights %d", m_lay.radii, straights));
return selftest_result::handled;
}
if (command == "layp")
{
m_lay.active = true;
for (glm::dvec3 point{0.0}; words >> point.x >> point.z;)
lay_point(point);
WriteLog(format("SELFTEST layp: %zu points, %s", m_lay.points.size(), m_lay.status.c_str()));
return selftest_result::handled;
}
if (command == "layreport")
{
std::string label;
words >> label;
lay_report(label);
return selftest_result::handled;
}
if (command == "layenter" || command == "laycancel" || command == "layback")
{
m_typed.clear();
m_lay.mouse_snap = {};
if (command == "layenter")
lay_enter();
else if (command == "layback")
lay_back();
else
lay_cancel();
WriteLog(format("SELFTEST %s: %zu points, chain %zu, history %zu, %s", command.c_str(), m_lay.points.size(), m_lay.chain.size(), m_history.size(), m_lay.status.c_str()));
return selftest_result::handled;
}
// a line of known curves, turning by the deflections given (degrees, to the left positive), the points along its curves clicked as
// they would be on the photo: the start as a bend, then in the mode of a curve from 30 m before the first curve to the end of the
// last one and two on the straight after it, every Spacing metres, shifted across by up to Noise
if (command == "laysample")
{
double x{0.0}, z{0.0}, heading{0.0}, spacing{20.0}, noise{0.0}, deflection{30.0}, radius{1000.0};
words >> x >> z >> heading >> spacing >> noise >> deflection >> radius;
double apart{0.0}, second{0.0}, secondradius{0.0};
auto const two{static_cast<bool>(words >> apart >> second >> secondradius)};
alignment::design truth;
truth.norms = laying_norms();
truth.shape = m_route.design.shape;
truth.speed = design_speed(nullptr, nullptr);
glm::dvec2 const start{x, z};
glm::dvec2 direction{std::cos(glm::radians(heading)), std::sin(glm::radians(heading))};
auto const corner1{start + direction * 400.0};
direction = geometry::turned(direction, glm::radians(deflection));
auto corner2{corner1};
if (two)
{
corner2 = corner1 + direction * apart;
direction = geometry::turned(direction, glm::radians(second));
}
auto const end{corner2 + direction * 400.0};
truth.start = {start.x, 0.0, start.y};
truth.end = {end.x, 0.0, end.y};
truth.start_direction = glm::normalize(corner1 - start);
truth.end_direction = direction;
auto const curve_of = [&](double const Radius) {
alignment::vertex vertex;
vertex.radius = Radius;
auto const recommended{alignment::recommend(std::min(truth.speed, alignment::speed_limit(Radius, truth.norms)), Radius, truth.norms)};
vertex.transition_in = vertex.transition_out = recommended.transition;
vertex.cant = recommended.cant;
vertex.offset = 400.0;
return vertex;
};
truth.vertices.push_back(curve_of(radius));
if (two)
truth.vertices.push_back(curve_of(secondradius));
auto const result{alignment::compute(truth)};
for (auto const &curve : result.curves)
WriteLog(format("SELFTEST laysample truth %d: R %.1f, L %.1f/%.1f, cant %.0f, deflection %.3f deg, valid %d", curve.vertex, curve.radius, curve.transition_in, curve.transition_out, curve.cant, glm::degrees(curve.deflection), result.valid ? 1 : 0));
if (false == result.valid)
return selftest_result::handled;
auto from{result.length};
auto to{0.0};
for (auto const &e : result.elements)
if (e.kind != alignment::element_kind::straight)
{
from = std::min(from, e.chainage);
to = std::max(to, e.chainage + e.length);
}
m_lay.active = true;
lay_cancel();
if (m_lay.mode != lay_mode::straights)
lay_switch_mode();
auto const along = [&](double const Chainage, double const Shift) {
auto const station{alignment::evaluate(result, Chainage)};
glm::dvec3 const across{-station.direction.z, 0.0, station.direction.x};
return station.position + glm::normalize(across) * Shift;
};
lay_point(truth.start);
lay_switch_mode();
int count{0};
for (auto chainage{std::max(0.0, from - 30.0)}; chainage <= to; chainage += spacing, ++count)
lay_point(along(chainage, noise * (count % 3 == 0 ? 1.0 : count % 3 == 1 ? -0.6 : 0.2)));
lay_point(along(to + 30.0, noise * 0.5));
lay_point(along(to + 60.0, -noise * 0.5));
lay_switch_mode();
WriteLog(format("SELFTEST laysample: %zu points clicked over %.1f m of curves", m_lay.points.size(), to - from));
lay_report("sample");
return selftest_result::handled;
}
if (command == "tracktab")
{
std::string key;
@@ -210,16 +384,20 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
undo_last();
else if (word == "r")
redo_last();
else if (word == "e")
{
m_lay.mouse_snap = {};
lay_enter();
}
else if (word == "t")
lay_switch_mode();
else if (word == "p")
lay_report("chain");
else
{
glm::dvec3 point{std::strtod(word.c_str(), nullptr), 5000.0, 0.0};
words >> point.z;
std::vector<char> found;
auto const ground{ground_heights({point}, true, &found)};
point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y;
m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {});
m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point);
lay_click();
lay_point(point);
word += format(" %.1f", point.z);
}
report(word);
@@ -253,9 +431,12 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
WriteLog(format("SELFTEST layclicks ground %.3f, track %.3f", point.y, m_lay.mouse.y));
if (false == m_lay.points.empty())
{
lay_trim();
auto points{m_lay.points};
auto modes{m_lay.modes};
modes.push_back(m_lay.mode);
points.push_back(m_lay.mouse_snap.track != nullptr ? m_lay.mouse_snap.position : m_lay.mouse);
m_lay.preview = lay_pieces(points, m_lay.mouse_snap, m_lay.preview_length, m_lay.preview_error);
m_lay.preview = lay_pieces(points, modes, m_lay.mouse_snap, m_lay.preview_length, m_lay.preview_error);
auto readout{track_readout()};
std::replace(readout.begin(), readout.end(), '\n', '|');
WriteLog("SELFTEST layclicks readout: " + readout);
@@ -263,6 +444,11 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
lay_click();
WriteLog(format("SELFTEST layclicks %.1f %.1f: %s", point.x, point.z, m_lay.status.c_str()));
}
// the points clicked are laid at one go, as Enter lays them
m_typed.clear();
m_lay.mouse_snap = {};
lay_enter();
WriteLog("SELFTEST layclicks enter: " + m_lay.status);
lay_cancel();
m_lay.active = active;
glm::dvec2 heading{0.0};

View File

@@ -485,6 +485,8 @@ void editor_mode::show_track_tab(track_tab const Wanted)
cancel_track_tools();
m_straights.tool = 0;
m_lay.points.clear();
m_lay.modes.clear();
m_lay.plan = {};
m_lay.start = {};
m_lay.chain.clear();
m_lay.preview.clear();
@@ -1654,15 +1656,17 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
auto const *selected{selected_track()};
if (m_lay.active)
{
bool const curve{m_lay.mode == lay_mode::curve};
auto const *tab{curve ? "back to the bends of straights" : "trace a curve on the photo"};
if (m_lay.points.empty())
hints = {{"LMB", "start, on the ground or at a free end"}, {"Drag from a free end", "lays the shape to the cursor"}, {"Shift+click a straight", "a parallel track (N)"}, {"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"Esc", "stop laying"}};
hints = {{"LMB", "start, on the ground or at a free end"}, {"Tab", tab}, {"Drag from a free end", "lays the shape to the cursor"}, {"Shift+click a straight", "a parallel track (N)"}, {"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"Esc", "stop laying"}};
else
{
bool const chained{m_lay.start.track != nullptr};
hints = {{"LMB", chained ? "lay the shape up to here" : "lay the straight up to here"}, {"LMB on a free end", "join and lay"}};
if (chained || m_lay.mouse_snap.track != nullptr)
hints = {{"LMB", curve ? "a point along the middle of the curve, the last two on the straight after it" : "a bend of the straights, eased with a large curve"}, {"LMB on a free end", "join and lay"}, {"Tab", tab}};
if (m_lay.points.size() == 1 && m_lay.mouse_snap.track != nullptr)
hints.insert(hints.end(), {{"Ctrl+wheel", "radius"}, {"Shift+wheel", "another shape"}});
hints.insert(hints.end(), {{"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"Enter", "lay up to the cursor"}, {"Backspace", m_lay.chain.empty() ? "take back the point" : "take back the last piece"}, {"Esc", "cancel"}});
hints.insert(hints.end(), {{"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"Enter", m_lay.points.size() > 1 ? "lay the points clicked, then go on" : "stop laying"},
{"Backspace", m_lay.points.size() > 1 || m_lay.chain.empty() ? "take back the point" : "take back the last piece"}, {"Esc", "drop the points not laid"}});
}
}
else if (false == m_straights.detour.empty())
@@ -1864,6 +1868,28 @@ std::string editor_mode::track_readout() const
std::string text;
if (lay.points.empty())
text = lay.mouse_snap.track != nullptr ? format(STR_C("starts at the end of %s"), name(lay.mouse_snap.track).c_str()) : std::string{STR_C("start")};
else if (false == lay.preview.empty() && lay.plan.shape.valid)
{
// the curve the cursor makes now: of the last bend, or the one traced
auto const &plan{lay.plan};
text = format("L %.1f m", lay.preview_length);
if (false == plan.shape.curves.empty())
{
auto const &curve{plan.shape.curves.back()};
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");
}
if (lay.mode == lay_mode::curve && false == plan.traces.empty() && false == plan.traces.back().offsets.empty())
{
auto const &trace{plan.traces.back()};
text += "\n" + format(STR_C("%zu points, departure %.0f cm, at most %.0f cm"), trace.points.size(), trace.rms * 100.0, trace.worst * 100.0);
}
auto const [from, to]{laying_ends()};
text += "\n" + design_speed_of(from, to);
if (auto const vertical{vertical_of(lay.preview)}; false == vertical.empty())
text += "\n" + vertical;
}
else if (false == lay.preview.empty())
{
double radius{0.0};
@@ -1895,8 +1921,9 @@ std::string editor_mode::track_readout() const
text += "\n" + format(STR_C("joins %s"), name(lay.mouse_snap.track).c_str());
if (false == lay.preview_error.empty())
text += "\n" + lay.preview_error;
if (lay.points.size() == 1 && (lay.start.track != nullptr || lay.mouse_snap.track != nullptr))
text += "\n" + track_shaping(lay.mouse_snap.track != nullptr && lay.start.track != nullptr);
if (lay.points.size() == 1 && lay.mouse_snap.track != nullptr)
text += "\n" + track_shaping(lay.start.track != nullptr);
text += "\n" + std::string{lay.mode == lay_mode::curve ? STR_C("Curve traced, Tab: the bends of straights") : STR_C("Bends of straights, Tab: a curve traced")};
if (ImGui::GetIO().KeyAlt && false == lay.points.empty())
text += "\n" + (lay.skew.empty() ? format(STR_C("azimuth %.0f deg"), azimuth(lay.points.back(), lay.mouse)) : lay.skew);
if (false == lay.status.empty() && ImGui::GetTime() - lay.status_time < 4.0)
@@ -2342,14 +2369,43 @@ void editor_mode::draw_build_overlay() const
if (m_lay.active)
{
auto const &lay{m_lay};
auto const &plan{lay.plan};
auto const piece_colour = [&](segment_data const &Path) {
if (false == lay.preview_error.empty())
if (false == lay.preview_error.empty() && plan.shape.warnings.empty())
return overlay_color::warning;
return piece_plan_radius(Path) < 0.5 ? IM_COL32(255, 255, 255, 230) : overlay_color::marked;
};
for (auto const &piece : lay.preview)
drawpath(piece, piece_colour(piece));
for (std::size_t i = 0, n = lay.preview.size(); i < n;)
auto const label_at = [&](glm::dvec3 const &Point, std::string const &Text, ImU32 const Colour) {
ImVec2 screen;
if (false == projection.project(Point, screen))
return;
auto const size{ImGui::CalcTextSize(Text.c_str())};
drawlist->AddRectFilled(ImVec2(screen.x + 6.0f, screen.y - 8.0f), ImVec2(screen.x + 14.0f + size.x, screen.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f);
drawlist->AddText(ImVec2(screen.x + 10.0f, screen.y - 8.0f), Colour, Text.c_str());
};
// the straights the next bend slides along, dashed
for (auto const &[from, to] : plan.guides)
{
auto const length{plan_distance(from, to)};
auto const dashes{std::clamp(static_cast<int>(length / 4.0), 1, 400)};
for (int k = 0; k < dashes; k += 2)
projection.line(drawlist, glm::mix(from, to, static_cast<double>(k) / dashes), glm::mix(from, to, static_cast<double>(k + 1) / dashes), IM_COL32(255, 255, 255, 150), 1.5f);
}
// the curves of the line laid by the points: the bends eased, the curves traced, in the colour of a warning where they don't meet
// the norms; the length of the straights between them
if (plan.shape.valid)
{
for (auto const &curve : plan.shape.curves)
if (curve.vertex >= 0 && curve.vertex < static_cast<int>(plan.shape.vertex_chainages.size()))
label_at(alignment::evaluate(plan.shape, plan.shape.vertex_chainages[curve.vertex]).position, curve_label(curve, laying_norms()),
alignment::meets_norms(curve, plan.design) ? overlay_color::marked : overlay_color::warning);
for (auto const &e : plan.shape.elements)
if (e.kind == alignment::element_kind::straight && e.length >= 20.0)
label_at(alignment::evaluate(plan.shape, e.chainage + e.length * 0.5).position, format(STR_C("straight %.0f m"), e.length), overlay_color::highlight);
}
for (std::size_t i = 0, n = plan.shape.valid ? 0 : lay.preview.size(); i < n;)
{
auto const radius{piece_plan_radius(lay.preview[i])};
bool const curved{radius >= 0.5};
@@ -2390,14 +2446,35 @@ void editor_mode::draw_build_overlay() const
for (auto const &[colour, width] : {std::pair{IM_COL32(0, 0, 0, 120), 3.0f}, std::pair{IM_COL32(255, 255, 255, 160), 1.0f}})
for (std::size_t i = 0; i + 1 < lay.points.size(); ++i)
projection.line(drawlist, lay.points[i], lay.points[i + 1], colour, width);
for (auto const &point : lay.points)
// the bends of straights as squares, the points along a curve as dots in the colour of how far the line laid is from them
auto const departure = [&](glm::dvec3 const &Point) {
for (auto const &trace : plan.traces)
for (std::size_t k = 0; k < trace.points.size() && k < trace.offsets.size(); ++k)
if (glm::distance(trace.points[k], Point) < 1e-6)
return std::abs(trace.offsets[k]);
return -1.0;
};
for (std::size_t i = 0; i < lay.points.size(); ++i)
{
ImVec2 screen;
if (projection.project(point, screen))
if (false == projection.project(lay.points[i], screen))
continue;
if (i > 0 && i < lay.modes.size() && lay.modes[i] == lay_mode::curve)
{
drawlist->AddCircleFilled(screen, 5.0f, overlay_color::grip);
drawlist->AddCircle(screen, 5.0f, IM_COL32(0, 0, 0, 170), 12, 1.5f);
auto const off{departure(lay.points[i])};
auto const colour{off < 0.0 ? overlay_color::grip : off <= 0.1 ? IM_COL32(90, 220, 110, 255) : off <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255)};
drawlist->AddCircleFilled(screen, 4.5f, colour);
drawlist->AddCircle(screen, 4.5f, IM_COL32(0, 0, 0, 170), 12, 1.5f);
continue;
}
drawlist->AddRectFilled(ImVec2(screen.x - 4.5f, screen.y - 4.5f), ImVec2(screen.x + 4.5f, screen.y + 4.5f), overlay_color::grip);
drawlist->AddRect(ImVec2(screen.x - 4.5f, screen.y - 4.5f), ImVec2(screen.x + 4.5f, screen.y + 4.5f), IM_COL32(0, 0, 0, 170), 0.0f, 0, 1.5f);
}
// the point the cursor would add along a curve
if (ImVec2 cursor; lay.mode == lay_mode::curve && false == lay.points.empty() && lay.mouse_snap.track == nullptr && projection.project(lay.mouse, cursor))
{
auto const off{departure(lay.mouse)};
drawlist->AddCircle(cursor, 6.0f, off < 0.0 ? overlay_color::grip : off <= 0.1 ? IM_COL32(90, 220, 110, 255) : off <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255), 16, 2.0f);
}
ImVec2 screen;
if (lay.mouse_snap.track != nullptr && projection.project(lay.mouse_snap.position, screen))

View File

@@ -70,6 +70,33 @@ inline std::string speed_of(double const Radius, alignment::limits const &Limits
return speed > 0.0 ? format(STR_C(" (up to %.0f km/h)"), speed) : std::string{};
}
// the sharpest arc of a curve, of a compound one too
inline double curve_sharpest(alignment::curve_report const &Curve)
{
auto sharpest{Curve.radius};
for (auto const radius : Curve.radii)
sharpest = std::min(sharpest, radius);
return sharpest;
}
// a curve of the line laid in short: two transition curves meeting at the radius, or the radii with the transitions and the cant, the
// speed the sharpest arc lets the trains through at
inline std::string curve_label(alignment::curve_report const &Curve, alignment::limits const &Limits)
{
if (Curve.spirals)
return format("2xL %.0f m R %.0f", Curve.transition_in, Curve.radius) + speed_of(Curve.radius, Limits);
std::string text{"R "};
if (Curve.radii.empty())
text += format("%.0f", Curve.radius);
for (std::size_t i = 0; i < Curve.radii.size(); ++i)
text += format(i == 0 ? "%.0f" : "/%.0f", Curve.radii[i]);
if (Curve.transition_in > 0.0 || Curve.transition_out > 0.0)
text += std::abs(Curve.transition_in - Curve.transition_out) < 0.5 ? format(" L %.0f", Curve.transition_in) : format(" L %.0f/%.0f", Curve.transition_in, Curve.transition_out);
if (Curve.cant > 0.0)
text += format(" h %.0f", Curve.cant);
return text + speed_of(curve_sharpest(Curve), Limits);
}
// 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

@@ -2228,9 +2228,6 @@ msgstr "odznacza"
msgid "Enter"
msgstr "Enter"
msgid "lay up to the cursor"
msgstr "układa do kursora"
msgid "Shift"
msgstr "Shift"
@@ -3984,9 +3981,6 @@ msgstr "Skróty klawiszowe"
msgid "Lay track / switch / turntable"
msgstr "Układanie / Rozjazd / Obrotnica"
msgid "Laying track: lay up to the cursor / take the last piece back"
msgstr "Układanie toru: ułóż do kursora / cofnij ostatni odcinek"
msgid "Leave the editor"
msgstr "Wyjście z edytora"
@@ -4533,9 +4527,6 @@ msgstr "Pokazuj po otwarciu edytora"
msgid "L, LMB, LMB..."
msgstr "L, LPM, LPM..."
msgid "Lay track: each click lays a piece and goes on from its end"
msgstr "Układanie toru: każdy klik układa odcinek i ciągnie tor dalej z jego końca"
msgid "Lead a track out of it; onto another free end: join the two"
msgstr "Wyprowadza z niego tor; na inny wolny koniec: łączy oba"
@@ -4548,9 +4539,6 @@ msgstr "Układanie: łuk i prosta, prosta i łuk, łuk, prosta; na wolnym końcu
msgid "0-9 while laying"
msgstr "0-9 w trakcie układania"
msgid "Type the length (or the radius) of the next piece, Enter lays it"
msgstr "Wpisz długość (lub promień) następnego odcinka, Enter go układa"
msgid "canyon"
msgstr "jar"
@@ -4626,18 +4614,9 @@ msgstr "Więcej"
msgid "Less"
msgstr "Mniej"
msgid "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked"
msgstr "Zwykłe łuki bez krzywych przejściowych i przechyłki, bez ostrzeżeń o normach: tor idzie tam, gdzie klikniesz"
msgid "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"
msgstr "Krzywe przejściowe i przechyłka dla prędkości projektowej; ostrzeżenia, gdy promień, przechyłka albo krzywe przejściowe nie spełniają norm"
msgid "Drag to change, Ctrl+click to type the value. While laying: Ctrl+wheel"
msgstr "Przeciągnij, by zmienić, Ctrl+LPM, by wpisać. W trakcie układania: Ctrl+kółko"
msgid "Speed limit of the new paths; the speed of the track joined comes first"
msgstr "Ograniczenie prędkości nowych torów; pierwszeństwo ma prędkość dołączanego toru"
msgid "The Parallel tool (N) with it"
msgstr "Narzędzie Równoległy (N) z tym torem"
@@ -4731,12 +4710,6 @@ msgstr "układa kształt do kursora"
msgid "a parallel track (N)"
msgstr "tor równoległy (N)"
msgid "lay the shape up to here"
msgstr "układa kształt do tego miejsca"
msgid "lay the straight up to here"
msgstr "układa prostą do tego miejsca"
msgid "another shape"
msgstr "inny kształt"
@@ -7001,18 +6974,6 @@ msgstr "Wyposażenie"
msgid "F1: all the shortcuts"
msgstr "F1: wszystkie skróty"
msgid "Free laying"
msgstr "Swobodnie"
msgid "Plain arcs, no transition curves nor cant"
msgstr "Same łuki, bez krzywych przejściowych i przechyłki"
msgid "To the PKP PLK norms"
msgstr "Według norm PKP PLK"
msgid "Transition curves and cant for the speed, checked against the norms"
msgstr "Krzywe przejściowe i przechyłka dobrane do prędkości"
msgid "its properties show here"
msgstr "jego właściwości pokazują się tutaj"
@@ -10109,9 +10070,6 @@ msgstr "Przechyłka (mm)"
msgid "Cant at the start and at the end: how much higher one rail is, + raises the left rail looking along the path"
msgstr "Przechyłka na początku i na końcu: o ile jedna szyna jest wyżej, + podnosi lewą szynę patrząc wzdłuż toru"
msgid "Plain arcs: the free design has no transition curves, the PKP PLK norms work them out from the speed of the line"
msgstr "Zwykłe łuki: projekt wolny nie ma krzywych przejściowych, normy PKP PLK liczą je z prędkości linii"
msgid "The place lies outside of the terrain model of Poland: check the location of the scenery"
msgstr "To miejsce leży poza NMT Polski: sprawdź położenie scenerii"
@@ -10262,24 +10220,6 @@ msgstr "LPM: początek pomiaru. M albo Esc: koniec"
msgid "Measure"
msgstr "Miarka"
msgid "How to lay the tracks###normsprompt"
msgstr "Jak kłaść tory###normsprompt"
msgid "Choose how the new tracks are laid. It can be changed any time at the top of the panels of laying and of the plan."
msgstr "Wybierz, jak mają się kłaść nowe tory. Zmienisz to w każdej chwili na górze paneli Układanie i Plan."
msgid "Remember the choice, don't ask again"
msgstr "Zapamiętaj wybór, nie pytaj więcej"
msgid "Asking again can be switched on in the settings of the editor (General)"
msgstr "Pytanie można włączyć z powrotem w ustawieniach edytora (Ogólne)"
msgid "Laying of new tracks"
msgstr "Kładzenie nowych torów"
msgid "Ask as the laying starts"
msgstr "Pytaj przy rozpoczęciu kładzenia"
msgid "Trace the curve on the photo"
msgstr "Odrysuj łuk ze zdjęcia"
@@ -11840,3 +11780,125 @@ msgstr "Jak w eu07.ini"
msgid "Sets the model of the timbers, with the fastenings where it has them, on all the switches and crossings of the scenery, in one step of the undo"
msgstr "Ustawia model podrozjazdnic, razem z przytwierdzeniami tam, gdzie je ma, na wszystkich rozjazdach i skrzyżowaniach scenerii, jednym krokiem cofania"
# scenery editor: laying by the bends of straights and by curves traced on the photo
msgid "Vertex %zu: the transition curves meet at R %.0f m, with no cant at least %.0f m for %.0f km/h"
msgstr "Wierzchołek %zu: krzywe przejściowe stykają się przy R %.0f m, bez przechyłki potrzeba co najmniej %.0f m dla %.0f km/h"
msgid "Vertex %zu"
msgstr "Wierzchołek %zu"
msgid "The straight before the curve and the one after it don't meet"
msgstr "Prosta przed łukiem i prosta za nim się nie przecinają"
msgid "No curve follows the points clicked along it"
msgstr "Żaden łuk nie przechodzi przez punkty kliknięte wzdłuż niego"
msgid "Curve: click points along the middle of the track, the last two on the straight after the curve. Tab: back to the straights"
msgstr "Łuk: klikaj punkty po środku toru, dwa ostatnie na prostej za łukiem. Tab: z powrotem proste"
msgid "Straights: each point clicked is a bend of the straights, eased with a large curve. Tab: a curve traced on the photo"
msgstr "Proste: każdy kliknięty punkt to załamanie prostych, wyokrąglone dużym łukiem. Tab: łuk odrysowany ze zdjęcia"
msgid "Straights Tab"
msgstr "Proste Tab"
msgid "Curve Tab"
msgstr "Łuk Tab"
msgid "Each point clicked is a bend of the straights: the bend eased with two transition curves meeting, with no cant, of a large radius; where they don't fit, an arc with the transitions and the cant of the norms, as large as fits"
msgstr "Każdy kliknięty punkt to załamanie prostych: wyokrąglone dwiema stycznymi krzywymi przejściowymi bez przechyłki, o dużym promieniu; gdzie się nie mieszczą, łukiem z krzywymi przejściowymi i przechyłką według norm, tak dużym, jak się mieści"
msgid "Click points along the middle of a curve seen on the photo, the last two on the straight after it: the curve, compound or with a straight in it, is fitted to them. It comes from the straight before it, of the points clicked before or of the free end"
msgstr "Klikaj punkty po środku łuku widocznego na zdjęciu, dwa ostatnie na prostej za nim: łuk, także koszowy albo z prostą w środku, zostanie do nich dopasowany. Wychodzi z prostej przed nim, wyznaczonej punktami klikniętymi wcześniej albo wolnym końcem"
msgid "Bends: R from (m)"
msgstr "Załamania: R od (m)"
msgid "to (m)"
msgstr "do (m)"
msgid "The radius the two transition curves of a bend meet at, at least; where they don't fit at it, an arc of the norms as large as fits"
msgstr "Promień, przy którym stykają się dwie krzywe przejściowe załamania, co najmniej; gdzie się przy nim nie mieszczą, łuk według norm tak duży, jak się mieści"
msgid "The radius tried first; a bend too slight for transitions long enough at the speed takes a larger one"
msgstr "Promień próbowany najpierw; załamanie zbyt małe na krzywe przejściowe dość długie dla prędkości dostaje większy"
msgid "Two curves with a straight between them, where the points show one"
msgstr "Dwa łuki z prostą między nimi, gdy punkty ją pokazują"
msgid "The points along the curve may make two curves, turning the same way or not, with a straight between them, where these follow the points much closer than one curve"
msgstr "Punkty wzdłuż łuku mogą dać dwa łuki, w tę samą stronę albo przeciwne, z prostą między nimi, gdy trzymają się punktów dużo lepiej niż jeden łuk"
msgid "Click points along the middle of the track on the photo, the last two on the straight after the curve"
msgstr "Klikaj punkty po środku toru na zdjęciu, dwa ostatnie na prostej za łukiem"
msgid "Curves of the track laid"
msgstr "Łuki kładzionego toru"
msgid "Meets the norms for the speed"
msgstr "Spełnia normy dla prędkości"
msgid "Dragged from a free end, or laid from a point to one"
msgstr "Przeciąganie z wolnego końca albo kładzenie od punktu do wolnego końca"
msgid "Drag to change, Ctrl+click to type the value. While dragging: Ctrl+wheel"
msgstr "Przeciągnij, aby zmienić, Ctrl+klik, aby wpisać wartość. W trakcie przeciągania: Ctrl+kółko"
msgid "straight %.0f m"
msgstr "prosta %.0f m"
msgid "this curve doesn't meet the norms, it's laid all the same"
msgstr "ten łuk nie spełnia norm, zostanie położony mimo to"
msgid "%zu points, departure %.0f cm, at most %.0f cm"
msgstr "%zu punktów, odchyłka %.0f cm, najwięcej %.0f cm"
msgid "Curve traced, Tab: the bends of straights"
msgstr "Łuk odrysowywany, Tab: załamania prostych"
msgid "Bends of straights, Tab: a curve traced"
msgstr "Załamania prostych, Tab: łuk odrysowywany"
msgid "back to the bends of straights"
msgstr "z powrotem załamania prostych"
msgid "trace a curve on the photo"
msgstr "odrysuj łuk ze zdjęcia"
msgid "a point along the middle of the curve, the last two on the straight after it"
msgstr "punkt po środku łuku, dwa ostatnie na prostej za nim"
msgid "a bend of the straights, eased with a large curve"
msgstr "załamanie prostych, wyokrąglone dużym łukiem"
msgid "lay the points clicked, then go on"
msgstr "kładzie kliknięte punkty i prowadzi dalej"
msgid "drop the points not laid"
msgstr "porzuca niepołożone punkty"
msgid "Lay track: each click a bend of the straights, eased with a large curve; Enter lays them and goes on from the end"
msgstr "Układanie toru: każdy klik to załamanie prostych, wyokrąglone dużym łukiem; Enter je kładzie i prowadzi tor dalej od końca"
msgid "Tab while laying"
msgstr "Tab przy układaniu"
msgid "The points clicked next go along the middle of a curve seen on the photo, the last two on the straight after it; Tab again: back to the bends"
msgstr "Następne punkty idą po środku łuku widocznego na zdjęciu, dwa ostatnie na prostej za nim; ponownie Tab: z powrotem załamania"
msgid "Type the length of the next straight, Enter puts the point"
msgstr "Wpisz długość następnej prostej, Enter stawia punkt"
msgid "Laying track: lay the points clicked / take back the last point, then the last piece laid"
msgstr "Układanie toru: połóż kliknięte punkty / cofnij ostatni punkt, potem ostatni położony odcinek"
msgid "Fitting the curve to the points..."
msgstr "Dopasowuję łuk do punktów..."
msgid "Two transition curves, no arc"
msgstr "Dwie krzywe przejściowe, bez łuku"
msgid "The bend eased the way the lines are drawn: two transition curves meeting at the radius, with no arc between them and no cant; their lengths follow from the radius and the angle"
msgstr "Załamanie wyokrąglone tak, jak projektuje się linie: dwie krzywe przejściowe stykające się przy promieniu, bez łuku między nimi i bez przechyłki; ich długości wynikają z promienia i kąta"