17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 12:09:19 +02:00

feat(editor): two free ends joined without loops or forced curves, the shape from a free end on the wheel

- the join of two free ends (a drag from one onto the other, or a click of the lay tool on it) tries in turn: a straight when the joints check can't see the kinks (half its angle), one circular arc through both ends (a piece cut out of a curve comes back as it was, its cant kept), a curve with straights where the tangents meet, a reverse curve, and only then a free Bezier curve; a curve turning more than it needs to, sharper than 10 m or longer than three chords is never taken
- the transition curves eased out of a curved neighbour only when there's room for them between the ends, never crossing each other into a loop
- cant and transition curves recommended for the radius the curve really gets, at the speed of the joined track or of the tool and never above what the radius allows; the circular arc keeps at least max(20 m, 0.4 V) between the transitions, the cant lowered to what the shortened transitions can ramp, transitions under 1 m left out
- shapes from a free end: arc then straight, straight then arc, arc through the cursor, straight; Ctrl+wheel steps the radius, Shift+wheel the shape (at a free end: the other joins), the plain wheel still zooms; a click of the lay tool from a free end lays the shape and goes on from its end
- self-test commands join, layto and joints; Polish translation
This commit is contained in:
maj00r
2026-10-11 11:11:21 +02:00
parent 3ed864812f
commit 16c1ac4c7b
8 changed files with 860 additions and 76 deletions

View File

