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

feat(editor): compound curves with dual radii and transition

This commit is contained in:
maj00r
2026-10-03 00:01:05 +02:00
parent 1985758a86
commit 40422aec30
5 changed files with 247 additions and 6 deletions

View File

@@ -750,6 +750,13 @@ bool editor_mode::route_from_curve(TTrack &Track)
if (curve.reversals == 0)
{
vertex.reverse_turn = std::abs(curve.turn) > glm::pi<double>();
if (curve.compound)
{
vertex.compound = true;
vertex.radius2 = std::round(curve.radius2);
vertex.transition_middle = std::round(curve.transition_middle);
vertex.split = curve.split;
}
design.vertices = {vertex};
}
else
@@ -932,8 +939,12 @@ void editor_mode::draw_route_overlay() const
drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(60, 230, 90, 255), 4);
if (i == route.grip)
drawlist->AddCircle(screen, 11.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f);
char radius[24];
std::snprintf(radius, sizeof(radius), "R %.0f", i < static_cast<int>(result.curves.size()) ? result.curves[i].radius : design.vertices[i].radius);
char radius[40];
auto const *report{i < static_cast<int>(result.curves.size()) ? &result.curves[i] : nullptr};
if (report != nullptr && report->radius2 > 0.0)
std::snprintf(radius, sizeof(radius), "R %.0f / %.0f", report->radius, report->radius2);
else
std::snprintf(radius, sizeof(radius), "R %.0f", report != nullptr ? report->radius : design.vertices[i].radius);
drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), IM_COL32(60, 230, 90, 255), radius);
}
char label[8];
@@ -1153,7 +1164,21 @@ void editor_mode::render_route_ui()
changed |= ImGui::InputScalarN("Position (x, z)", ImGuiDataType_Double, glm::value_ptr(vertex.position), 2, nullptr, nullptr, "%.2f");
ImGui::PopItemWidth();
}
changed |= ImGui::InputDouble("Radius R (m)", &vertex.radius, 0.0, 0.0, "%.1f");
changed |= ImGui::InputDouble(vertex.compound ? "Radius R1 (m)" : "Radius R (m)", &vertex.radius, 0.0, 0.0, "%.1f");
changed |= ImGui::Checkbox("Compound curve (two radii)", &vertex.compound);
if (vertex.compound)
{
changed |= ImGui::InputDouble("Radius R2 (m)", &vertex.radius2, 0.0, 0.0, "%.1f");
changed |= ImGui::InputDouble("Transition R1-R2 (m)", &vertex.transition_middle, 0.0, 0.0, "%.1f");
float split{static_cast<float>(vertex.split)};
if (ImGui::SliderFloat("Share of R1", &split, 0.0f, 1.0f, "%.2f"))
{
vertex.split = split;
changed = true;
}
vertex.radius2 = std::max(1.0, vertex.radius2);
vertex.transition_middle = std::max(0.0, vertex.transition_middle);
}
changed |= ImGui::InputDouble("Transition in (m)", &vertex.transition_in, 0.0, 0.0, "%.1f");
changed |= ImGui::InputDouble("Transition out (m)", &vertex.transition_out, 0.0, 0.0, "%.1f");
changed |= ImGui::InputDouble("Cant (mm)", &vertex.cant, 0.0, 0.0, "%.0f");

View File

