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

feat(track): 3D sleepers and switch timbers, blade heel and throw from templates, one rail carving pass

- sleepers3d / sleepers3dplain / sleepers3dbed in eu07.ini: timbers put together from the begin, main and end parts of a model
  and stretched to their length, plain sleepers of their own model; tracks whose plain sleepers would touch are laid as one
  cluster, lines of timbers led by priority and turned to the bisectors of frogs and crossings at a limited rate, whole timbers
  under crossings, split ones between tracks with room to part them, gaps filled per rail and overlaps shortened or dropped;
  clusters worked out in a background thread while driving, on demand in the editor
- the ballast can be left out under the beams (sleepers3dbed no)
- blades get their heel and throw from the switch template or blades <heel> <throw> in the scenery; blades of double switches
  run on into the track linking them to the next switch
- rail cutting at frogs, wing rails, movable noses and switch crossings done by one carving pass (RailCarve)
- tint of the rail treads applied to the colour only, without the matte band at the frog
- a switch trailed through with a movable nose set the other way derails the vehicle
- self-tests: sleepercheck, sharedzones, beamsnear, throw, sleepersbg
This commit is contained in:
maj00r
2026-10-17 23:28:36 +02:00
parent 21eca4d9a6
commit d25f20a56c
49 changed files with 9802 additions and 848 deletions

View File

