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

feat(editor): laid track follows the ground between the clicks

- the height along the new track is a smooth curve through points on the ground under the foot of the ballast, added where the track would hang or sink more than 0.1 m, at least 10 m apart; the paths are cut there, each joint keeps one grade on both sides
- the clicked ends and the free ends joined keep their height and grade as before; no grade limits
- self-test layclicks: how far the foot of the ballast goes over and under the ground along the paths laid
This commit is contained in:
maj00r
2026-10-11 12:12:48 +02:00
parent cfc2b2c64a
commit 6284f4e0d3
4 changed files with 207 additions and 68 deletions

View File

@@ -201,54 +201,27 @@ std::vector<segment_data> straight_piece(glm::dvec3 const &Point, glm::dvec2 con
return {path};
}
void follow_heights(std::vector<segment_data> &Pieces, double const Height0, double Grade0, double const Height1, double Grade1)
double hermite(double const Length, double const Height0, double const Grade0, double const Height1, double const Grade1, double const T, double &Grade)
{
std::vector<double> lengths;
double total{0.0};
for (auto const &piece : Pieces)
total += lengths.emplace_back(bezier{piece}.plan_length());
if (total < 1e-3)
return;
auto const chord{(Height1 - Height0) / total};
if (std::isnan(Grade0))
Grade0 = chord;
if (std::isnan(Grade1))
Grade1 = chord;
auto const height = [&](double const Chainage) {
auto const t{Chainage / total};
return (2.0 * t * t * t - 3.0 * t * t + 1.0) * Height0 + (t * t * t - 2.0 * t * t + t) * total * Grade0 + (3.0 * t * t - 2.0 * t * t * t) * Height1 + (t * t * t - t * t) * total * Grade1;
};
auto const grade = [&](double const Chainage) {
auto const t{Chainage / total};
return (6.0 * t * t - 6.0 * t) * (Height0 - Height1) / total + (3.0 * t * t - 4.0 * t + 1.0) * Grade0 + (3.0 * t * t - 2.0 * t) * Grade1;
};
double chainage{0.0};
for (std::size_t i = 0; i < Pieces.size(); ++i)
auto const t2{T * T};
auto const t3{t2 * T};
Grade = (6.0 * t2 - 6.0 * T) * (Height0 - Height1) / Length + (3.0 * t2 - 4.0 * T + 1.0) * Grade0 + (3.0 * t2 - 2.0 * T) * Grade1;
return (2.0 * t3 - 3.0 * t2 + 1.0) * Height0 + (t3 - 2.0 * t2 + T) * Length * Grade0 + (3.0 * t2 - 2.0 * t3) * Height1 + (t3 - t2) * Length * Grade1;
}
segment_data part_of(segment_data const &Piece, double const From, double const To)
{
bezier const curve{Piece};
segment_data result{Piece};
result.points[segment_data::point::start] = curve.point(From);
result.points[segment_data::point::end] = curve.point(To);
if (Piece.points[segment_data::point::control1] != glm::dvec3{} || Piece.points[segment_data::point::control2] != glm::dvec3{})
{
auto &points{Pieces[i].points};
auto const length{lengths[i]};
auto const from{grade(chainage)};
auto const to{grade(chainage + length)};
auto &start{points[segment_data::point::start]};
auto &end{points[segment_data::point::end]};
auto &control1{points[segment_data::point::control1]};
auto &control2{points[segment_data::point::control2]};
start.y = height(chainage);
end.y = height(chainage + length);
chainage += length;
if (control1 != glm::dvec3{} || control2 != glm::dvec3{})
{
control1.y = from * glm::length(plan_of(control1));
control2.y = -to * glm::length(plan_of(control2));
continue;
}
auto const linear{(end.y - start.y) / std::max(length, 1e-6)};
if (length < 1e-3 || (std::abs(from - linear) < 1e-6 && std::abs(to - linear) < 1e-6))
continue;
auto const along{glm::normalize(plan_of(end - start)) * (length / 3.0)};
control1 = {along.x, from * length / 3.0, along.y};
control2 = {-along.x, -to * length / 3.0, -along.y};
result.points[segment_data::point::control1] = curve.first(From) * ((To - From) / 3.0);
result.points[segment_data::point::control2] = -curve.first(To) * ((To - From) / 3.0);
}
result.rolls = {glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast<float>(From)), glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast<float>(To))};
return result;
}
std::vector<segment_data> arc_through(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec2 const &Offset)
@@ -4030,25 +4003,174 @@ glm::dvec3 editor_mode::cursor_terrain(double const Height)
return point;
}
double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style)
std::vector<double> editor_mode::bed_heights(std::vector<glm::dvec3> const &Points, std::vector<glm::dvec2> const &Directions, TTrack const *Style) const
{
auto const [depth, half]{Style != nullptr ? profile_ballast(*Style) : std::pair<double, double>{0.2, 0.5 * 1.435 + 0.5 + 1.1}};
std::vector<glm::dvec3> points{Point};
if (glm::length(Direction) > 1e-6)
std::vector<glm::dvec3> probes;
probes.reserve(Points.size() * 3);
for (std::size_t i = 0; i < Points.size(); ++i)
{
auto const side{glm::normalize(glm::dvec2{-Direction.y, Direction.x}) * half};
points.push_back(Point + glm::dvec3{side.x, 0.0, side.y});
points.push_back(Point - glm::dvec3{side.x, 0.0, side.y});
auto const &direction{Directions[i]};
auto const side{glm::length(direction) > 1e-6 ? glm::normalize(glm::dvec2{-direction.y, direction.x}) * half : glm::dvec2{0.0}};
probes.push_back(Points[i]);
probes.push_back(Points[i] + glm::dvec3{side.x, 0.0, side.y});
probes.push_back(Points[i] - glm::dvec3{side.x, 0.0, side.y});
}
std::vector<char> found;
auto const heights{ground_heights(points, true, &found)};
auto toe{std::numeric_limits<double>::max()};
for (std::size_t i = 1; i < points.size(); ++i)
if (found[i] != 0)
toe = std::min(toe, heights[i]);
if (found[0] == 0)
return toe < std::numeric_limits<double>::max() ? toe + depth : Point.y;
return std::max(toe < std::numeric_limits<double>::max() ? toe + depth : heights[0] + depth, heights[0]);
auto const heights{ground_heights(probes, true, &found)};
std::vector<double> result(Points.size(), std::numeric_limits<double>::quiet_NaN());
for (std::size_t i = 0; i < Points.size(); ++i)
{
auto const axis{found[3 * i] != 0 ? heights[3 * i] : std::numeric_limits<double>::quiet_NaN()};
auto toe{std::numeric_limits<double>::infinity()};
for (std::size_t j = 3 * i + 1; j < 3 * i + 3; ++j)
if (found[j] != 0)
toe = std::min(toe, heights[j]);
if (std::isfinite(toe))
result[i] = std::isnan(axis) ? toe + depth : std::max(toe + depth, axis);
else if (false == std::isnan(axis))
result[i] = axis + depth;
}
return result;
}
double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) const
{
auto const height{bed_heights({Point}, {Direction}, Style).front()};
return std::isnan(height) ? Point.y : height;
}
void editor_mode::follow_ground(std::vector<segment_data> &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const
{
double constexpr step{2.0};
double constexpr tolerance{0.1};
double constexpr spacing{10.0};
std::vector<double> lengths;
double total{0.0};
for (auto const &piece : Pieces)
total += lengths.emplace_back(bezier{piece}.plan_length(256));
if (total < 1e-3)
return;
std::vector<double> chainages;
std::vector<glm::dvec3> points;
std::vector<glm::dvec2> directions;
double reached{0.0};
for (std::size_t i = 0; i < Pieces.size(); ++i)
{
bezier const curve{Pieces[i]};
auto const count{std::max(1, static_cast<int>(std::ceil(lengths[i] / step)))};
for (int k = (i == 0 ? 1 : 0); k < count; ++k)
{
auto const along{lengths[i] * k / count};
auto const t{curve.parameter(along)};
auto const chainage{reached + along};
auto point{curve.point(t)};
point.y = Height0 + (Height1 - Height0) * chainage / total + 50.0;
chainages.push_back(chainage);
points.push_back(point);
directions.push_back(plan_of(curve.first(t)));
}
reached += lengths[i];
}
auto const targets{bed_heights(points, directions, Style)};
std::vector<double> knots{0.0, total};
std::vector<double> heights{Height0, Height1};
auto const slopes = [&]() {
auto const last{knots.size() - 1};
std::vector<double> result(knots.size());
for (std::size_t i = 0; i <= last; ++i)
{
auto const a{i == 0 ? 0 : i - 1};
auto const b{i == last ? last : i + 1};
result[i] = (heights[b] - heights[a]) / (knots[b] - knots[a]);
}
if (false == std::isnan(Grade0))
result.front() = Grade0;
if (false == std::isnan(Grade1))
result.back() = Grade1;
return result;
};
auto slope{slopes()};
auto const at = [&](double const Chainage, double &Grade) {
auto const next{static_cast<std::size_t>(std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin())};
auto const i{std::clamp<std::size_t>(next, 1, knots.size() - 1) - 1};
auto const length{knots[i + 1] - knots[i]};
return hermite(length, heights[i], slope[i], heights[i + 1], slope[i + 1], std::clamp((Chainage - knots[i]) / length, 0.0, 1.0), Grade);
};
for (std::size_t pass = 0; pass < chainages.size(); ++pass)
{
auto worst{tolerance};
auto pick{chainages.size()};
for (std::size_t j = 0; j < chainages.size(); ++j)
{
if (std::isnan(targets[j]))
continue;
double grade;
auto const miss{std::abs(at(chainages[j], grade) - targets[j])};
if (miss <= worst)
continue;
auto const next{std::upper_bound(knots.begin(), knots.end(), chainages[j])};
if (next == knots.end() || *next - chainages[j] < spacing || chainages[j] - *std::prev(next) < spacing)
continue;
worst = miss;
pick = j;
}
if (pick == chainages.size())
break;
auto const where{std::upper_bound(knots.begin(), knots.end(), chainages[pick]) - knots.begin()};
knots.insert(knots.begin() + where, chainages[pick]);
heights.insert(heights.begin() + where, targets[pick]);
slope = slopes();
}
std::vector<segment_data> result;
std::vector<std::pair<double, double>> spans;
double from{0.0};
for (std::size_t i = 0; i < Pieces.size(); ++i)
{
bezier const curve{Pieces[i]};
auto const to{from + lengths[i]};
double cut{from};
double parameter{0.0};
for (auto const knot : knots)
{
if (knot < cut + 0.5 || knot > to - 0.5)
continue;
auto const next{curve.parameter(knot - from)};
result.push_back(part_of(Pieces[i], parameter, next));
spans.emplace_back(cut, knot);
cut = knot;
parameter = next;
}
result.push_back(parameter > 0.0 ? part_of(Pieces[i], parameter, 1.0) : Pieces[i]);
spans.emplace_back(cut, to);
from = to;
}
for (std::size_t i = 0; i < result.size(); ++i)
{
auto &points{result[i].points};
auto const [a, b]{spans[i]};
auto const length{b - a};
double begin, end;
auto &start{points[segment_data::point::start]};
auto &finish{points[segment_data::point::end]};
auto &control1{points[segment_data::point::control1]};
auto &control2{points[segment_data::point::control2]};
start.y = at(a, begin);
finish.y = at(b, end);
if (control1 != glm::dvec3{} || control2 != glm::dvec3{})
{
control1.y = begin * glm::length(plan_of(control1));
control2.y = -end * glm::length(plan_of(control2));
continue;
}
auto const linear{(finish.y - start.y) / std::max(length, 1e-6)};
if (length < 1e-3 || (std::abs(begin - linear) < 1e-6 && std::abs(end - linear) < 1e-6))
continue;
auto const along{glm::normalize(plan_of(finish - start)) * (length / 3.0)};
control1 = {along.x, begin * length / 3.0, along.y};
control2 = {-along.x, -end * length / 3.0, -along.y};
}
Pieces = std::move(result);
}
glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point)
@@ -4294,7 +4416,7 @@ std::vector<segment_data> editor_mode::extend_pieces() const
auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi<double>()), side)};
for (auto &piece : pieces)
piece.rolls = {diverging.rolls[1], diverging.rolls[1]};
follow_heights(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits<double>::quiet_NaN());
follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits<double>::quiet_NaN(), tool.track);
return pieces;
}
}
@@ -4302,7 +4424,7 @@ std::vector<segment_data> editor_mode::extend_pieces() const
if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0)
radius = std::abs(values[0]);
auto pieces{layout_pieces(tool.point, tool.direction, tool.grade, tool.mouse, radius)};
follow_heights(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits<double>::quiet_NaN());
follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits<double>::quiet_NaN(), tool.track);
return pieces;
}
@@ -4639,19 +4761,20 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
{
auto const grade{-grade_of(tool.start.direction)};
pieces = layout_pieces(Points[0], glm::normalize(leaving), grade, Points[1], tool.radius);
follow_heights(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits<double>::quiet_NaN());
follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits<double>::quiet_NaN(), tool.start.track);
}
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)
{
auto const back{layout_pieces(End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)};
for (auto piece{back.rbegin()}; piece != back.rend(); ++piece)
pieces.push_back(turned_around(*piece));
follow_heights(pieces, Points[0].y, std::numeric_limits<double>::quiet_NaN(), End.position.y, grade_of(End.direction));
follow_ground(pieces, Points[0].y, std::numeric_limits<double>::quiet_NaN(), End.position.y, grade_of(End.direction), End.track);
}
for (auto const &piece : pieces)
Length += bezier{piece}.plan_length();