@@ -17,6 +17,7 @@ http:
#include <cmath>
#include <cstdio>
#include <functional>
#include <limits>
namespace alignment
{
@@ -378,6 +379,7 @@ result fit_between(design const &Design)
double tangent_in{0.0};
double tangent_out{0.0};
double deflection{0.0};
double cant{0.0};
bool compound{false};
std::vector<compound_arc> arcs;
std::function<void(double, double &, double &)> tangents;
@@ -518,6 +520,44 @@ result fit_between(design const &Design)
}
r.warnings.emplace_back(format("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends", k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out));
}
for (std::size_t k = 0; k < count; ++k)
{
auto &fit{fits[k]};
auto const &vertex{Design.vertices[k]};
fit.cant = std::max(0.0, vertex.cant);
if (fit.compound || fit.deflection < 1e-7 || fit.transition_in + fit.transition_out <= 0.0)
continue;
auto const angles = [&](double const Factor) {
return transition_point(Design.shape, fit.transition_in * Factor, fit.radius, 1.0).angle + transition_point(Design.shape, fit.transition_out * Factor, fit.radius, 1.0).angle;
};
auto const minimum{std::min(fit.radius * fit.deflection, std::max(Design.norms.arc_min, Design.norms.arc_min_factor * Design.speed))};
if (fit.radius * (fit.deflection - angles(1.0)) < minimum - 1e-6)
{
auto const target{fit.deflection - minimum / fit.radius};
double low{0.0}, high{1.0};
for (int i = 0; i < 50; ++i)
{
auto const middle{(low + high) * 0.5};
(angles(middle) > target ? high : low) = middle;
}
fit.transition_in *= low;
fit.transition_out *= low;
r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to %.1f / %.1f m, the circular arc keeps %.0f m", k + 1, fit.transition_in, fit.transition_out, minimum));
}
if (fit.transition_in < 1.0)
fit.transition_in = 0.0;
if (fit.transition_out < 1.0)
fit.transition_out = 0.0;
auto const shortest{std::min(fit.transition_in, fit.transition_out)};
if (fit.cant <= 0.0 || (fit.transition_in >= vertex.transition_in - 1e-6 && fit.transition_out >= vertex.transition_out - 1e-6))
continue;
auto const speed{std::max(1.0, Design.speed)};
auto const ramp{std::min(shortest * 1000.0 / (Design.norms.ramp_factor * speed), Design.norms.cant_rate * 3.6 * shortest / speed)};
if (ramp >= fit.cant)
continue;
fit.cant = ramp < 5.0 ? 0.0 : std::floor(ramp / 5.0) * 5.0;
r.warnings.emplace_back(format("Vertex %zu: cant lowered to %.0f mm, the shortened transition curves can't ramp more", k + 1, fit.cant));
}
std::vector<double> vertexchainage(count, 0.0);
glm::dvec2 cursor{start};
@@ -650,7 +690,7 @@ result fit_between(design const &Design)
curve.vertex = static_cast<int>(k);
curve.radius = radius;
curve.turn = side;
curve.cant = std::max(0.0, vertex.cant);
curve.cant = fit.cant;
if (lengthin > 0.0)
{
curve.kind = element_kind::transition_in;
@@ -804,6 +844,227 @@ result compute(design const &Design)
return r;
}
double speed_limit(double const Radius, limits const &Limits)
{
if (Radius <= 0.0)
return 0.0;
auto const factor{Limits.gauge / (kGravity * 12.96)};
auto const allowance{Limits.gauge * Limits.unbalanced / kGravity};
return std::sqrt(Radius * (Limits.cant_max + allowance) / factor);
}
result compute_recommended(design &Design, bool const Transitions)
{
for (auto &vertex : Design.vertices)
{
vertex.cant = 0.0;
vertex.transition_in = vertex.transition_out = 0.0;
}
auto r{compute(Design)};
if (false == Transitions || false == r.valid)
return r;
auto const speed{Design.speed};
for (int i = 0; i < 3; ++i)
{
bool accepted{false};
for (auto const share : {1.0, 0.75, 0.5, 0.35, 0.25})
{
auto attempt{Design};
auto slowest{speed};
for (auto const &curve : r.curves)
{
if (curve.vertex < 0 || curve.vertex >= static_cast<int>(attempt.vertices.size()))
continue;
auto const allowed{std::max(1.0, std::min(speed, speed_limit(curve.radius, attempt.norms)))};
auto const recommended{recommend(allowed, curve.radius, attempt.norms)};
auto const length{recommended.transition * share};
auto &vertex{attempt.vertices[curve.vertex]};
vertex.transition_in = vertex.transition_out = length;
vertex.cant = std::min({recommended.cant, length * 1000.0 / (attempt.norms.ramp_factor * allowed), attempt.norms.cant_rate * 3.6 * length / allowed});
vertex.cant = vertex.cant < 5.0 ? 0.0 : std::floor(vertex.cant / 5.0) * 5.0;
slowest = std::min(slowest, allowed);
}
attempt.speed = slowest;
auto next{compute(attempt)};
if (false == next.valid)
continue;
auto const kept{std::all_of(next.curves.begin(), next.curves.end(), [&](curve_report const &Curve) {
auto const &vertex{attempt.vertices[Curve.vertex]};
return Curve.transition_in >= 0.95 * vertex.transition_in && Curve.transition_out >= 0.95 * vertex.transition_out;
})};
if (false == kept && share > 0.25)
continue;
Design = attempt;
r = next;
accepted = true;
break;
}
if (false == accepted)
break;
}
return r;
}
namespace
{
double turning(result const &Result)
{
double total{0.0};
for (auto const &e : Result.elements)
{
if (e.radius <= 0.0)
continue;
if (e.kind == element_kind::arc)
total += e.length / e.radius;
else if (e.kind == element_kind::transition_in || e.kind == element_kind::transition_out)
total += e.length / (2.0 * e.radius);
}
return total;
}
void spread_rolls(std::vector<segment_data> &Pieces, double const Start, double const End)
{
auto const count{static_cast<double>(Pieces.size())};
for (std::size_t i = 0; i < Pieces.size(); ++i)
Pieces[i].rolls = {static_cast<float>(Start + (End - Start) * i / count), static_cast<float>(Start + (End - Start) * (i + 1) / count)};
}
} // namespace
join_result join(join_request const &Request)
{
join_result chosen;
auto const &a{Request.start};
auto const &b{Request.end};
auto const chord{plan_of(b.position) - plan_of(a.position)};
auto const distance{glm::length(chord)};
if (distance < 0.05 || glm::length(a.direction) < 1e-9 || glm::length(b.direction) < 1e-9)
return chosen;
auto const startdirection{glm::normalize(a.direction)};
auto const enddirection{glm::normalize(b.direction)};
auto const tolerance{glm::radians(Request.kink)};
auto const heading{std::abs(geometry::signed_angle(startdirection, enddirection))};
auto const rise{b.position.y - a.position.y};
std::vector<join_result> candidates;
if (std::abs(geometry::signed_angle(startdirection, chord)) <= tolerance && std::abs(geometry::signed_angle(chord, enddirection)) <= tolerance)
{
join_result straight;
straight.kind = join_kind::straight;
segment_data path;
path.points[segment_data::point::start] = a.position;
path.points[segment_data::point::end] = b.position;
straight.pieces = {path};
spread_rolls(straight.pieces, a.roll, b.roll);
straight.length = glm::distance(a.position, b.position);
candidates.push_back(straight);
}
auto const lateral{cross(startdirection, chord)};
auto const along{glm::dot(startdirection, chord)};
if (std::abs(lateral) > 1e-6)
{
auto const angle{2.0 * std::atan2(std::abs(lateral), along)};
int const side{lateral > 0.0 ? 1 : -1};
auto const radius{distance * distance / (2.0 * std::abs(lateral))};
if (angle < kPi - 1e-3 && std::abs(geometry::signed_angle(turned(startdirection, side * angle), enddirection)) <= tolerance)
{
join_result arc;
arc.kind = join_kind::arc;
arc.pieces = geometry::arc_pieces(a.position, startdirection, rise / (radius * angle), radius, angle, side);
arc.pieces.back().points[segment_data::point::end] = b.position;
spread_rolls(arc.pieces, a.roll, b.roll);
arc.radius = radius;
arc.length = radius * angle;
candidates.push_back(arc);
}
}
auto const fitted = [&](join_kind const Kind) {
join_result curve;
design shape;
shape.norms = Request.norms;
shape.shape = Request.shape;
shape.speed = Request.speed;
shape.arc_piece_angle = 90.0;
shape.start = a.position;
shape.end = b.position;
shape.start_direction = startdirection;
shape.end_direction = enddirection;
shape.start_grade = shape.end_grade = rise / distance;
vertex corner;
corner.radius = Request.radius > 0.0 ? Request.radius : 100000.0;
if (Kind == join_kind::curve)
shape.vertices = {corner};
else
{
corner.offset = distance / 3.0;
shape.vertices = {corner, corner};
}
auto const r{compute_recommended(shape, Request.transitions)};
auto const sharp{std::any_of(r.curves.begin(), r.curves.end(), [](curve_report const &Report) { return Report.radius < 10.0; })};
if (false == r.valid || sharp || turning(r) > heading + kPi + 1e-6 || r.length > 3.0 * distance + 1.0)
return curve;
curve.kind = Kind;
curve.pieces = pieces(r, shape, 0);
if (curve.pieces.empty())
return curve;
curve.pieces.front().rolls[0] = static_cast<float>(a.roll);
curve.pieces.back().rolls[1] = static_cast<float>(b.roll);
curve.length = r.length;
curve.speed = shape.speed;
curve.warnings = r.warnings;
curve.radius = std::numeric_limits<double>::max();
for (auto const &report : r.curves)
{
curve.radius = std::min(curve.radius, report.radius);
curve.transition = std::max({curve.transition, report.transition_in, report.transition_out});
}
for (auto const &e : r.elements)
curve.cant = std::max(curve.cant, e.cant);
if (r.curves.empty())
curve.radius = 0.0;
return curve;
};
design probe;
probe.start = a.position;
probe.end = b.position;
probe.start_direction = startdirection;
probe.end_direction = enddirection;
glm::dvec2 intersection;
if (tangent_intersection(probe, intersection))
{
if (auto curve{fitted(join_kind::curve)}; curve.kind != join_kind::none)
candidates.push_back(curve);
}
else if (auto curve{fitted(join_kind::reverse_curve)}; curve.kind != join_kind::none)
candidates.push_back(curve);
if (candidates.empty())
{
join_result fallback;
fallback.kind = join_kind::bezier;
auto const grade{rise / distance};
auto const handle{distance / 3.0};
segment_data path;
path.points[segment_data::point::start] = a.position;
path.points[segment_data::point::end] = b.position;
path.points[segment_data::point::control1] = glm::dvec3{startdirection.x, grade, startdirection.y} * handle;
path.points[segment_data::point::control2] = -glm::dvec3{enddirection.x, grade, enddirection.y} * handle;
fallback.pieces = {path};
spread_rolls(fallback.pieces, a.roll, b.roll);
fallback.length = distance;
candidates.push_back(fallback);
}
auto const count{static_cast<int>(candidates.size())};
chosen = candidates[((Request.variant % count) + count) % count];
chosen.variants = count;
if (chosen.speed <= 0.0)
chosen.speed = Request.speed;
return chosen;
}
bool tangent_intersection(design const &Design, glm::dvec2 &Point)
{
auto const start{plan_of(Design.start)};