@@ -94,6 +94,58 @@ local_point transition_point(transition_shape const Shape, double const Length,
return {x * h / 3.0, y * h / 3.0, angle(distance)};
}
local_point spiral_point(double const Start, double const End, double const Length, double const Distance)
{
if (Distance <= 0.0)
return {};
auto const heading = [&](double const S) { return Start * S + (End - Start) * S * S / (2.0 * std::max(Length, 1e-9)); };
int const steps{std::max(8, static_cast<int>(std::ceil(Distance / 0.5)) * 2)};
double const h{Distance / steps};
double x{0.0}, y{0.0};
for (int i = 0; i <= steps; ++i)
{
double const weight = (i == 0 || i == steps) ? 1.0 : (i % 2 ? 4.0 : 2.0);
auto const a{heading(i * h)};
x += weight * std::cos(a);
y += weight * std::sin(a);
}
return {x * h / 3.0, y * h / 3.0, heading(Distance)};
}
struct compound_curve
{
std::vector<std::array<double, 3>> segments;
local_point end;
double shortened{1.0};
};
compound_curve build_compound(double const Deflection, double const Radius1, double const Radius2, double const Lengthin, double const Lengthmiddle, double const Lengthout, double const Split)
{
compound_curve result;
auto const k1{1.0 / Radius1};
auto const k2{1.0 / Radius2};
auto const transitions{(k1 * Lengthin + (k1 + k2) * Lengthmiddle + k2 * Lengthout) * 0.5};
if (transitions > Deflection)
result.shortened = Deflection / transitions * (1.0 - 1e-9);
auto const f{result.shortened};
auto const arcs{std::max(0.0, Deflection - transitions * f)};
auto const split{std::clamp(Split, 0.0, 1.0)};
std::array<double, 3> const segments[] = {{0.0, k1, Lengthin * f}, {k1, k1, split * arcs / k1}, {k1, k2, Lengthmiddle * f}, {k2, k2, (1.0 - split) * arcs / k2}, {k2, 0.0, Lengthout * f}};
double x{0.0}, y{0.0}, heading{0.0};
for (auto const &segment : segments)
{
if (segment[2] < 1e-6)
continue;
result.segments.push_back(segment);
auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])};
x += local.x * std::cos(heading) - local.y * std::sin(heading);
y += local.x * std::sin(heading) + local.y * std::cos(heading);
heading += local.angle;
}
result.end = {x, y, heading};
return result;
}
void element_point(element const &Element, transition_shape const Shape, double const Distance, glm::dvec2 &Position, glm::dvec2 &Direction)
{
switch (Element.kind)
@@ -116,6 +168,13 @@ void element_point(element const &Element, transition_shape const Shape, double
Direction = Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle);
break;
}
case element_kind::spiral:
{
auto const local{spiral_point(Element.curvature_start, Element.curvature_end, Element.length, Distance)};
Position = Element.origin + Element.direction * local.x + Element.normal * local.y;
Direction = Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle);
break;
}
case element_kind::transition_out:
{
auto const local{transition_point(Shape, Element.transition, Element.radius, 1.0 - Distance / Element.transition)};
@@ -133,6 +192,11 @@ double element_cant(element const &Element, double const Distance)
case element_kind::transition_in: return Element.cant * Distance / Element.transition;
case element_kind::transition_out: return Element.cant * (1.0 - Distance / Element.transition);
case element_kind::arc: return Element.cant;
case element_kind::spiral:
{
auto const curvature{Element.curvature_start + (Element.curvature_end - Element.curvature_start) * Distance / std::max(Element.length, 1e-9)};
return Element.curvature_peak > 0.0 ? Element.cant * curvature / Element.curvature_peak : 0.0;
}
default: return 0.0;
}
}
@@ -164,6 +228,11 @@ std::size_t element_pieces(element const &Element, transition_shape const Shape)
case element_kind::transition_in:
case element_kind::transition_out:
return Shape == transition_shape::cubic_parabola ? 1 : static_cast<std::size_t>(std::max(1.0, std::ceil(Element.length / 20.0 - 1e-9)));
case element_kind::spiral:
{
auto const turn{(Element.curvature_start + Element.curvature_end) * 0.5 * Element.length};
return static_cast<std::size_t>(std::max({1.0, std::ceil(turn / (10.0 * kPi / 180.0) - 1e-9), std::ceil(Element.length / 20.0 - 1e-9)}));
}
default:
return 1;
}
@@ -290,6 +359,9 @@ result compute(design const &Design)
double tangent_in{0.0};
double tangent_out{0.0};
double deflection{0.0};
bool compound{false};
double radius2{0.0};
double transition_middle{0.0};
std::function<void(double, double &, double &)> tangents;
};
std::vector<fitted> fits(count);
@@ -314,6 +386,25 @@ result compute(design const &Design)
r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1));
return r;
}
if (vertex.compound)
{
fit.compound = true;
fit.radius2 = std::max(1.0, vertex.radius2);
fit.transition_middle = std::max(0.0, vertex.transition_middle);
fit.deflection = deflection;
fit.tangents = [&, k, directionin, directionout, side](double const Radius, double &Tangentin, double &Tangentout) {
auto const &own{fits[k]};
auto const scale{Radius / own.radius};
auto const shape{build_compound(own.deflection, Radius, own.radius2 * scale, own.transition_in * scale, own.transition_middle * scale, own.transition_out * scale, Design.vertices[k].split)};
auto const normalin{perpendicular(directionin) * static_cast<double>(side)};
auto const end{directionin * shape.end.x + normalin * shape.end.y};
auto const along{cross(directionout, end) / cross(directionout, directionin)};
Tangentin = std::max(0.0, along);
Tangentout = std::max(0.0, glm::dot(end - directionin * along, directionout));
};
fit.tangents(fit.radius, fit.tangent_in, fit.tangent_out);
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;
};
@@ -382,7 +473,7 @@ result compute(design const &Design)
}
low = high * (1.0 + 1e-9);
high = fit.radius;
if (fits_budget(low))
if (false == fit.compound && fits_budget(low))
{
for (int i = 0; i < 60; ++i)
{
@@ -394,7 +485,9 @@ result compute(design const &Design)
else
{
fit.radius *= scales[k];
fit.radius2 *= scales[k];
fit.transition_in *= scales[k];
fit.transition_middle *= scales[k];
fit.transition_out *= scales[k];
}
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));
@@ -430,6 +523,71 @@ result compute(design const &Design)
r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1));
return r;
}
if (fit.compound)
{
auto const shape{build_compound(deflection, fit.radius, fit.radius2, fit.transition_in, fit.transition_middle, fit.transition_out, vertex.split)};
if (shape.shortened < 1.0)
r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to fit the deflection angle", k + 1));
auto const normalin{perpendicular(directionin) * static_cast<double>(side)};
auto const endoffset{directionin * shape.end.x + normalin * shape.end.y};
auto const along{cross(directionout, endoffset) / cross(directionout, directionin)};
auto const curvestart{position - directionin * along};
auto const straight{glm::dot(curvestart - cursor, directionin)};
if (straight < -1e-4)
{
if (k == 0)
r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight));
else
r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight));
}
push_straight(cursor, directionin, straight);
auto const startchainage{chainage};
glm::dvec2 origin{curvestart};
glm::dvec2 heading{directionin};
auto const peak{std::max(1.0 / fit.radius, 1.0 / fit.radius2)};
for (auto const &segment : shape.segments)
{
element piece;
piece.kind = element_kind::spiral;
piece.vertex = static_cast<int>(k);
piece.turn = side;
piece.cant = std::max(0.0, vertex.cant);
piece.curvature_start = segment[0];
piece.curvature_end = segment[1];
piece.curvature_peak = peak;
piece.radius = std::max(segment[0], segment[1]) > 0.0 ? 1.0 / std::max(segment[0], segment[1]) : 0.0;
piece.length = segment[2];
piece.origin = origin;
piece.direction = heading;
piece.normal = perpendicular(heading) * static_cast<double>(side);
push(piece);
auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])};
origin += heading * local.x + piece.normal * local.y;
heading = rotate(heading, side * local.angle);
}
vertexchainage[k] = (startchainage + chainage) * 0.5;
cursor = origin;
curve_report report;
report.vertex = static_cast<int>(k);
report.radius = fit.radius;
report.radius2 = fit.radius2;
report.transition_in = fit.transition_in * shape.shortened;
report.transition_out = fit.transition_out * shape.shortened;
report.deflection = deflection;
report.arc_length = chainage - startchainage;
report.tangent_in = glm::distance(position, curvestart);
report.tangent_out = glm::distance(position, cursor);
auto const sharpest{std::min(fit.radius, fit.radius2)};
report.unbalanced = unbalanced_acceleration(Design.speed, sharpest, std::max(0.0, vertex.cant), Design.norms);
report.recommended = recommend(Design.speed, sharpest, Design.norms);
r.curves.push_back(report);
if (sharpest < report.recommended.radius_min - 1e-6)
r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, sharpest, report.recommended.radius_min, Design.speed));
if (report.unbalanced > Design.norms.unbalanced + 1e-6)
r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2", k + 1, report.unbalanced, Design.norms.unbalanced));
continue;
}
auto const radius{fit.radius};
auto const lengthin{fit.transition_in};
auto const lengthout{fit.transition_out};
@@ -748,7 +906,7 @@ std::vector<segment_data> pieces(result const &Result, design const &Design, std
}
default:
{
if (Design.shape == transition_shape::cubic_parabola)
if (Design.shape == transition_shape::cubic_parabola && e.kind != element_kind::spiral)
{
std::array<glm::dvec2, 4> const local{glm::dvec2{0.0, 0.0}, glm::dvec2{e.transition / 3.0, 0.0}, glm::dvec2{e.transition * 2.0 / 3.0, 0.0}, glm::dvec2{e.transition, e.transition * e.transition / (6.0 * e.radius)}};
auto const a{e.kind == element_kind::transition_in ? piece.from / e.transition : 1.0 - piece.from / e.transition};
@@ -785,6 +943,11 @@ std::vector<segment_data> pieces(result const &Result, design const &Design, std
path.rolls[0] = roll(element_cant(e, piece.from));
path.rolls[1] = roll(element_cant(e, piece.to));
path.radius = e.kind == element_kind::straight ? 0.f : static_cast<float>(e.radius);
if (e.kind == element_kind::spiral)
{
auto const curvature{e.curvature_start + (e.curvature_end - e.curvature_start) * (piece.from + piece.to) * 0.5 / std::max(e.length, 1e-9)};
path.radius = curvature > 1e-9 ? static_cast<float>(1.0 / curvature) : 0.f;
}
result.push_back(path);
}
return result;

View File

@@ -49,6 +49,10 @@ struct vertex
double elevation{0.0};
double vertical_radius{0.0};
bool reverse_turn{false};
bool compound{false};
double radius2{600.0};
double transition_middle{0.0};
double split{0.5};
};
struct design
@@ -85,7 +89,8 @@ enum class element_kind
straight,
transition_in,
arc,
transition_out
transition_out,
spiral
};
struct element
@@ -98,6 +103,9 @@ struct element
double transition{0.0};
int turn{0};
double cant{0.0};
double curvature_start{0.0};
double curvature_end{0.0};
double curvature_peak{0.0};
glm::dvec2 origin{0.0};
glm::dvec2 direction{0.0, 1.0};
glm::dvec2 normal{0.0};
@@ -107,6 +115,7 @@ struct curve_report
{
int vertex{-1};
double radius{0.0};
double radius2{0.0};
double transition_in{0.0};
double transition_out{0.0};
double deflection{0.0};

View File

@@ -1039,6 +1039,46 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance
if (Curve.transition_in + Curve.transition_out >= std::accumulate(lengths.begin(), lengths.end(), 0.0))
Curve.transition_in = Curve.transition_out = 0.0;
Curve.radius = smallest;
std::vector<std::pair<double, double>> arcs;
for (std::size_t i = 0; i < run.size(); ++i)
{
if (varying[i])
continue;
auto const radius{path_radius(run[i].first->m_paths.front(), 0.5)};
if (radius <= 0.0)
continue;
if (false == arcs.empty() && std::abs(arcs.back().first - radius) < 0.05 * radius)
arcs.back().second += lengths[i];
else
arcs.emplace_back(radius, lengths[i]);
}
if (arcs.size() >= 2 && Curve.reversals == 0)
{
auto const &first{arcs.front()};
auto const &last{arcs.back()};
if (std::abs(first.first - last.first) > 0.1 * std::min(first.first, last.first))
{
Curve.compound = true;
Curve.radius = first.first;
Curve.radius2 = last.first;
auto const turnfirst{first.second / first.first};
auto const turnlast{last.second / last.first};
Curve.split = turnfirst / std::max(1e-9, turnfirst + turnlast);
double middle{0.0};
bool inside{false};
for (std::size_t i = 0; i < run.size(); ++i)
{
if (false == varying[i])
{
inside = true;
continue;
}
if (inside && i + 1 < run.size() && std::any_of(varying.begin() + i + 1, varying.end(), [](bool const Varying) { return false == Varying; }))
middle += lengths[i];
}
Curve.transition_middle = middle;
}
}
Curve.from = run.front().first;
Curve.to = run.back().first;
return true;

View File

@@ -128,6 +128,10 @@ class editor_track
double transition_in{0.0};
double transition_out{0.0};
double cant{0.0};
bool compound{false};
double radius2{0.0};
double transition_middle{0.0};
double split{0.5};
};
static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve);
struct switch_template