@@ -31,6 +31,9 @@ namespace
using geometry::bezier;
using geometry::has_controls;
using geometry::cross;
using geometry::grade_of;
using geometry::plan_distance;
using geometry::plan_of;
struct switch_end_move
@@ -90,6 +93,80 @@ std::string switch_rejoin(TTrack &Track, std::vector<editor_track::joint> const
return loose;
}
double constexpr kBranchStraight{500.0}; // how far the straight past the curve next to a switch is followed
// direction out of the path at the end
glm::dvec3 end_outward(segment_data const &Path, bool const Atend)
{
bezier const curve{Path};
auto direction{Atend ? curve.first(1.0) : -curve.first(0.0)};
if (glm::length(plan_of(direction)) < 1e-9)
direction = Atend ? curve.p3 - curve.p0 : curve.p0 - curve.p3;
return direction;
}
// straight from the start of the line to its first curve
double opening(alignment::result const &Result)
{
return Result.elements.empty() || Result.elements.front().kind != alignment::element_kind::straight ? 0.0 : Result.elements.front().length;
}
// straight from the last curve of the line to its end
double closing(alignment::result const &Result)
{
return Result.elements.empty() || Result.elements.back().kind != alignment::element_kind::straight ? 0.0 : Result.elements.back().length;
}
alignment::vertex scaled(alignment::vertex Vertex, double const Factor)
{
Vertex.radius *= Factor;
Vertex.transition_in *= Factor;
Vertex.transition_out *= Factor;
for (auto &arc : Vertex.arcs)
{
arc.radius *= Factor;
arc.transition *= Factor;
}
return Vertex;
}
// where the path of the index leaves the chain
glm::dvec3 exit_of(editor_track::chain const &Chain, std::size_t const Index)
{
auto const &path{Chain.tracks[Index]->m_paths.front()};
return path.points[Chain.forward[Index] ? segment_data::point::end : segment_data::point::start];
}
// raises the pieces by Rise at their start, less and less up to nothing at their end
void lift(std::vector<segment_data> &Pieces, double const Rise)
{
std::vector<double> lengths;
double total{0.0};
for (auto const &piece : Pieces)
{
lengths.push_back(bezier{piece}.plan_length());
total += lengths.back();
}
if (total <= 1e-6 || std::abs(Rise) <= 1e-6)
return;
auto const slope{Rise / total};
double along{0.0};
for (std::size_t i = 0; i < Pieces.size(); ++i)
{
auto &points{Pieces[i].points};
auto const controls{has_controls(Pieces[i])};
points[segment_data::point::start].y += Rise - slope * along;
points[segment_data::point::end].y += Rise - slope * (along + lengths[i]);
// the controls are kept from their ends
if (controls)
{
points[segment_data::point::control1].y -= slope * glm::length(plan_of(points[segment_data::point::control1]));
points[segment_data::point::control2].y += slope * glm::length(plan_of(points[segment_data::point::control2]));
}
along += lengths[i];
}
}
} // namespace
bool editor_mode::switch_room(TTrack &Track, double &Low, double &High, std::string &Reason)
@@ -178,6 +255,258 @@ bool editor_mode::switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec
return true;
}
std::optional<editor_mode::switch_branch> editor_mode::take_switch_branch(TTrack const &Track, editor_track::joint const &Joint, std::size_t const Index) const
{
auto *first{Joint.other};
if (first == nullptr || first->eType != tt_Normal || first->m_paths.empty())
return std::nullopt;
auto const category{first->iCategoryFlag & 15};
auto const regular = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && false == Other->m_editorremoved && editor_track::is_supported(*Other) && (Other->iCategoryFlag & 15) == category; };
// from the switch on: a straight up to the curve maybe, the curve, the straight past it
std::vector<TTrack *> run;
auto *current{first};
auto atstart{Joint.theirs.kind == editor_track::point_kind::start};
int phase{0};
std::size_t straight{0};
double past{0.0};
while (regular(current) && std::find(run.begin(), run.end(), current) == run.end() && past < kBranchStraight)
{
auto const plain{editor_track::is_straight(*current, m_straights.tolerance)};
if (false == plain && phase == 2)
break;
if (false == plain)
phase = 1;
else if (phase == 1)
{
phase = 2;
straight = run.size();
}
if (phase == 2)
past += current->Length();
run.push_back(current);
auto *next{atstart ? current->trNext : current->trPrev};
if (next == nullptr || (next->trPrev != current && next->trNext != current))
break;
atstart = next->trPrev == current;
current = next;
}
if (phase != 2)
return std::nullopt;
switch_branch branch;
branch.joint = Index;
branch.straight = straight;
std::string error;
auto &chain{branch.chain};
if (false == editor_track::find_chain(run.front(), run.back(), chain, error) || chain.tracks != run || plan_distance(chain.start, editor_track::point_position(Track, Joint.own)) > geometry::same_point)
return std::nullopt;
auto &design{branch.design};
design.norms = m_route.design.norms;
design.shape = m_route.design.shape;
design.transition_pieces = m_route.design.transition_pieces;
design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0;
if (auto const hit{alignment_store::on_track(*run[straight - 1])})
{
auto const &stored{alignment_store::records()[hit->record].design};
design.norms = stored.norms;
design.shape = stored.shape;
design.transition_pieces = stored.transition_pieces;
design.arc_piece_angle = stored.arc_piece_angle;
design.speed = stored.speed;
}
design.norms.checked = false;
auto const found{editor_track::recognize_line(chain, m_straights.tolerance, design.norms.gauge, design.shape)};
if (found.size() != 1 || found.front().kink)
return std::nullopt;
design.start = chain.start;
design.end = chain.end;
design.start_direction = glm::normalize(plan_of(chain.start_direction));
design.end_direction = glm::normalize(plan_of(chain.end_direction));
design.start_grade = grade_of(chain.start_direction);
design.end_grade = grade_of(chain.end_direction);
auto vertex{route_vertex_of(found.front().shape)};
vertex.position = found.front().position;
design.vertices = {vertex};
branch.result = alignment::compute(design);
if (false == branch.result.valid)
return std::nullopt;
return branch;
}
bool editor_mode::plan_switch_branch(segment_data const &Path, editor_track::joint const &Joint, switch_branch &Branch) const
{
auto const atend{Joint.own.kind == editor_track::point_kind::end};
auto const start{Path.points[atend ? segment_data::point::end : segment_data::point::start]};
auto const outward{end_outward(Path, atend)};
auto laid{Branch.design};
laid.start = start;
laid.start_direction = glm::normalize(plan_of(outward));
laid.start_grade = grade_of(outward);
auto const original{Branch.design.vertices.front()};
auto const make = [&](double const Factor) {
laid.vertices = {scaled(original, Factor)};
return alignment::compute(laid);
};
// compute() makes the curve tighter when it doesn't fit between the ends
auto const fits = [&](alignment::result const &Result) { return Result.valid && false == Result.curves.empty() && Result.curves.front().radius >= laid.vertices.front().radius * (1.0 - 1e-6); };
double factor{1.0};
auto result{make(factor)};
if (false == result.valid || result.curves.empty())
return false;
if (false == fits(result))
{
// the design keeps the curve the way it was made to fit
auto &vertex{laid.vertices.front()};
auto const &report{result.curves.front()};
auto const reduced{report.radius / vertex.radius};
if (vertex.compound)
vertex = scaled(vertex, reduced);
else
{
vertex.radius = report.radius;
vertex.transition_in = report.transition_in;
vertex.transition_out = report.transition_out;
}
factor = reduced;
result = alignment::compute(laid);
if (false == result.valid)
return false;
}
else if (opening(Branch.result) < 0.1 && opening(result) >= 0.1)
{
// the curve which adjoined the switch keeps to it: the largest one which fits
double low{1.0};
double high{1.5};
while (fits(make(high)) && high < 5.0)
high *= 1.5;
if (fits(make(high)))
return false;
for (int i = 0; i < 40; ++i)
{
auto const middle{0.5 * (low + high)};
(fits(make(middle)) ? low : high) = middle;
}
factor = low;
result = make(factor);
if (false == result.valid)
return false;
}
// the paths are laid up to the first joint on the straight past the old and the new curve
auto const &chain{Branch.chain};
auto const end{plan_of(chain.end)};
auto const behind = [&](glm::dvec3 const &Point) { return glm::dot(end - plan_of(Point), laid.end_direction); };
auto const curveend{closing(result)};
auto last{chain.tracks.size()};
for (auto k{Branch.straight - 1}; k < chain.tracks.size(); ++k)
if (behind(exit_of(chain, k)) <= curveend - 0.5)
{
last = k;
break;
}
if (last == chain.tracks.size())
return false;
auto part{laid};
auto const joint{exit_of(chain, last)};
part.end = joint;
part.end_grade = grade_of(end_outward(chain.tracks[last]->m_paths.front(), chain.forward[last]));
auto const partresult{alignment::compute(part)};
if (false == partresult.valid)
return false;
Branch.part = {};
Branch.part.tracks.assign(chain.tracks.begin(), chain.tracks.begin() + last + 1);
Branch.part.forward.assign(chain.forward.begin(), chain.forward.begin() + last + 1);
Branch.part.start = chain.start;
Branch.part.end = joint;
Branch.pieces = alignment::pieces(partresult, part, last + 1);
if (Branch.pieces.size() < last + 1)
return false;
editor_track::keep_heights(Branch.part, Branch.pieces);
lift(Branch.pieces, start.y - chain.start.y);
Branch.pieces.front().points[segment_data::point::start] = start;
Branch.pieces.back().points[segment_data::point::end] = joint;
Branch.laid = laid;
Branch.factor = factor;
return true;
}
void editor_mode::lay_switch_branch(switch_branch const &Branch, editor_track::edit &Edit)
{
for (auto *track : Branch.part.tracks)
Edit.capture(*track);
auto const created{editor_track::relay(Branch.part, Branch.pieces)};
Edit.created.insert(Edit.created.end(), created.begin(), created.end());
// the design kept for the line starting or ending at the switch follows it
auto const &records{alignment_store::records()};
auto const from{Branch.chain.start};
auto const joint{Branch.part.end};
for (std::size_t i = 0; i < records.size(); ++i)
{
auto const &record{records[i]};
auto const atstart{plan_distance(record.design.start, from) < geometry::same_point};
if (false == atstart && plan_distance(record.design.end, from) >= geometry::same_point)
continue;
auto design{record.design};
auto &vertices{design.vertices};
if (vertices.empty())
return;
auto const outward{Branch.laid.start_direction};
auto const k{atstart ? std::size_t{0} : vertices.size() - 1};
if (atstart)
{
design.start = Branch.laid.start;
design.start_direction = outward;
design.start_grade = Branch.laid.start_grade;
}
else
{
design.end = Branch.laid.start;
design.end_direction = -outward;
design.end_grade = -Branch.laid.start_grade;
}
auto const &corners{record.result.vertices};
if (vertices.size() >= 2 && corners.size() == vertices.size())
{
// the corner moves along its other leg to the line from the switch
auto const corner{corners[k]};
auto const leg{corners[atstart ? 1 : k - 1] - corner};
auto const determinant{cross(outward, leg)};
if (std::abs(determinant) < 1e-9)
return;
auto const offset{cross(corner - plan_of(Branch.laid.start), leg) / determinant};
if (offset <= 0.0)
return;
vertices[k].offset = offset;
}
// the curve next to the switch takes the shape it was laid with
auto const &shape{Branch.laid.vertices.front()};
if (shape.compound || vertices[k].compound)
vertices[k] = scaled(vertices[k], Branch.factor);
else
{
vertices[k].radius = shape.radius;
vertices[k].transition_in = atstart ? shape.transition_in : shape.transition_out;
vertices[k].transition_out = atstart ? shape.transition_out : shape.transition_in;
}
auto const result{alignment::compute(design)};
if (false == result.valid || false == alignment_store::follows(result, Branch.part.tracks))
return;
// the km of the joint the paths are laid up to stays as it was
auto origin{record.origin};
if (atstart)
{
auto const before{alignment_store::chainage_on(record.result, joint)};
auto const after{alignment_store::chainage_on(result, joint)};
if (before.has_value() && after.has_value())
origin += *before - *after;
}
auto const layer{record.layer};
auto const source{record.source};
auto const fit{record.fit};
alignment_store::forget(static_cast<int>(i));
alignment_store::store(design, result, origin, layer, source, fit);
return;
}
}
bool editor_mode::move_switch_paths(TTrack &Track, std::vector<segment_data> const &Paths, std::string &Status)
{
if (Paths.size() < 2 || Track.m_paths.size() < 2 || false == editor_track::main_axis(Track).has_value())
@@ -199,6 +528,7 @@ bool editor_mode::move_switch_paths(TTrack &Track, std::vector<segment_data> con
for (auto const &joint : joints)
positions.push_back(editor_track::point_position(Track, joint.own));
std::vector<bool> handled(joints.size(), false);
auto branches{switch_branches(Track, joints)};
editor_track::edit edit;
edit.capture(Track);
auto const line{online ? editor_track::find_straight(Track, m_straights.tolerance) : editor_track::straight{}};
@@ -211,14 +541,56 @@ bool editor_mode::move_switch_paths(TTrack &Track, std::vector<segment_data> con
return false;
}
Track.m_paths.assign(Paths.begin(), Paths.begin() + 2);
auto const notes{plan_switch_branches(Track, joints, branches, handled)};
auto const loose{switch_rejoin(Track, joints, handled, edit)};
editor_track::commit(edit.changed);
for (auto const &branch : branches)
lay_switch_branch(branch, edit);
push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed));
if (false == loose.empty())
Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose;
if (false == notes.empty())
Status += (Status.empty() ? "" : "\n") + notes;
return true;
}
std::vector<editor_mode::switch_branch> editor_mode::switch_branches(TTrack const &Track, std::vector<editor_track::joint> const &Joints, std::vector<TTrack const *> const &Skip) const
{
std::vector<switch_branch> branches;
auto const axis{editor_track::main_axis(Track)};
if (false == axis.has_value())
return branches;
for (std::size_t i = 0; i < Joints.size(); ++i)
{
if (std::find(Skip.begin(), Skip.end(), Joints[i].other) != Skip.end() || axis->lateral(editor_track::point_position(Track, Joints[i].own)) <= m_straights.tolerance.offset)
continue;
if (auto branch{take_switch_branch(Track, Joints[i], i)})
branches.push_back(std::move(*branch));
}
return branches;
}
std::string editor_mode::plan_switch_branches(TTrack const &Track, std::vector<editor_track::joint> const &Joints, std::vector<switch_branch> &Branches, std::vector<bool> &Handled) const
{
std::string notes;
for (auto branch{Branches.begin()}; branch != Branches.end();)
{
auto const &joint{Joints[branch->joint]};
std::string reason;
if (false == plan_switch_branch(Track.m_paths[joint.own.path], joint, *branch) || false == tracks_editable(branch->part.tracks, reason))
{
notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch can't keep to it, %s follows the switch on its own"), track_label(*joint.other).c_str());
branch = Branches.erase(branch);
continue;
}
Handled[branch->joint] = true;
if (std::abs(branch->factor - 1.0) > 1e-4)
notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch keeps to it with the radius of %.1f m instead of %.1f m"), branch->laid.vertices.front().radius, branch->design.vertices.front().radius);
++branch;
}
return notes;
}
bool editor_mode::double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason)
{
double low{0.0};
@@ -288,6 +660,9 @@ bool editor_mode::move_double_paths(double_switch const &Set, std::vector<segmen
auto const joint{Set.first->m_paths.front().points[segment_data::point::end]};
auto const &link{Set.link->m_paths.front()};
bool const reversed{glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint)};
std::array<std::vector<switch_branch>, 2> branches;
for (std::size_t k = 0; k < 2; ++k)
branches[k] = switch_branches(*switches[k], joints[k], {Set.first, Set.link, Set.second});
editor_track::edit edit;
for (auto *track : {Set.first, Set.link, Set.second})
edit.capture(*track);
@@ -304,6 +679,13 @@ bool editor_mode::move_double_paths(double_switch const &Set, std::vector<segmen
Set.first->m_paths.assign(Paths.begin(), Paths.begin() + 2);
Set.second->m_paths.assign(Paths.begin() + 2, Paths.begin() + 4);
Set.link->m_paths = {reversed ? geometry::reversed(Paths[4]) : Paths[4]};
std::string notes;
for (std::size_t k = 0; k < 2; ++k)
{
auto const note{plan_switch_branches(*switches[k], joints[k], branches[k], handled[k])};
if (false == note.empty())
notes += (notes.empty() ? "" : "\n") + note;
}
std::string loose;
for (std::size_t k = 0; k < 2; ++k)
{
@@ -312,9 +694,14 @@ bool editor_mode::move_double_paths(double_switch const &Set, std::vector<segmen
loose += (loose.empty() ? "" : ", ") + left;
}
editor_track::commit(edit.changed);
for (auto const &set : branches)
for (auto const &branch : set)
lay_switch_branch(branch, edit);
push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed));
if (false == loose.empty())
Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose;
if (false == notes.empty())
Status += (Status.empty() ? "" : "\n") + notes;
return true;
}
@@ -404,6 +791,19 @@ void editor_mode::switch_slide_start(TTrack &Track)
slide.joints.push_back(axis->along(path.points[index]));
}
}
std::vector<TTrack const *> const skip(members.begin(), members.end());
for (auto *member : members)
{
if (member->eType != tt_Switch)
continue;
std::size_t const offset{set.has_value() && member == set->second ? 2u : 0u};
auto const joints{editor_track::joints(*member)};
for (auto &branch : switch_branches(*member, joints, skip))
{
auto const &joint{joints[branch.joint]};
slide.branches.push_back({joint, offset + static_cast<std::size_t>(joint.own.path), std::move(branch)});
}
}
}
void editor_mode::switch_slide_update()
@@ -465,16 +865,27 @@ void editor_mode::draw_switch_slide() const
}
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
for (auto const &path : paths)
{
bezier const curve{path};
auto const draw = [&](segment_data const &Path, ImU32 const Color, float const Width) {
bezier const curve{Path};
auto previous{curve.point(0.0)};
for (int i = 1; i <= 24; ++i)
{
auto const next{curve.point(i / 24.0)};
projection.line(drawlist, previous, next, overlay_color::marked, 3.0f);
projection.line(drawlist, previous, next, Color, Width);
previous = next;
}
};
for (auto const &path : paths)
draw(path, overlay_color::marked, 3.0f);
// the curves past the switch, laid again up to the straight beyond them
for (auto const &line : slide.branches)
{
if (line.path >= paths.size())
continue;
auto branch{line.branch};
if (plan_switch_branch(paths[line.path], line.joint, branch))
for (auto const &piece : branch.pieces)
draw(piece, overlay_color::grip, 2.0f);
}
ImVec2 at;
auto const start{paths.front().points[segment_data::point::start]};