17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 15:04:54 +02:00
Files
maszyna/editor/editorTrack.cpp
maj00r ff1710d9ea feat(editor): labels for a layman, light at the start; whole sceneries fitted to the terrain model
Labels:
- every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the
  sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track,
  grade), sides told from directions, degrees and per mille as signs, messages built from one format string
- plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header
- lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped
- roads: the notes and errors naming a road translated; the build options and the layout fold the texture length,
  kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the
  lanes start folded

Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once:
- fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone
  past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way
  is one point of intersection, a curve with no room is as sharp as the room lets it be
- the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what
  the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run
- apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into
  pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the
  ends of the route are brought to its height
- Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
2026-10-19 15:27:50 +02:00

3586 lines
127 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http:
*/
#include "stdafx.h"
#include "editor/editorTrack.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorAlignmentStore.hpp"
#include "world/Track.h"
#include "vehicle/DynObj.h"
#include "scene/scene.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "editor/editorIncludeInfo.hpp"
#include "world/Event.h"
#include "world/MemCell.h"
#include "rendering/renderer.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include "utilities/parser.h"
#include "utilities/Logs.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <deque>
#include <map>
#include <numeric>
#include <functional>
#include <cstdlib>
#include <sstream>
#include <tuple>
#include <unordered_set>
namespace
{
using geometry::bezier;
using geometry::chord_length;
using geometry::cross;
using geometry::plan_of;
using geometry::reversed;
using geometry::signed_angle;
using geometry::turned;
double const kSamePoint{geometry::same_point};
segment_data::point point_index(editor_track::point_kind const Kind)
{
switch (Kind)
{
case editor_track::point_kind::start: return segment_data::point::start;
case editor_track::point_kind::control1: return segment_data::point::control1;
case editor_track::point_kind::control2: return segment_data::point::control2;
default: return segment_data::point::end;
}
}
bool has_switch_paths(TTrack const &Track)
{
return Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross);
}
void add_unique(std::vector<TTrack *> &Tracks, TTrack *Track)
{
if (Track != nullptr && std::find(Tracks.begin(), Tracks.end(), Track) == Tracks.end())
Tracks.emplace_back(Track);
}
glm::dvec3 path_tangent(segment_data const &Path, bool const Atend)
{
auto const &start{Path.points[segment_data::point::start]};
auto const &end{Path.points[segment_data::point::end]};
auto const &control1{Path.points[segment_data::point::control1]};
auto const &control2{Path.points[segment_data::point::control2]};
if (false == Atend)
return control1 != glm::dvec3{} ? control1 : (end + control2) - start;
return control2 != glm::dvec3{} ? -control2 : end - (start + control1);
}
double path_radius(segment_data const &Path, double const T)
{
if (Path.points[segment_data::point::control1] == glm::dvec3{} && Path.points[segment_data::point::control2] == glm::dvec3{})
return 0.0;
bezier const curve{Path};
auto const first{curve.first(T)};
auto const second{curve.second(T)};
auto const speed{std::hypot(first.x, first.z)};
auto const curvature{std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed)};
return curvature > 1e-7 ? 1.0 / curvature : 0.0;
}
bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool const Leaving, glm::dvec3 &Direction, double &Radius)
{
if (Neighbour == nullptr)
return false;
for (auto const &path : Neighbour->m_paths)
{
for (auto const atend : {false, true})
{
auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]};
if (glm::distance(point, Joint) > kSamePoint)
continue;
auto const tangent{glm::normalize(path_tangent(path, atend))};
Direction = (Leaving != atend) ? tangent : -tangent;
Radius = path_radius(path, atend ? 1.0 : 0.0);
return true;
}
}
return false;
}
}
editor_track::observer *editor_track::s_observer{nullptr};
editor_track::state editor_track::capture(TTrack const &Track)
{
if (s_observer != nullptr)
s_observer->track_captured(Track);
state result;
result.paths = Track.m_paths;
result.velocity = Track.fVelocity;
result.switchvelocity = Track.SwitchExtension ? Track.SwitchExtension->fVelocity : -1.f;
result.width = Track.fTrackWidth;
result.friction = Track.fFriction;
result.sounddistance = Track.fSoundDistance;
result.quality = Track.iQualityFlag;
result.damage = Track.iDamageFlag;
result.environment = static_cast<int>(Track.eEnvironment);
result.material1 = Track.m_material1;
result.material2 = Track.m_material2;
result.material3 = Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle;
result.frogs = Track.SwitchExtension && Track.SwitchExtension->m_frogs;
result.movablenose = Track.SwitchExtension && Track.SwitchExtension->m_movablenose;
result.bladeheel = Track.SwitchExtension ? Track.SwitchExtension->m_bladeheel : 0.0;
result.bladethrow = Track.SwitchExtension ? Track.SwitchExtension->m_bladethrow : 0.0;
result.texlength = Track.fTexLength;
result.texheight = texture_height(Track);
result.texwidth = Track.fTexWidth;
result.texslope = Track.fTexSlope;
for (auto const *isolated : Track.Isolated)
result.isolated.push_back(isolated->asName);
result.overhead = Track.fOverhead;
result.sleepers = Track.m_sleeper_enabled;
result.sleeper_frequency = Track.m_sleeper_frequency;
result.sleeper_model = Track.m_sleeper_model_name;
result.sleeper_skin = Track.m_sleeper_skin_name;
result.sleeper_offset = Track.m_sleeper_offset;
result.sleeper_ballast = Track.m_sleeper_ballast_z;
result.timber_model = Track.m_timber_model_name;
result.timber_skin = Track.m_timber_skin_name;
result.rail_profile = Track.m_profile1;
for (int i = 0; i < 6; ++i)
result.events[i] = events(const_cast<TTrack &>(Track), i);
result.name = Track.name();
return result;
}
std::vector<std::pair<std::string, basic_event *>> &editor_track::events(TTrack &Track, int const Index)
{
switch (Index)
{
case 1: return Track.m_events1;
case 2: return Track.m_events2;
case 3: return Track.m_events0all;
case 4: return Track.m_events1all;
case 5: return Track.m_events2all;
default: return Track.m_events0;
}
}
char const *editor_track::event_keyword(int const Index)
{
char const *const keywords[] = {"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"};
return keywords[std::clamp(Index, 0, 5)];
}
void editor_track::bind_events(TTrack &Track, std::string &Missing)
{
Missing.clear();
Track.m_events = false;
for (int i = 0; i < 6; ++i)
for (auto &event : events(Track, i))
{
// the ones bound by the name of the path have no name of their own
if (false == event.first.empty())
event.second = simulation::Events.FindEvent(event.first);
if (event.second != nullptr)
Track.m_events = true;
else
Missing += (Missing.empty() ? "" : ", ") + event.first;
}
}
void editor_track::isolated(TTrack &Track, std::vector<std::string> const &Names)
{
Track.Isolated.clear();
for (auto const &name : Names)
{
if (name.empty())
continue;
bool known{false};
for (auto *existing{TIsolated::Root()}; existing != nullptr && false == known; existing = existing->Next())
known = (existing->asName == name);
auto *isolated{TIsolated::Find(name)};
if (false == known)
isolated->AssignEvents();
Track.Isolated.push_back(isolated);
}
}
void editor_track::overhead(TTrack &Track, float const Overhead)
{
Track.fOverhead = Overhead;
if (Overhead > 0.f)
Track.iAction |= 0x40;
else
Track.iAction &= ~0x40;
}
void editor_track::apply(TTrack &Track, state const &State)
{
Track.m_paths = State.paths;
Track.fVelocity = State.velocity;
if (Track.SwitchExtension)
{
Track.SwitchExtension->fVelocity = State.switchvelocity;
Track.SwitchExtension->m_material3 = State.material3;
Track.SwitchExtension->m_frogs = State.frogs;
Track.SwitchExtension->m_movablenose = State.movablenose;
Track.SwitchExtension->m_bladeheel = State.bladeheel;
Track.SwitchExtension->m_bladethrow = State.bladethrow;
}
Track.fTrackWidth = State.width;
Track.fTrackWidth2 = State.width;
Track.fFriction = State.friction;
Track.fSoundDistance = State.sounddistance;
Track.iQualityFlag = State.quality;
damage(Track, State.damage);
Track.eEnvironment = static_cast<TEnvironmentType>(State.environment);
Track.m_material1 = State.material1;
Track.m_material2 = State.material2;
Track.fTexLength = State.texlength;
texture_height(Track, State.texheight);
Track.fTexWidth = State.texwidth;
Track.fTexSlope = State.texslope;
isolated(Track, State.isolated);
overhead(Track, State.overhead);
Track.m_sleeper_enabled = State.sleepers;
Track.m_sleeper_frequency = State.sleeper_frequency;
Track.m_sleeper_model_name = State.sleeper_model;
Track.m_sleeper_skin_name = State.sleeper_skin;
Track.m_sleeper_offset = State.sleeper_offset;
Track.m_sleeper_ballast_z = State.sleeper_ballast;
Track.m_timber_model_name = State.timber_model;
Track.m_timber_skin_name = State.timber_skin;
Track.m_profile1 = State.rail_profile;
for (int i = 0; i < 6; ++i)
events(Track, i) = State.events[i];
Track.m_events = std::any_of(State.events.begin(), State.events.end(), [](auto const &Events) {
return std::any_of(Events.begin(), Events.end(), [](auto const &Event) { return Event.second != nullptr; });
});
if (State.name != Track.name())
rename(Track, State.name);
}
namespace
{
std::string lower_name(std::string Name)
{
for (auto &character : Name)
if (character >= 'A' && character <= 'Z')
character = static_cast<char>(character - 'A' + 'a');
return Name;
}
int track_parameter(scene::instance_handle const Instance, std::string &File, std::vector<std::string> &Values)
{
if (false == scene::Layers.tracked(Instance))
return -1;
auto const &included{scene::Layers.instance(Instance)};
if (included.removed || included.dead || included.file == nullptr)
return -1;
static std::map<std::string, include_info> descriptions;
auto found{descriptions.find(*included.file)};
if (found == descriptions.end())
{
include_info info;
std::string error;
editor_includes::load(*included.file, info, error);
found = descriptions.emplace(*included.file, std::move(info)).first;
}
auto const index{editor_includes::parameter_with_role(found->second, "track") - 1};
if (index < 0 || false == editor_includes::parse_directive(scene::Layers.directive(Instance), File, Values) || index >= static_cast<int>(Values.size()))
return -1;
return index;
}
} // namespace
bool editor_track::name_valid(TTrack const &Track, std::string &Name, std::string &Reason, TTrack **Owner)
{
Name = lower_name(Name);
if (Name.empty() || Name == "none")
{
Reason = "The path needs a name";
return false;
}
if (Name.find_first_of(" \t\r\n\"';") != std::string::npos || Name.find("//") != std::string::npos)
{
Reason = "The name can't hold blanks, quotes, semicolons or //";
return false;
}
auto *owner{simulation::Paths.find(Name)};
if (owner != nullptr && owner != &Track && false == owner->m_editorremoved)
{
if (Owner != nullptr)
*Owner = owner;
Reason = "Another path has this name";
return false;
}
Reason.clear();
return true;
}
editor_track::name_uses editor_track::uses_of_name(TTrack const &Track)
{
name_uses result;
auto const &name{Track.name()};
if (name.empty() || name == "none")
return result;
for (auto const *cell : simulation::Memory.sequence())
result.cells += (cell != nullptr && cell->Track == &Track) ? 1 : 0;
for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle)
{
std::string file;
std::vector<std::string> values;
auto const index{track_parameter(handle, file, values)};
result.includes += (index >= 0 && lower_name(values[index]) == name) ? 1 : 0;
}
static std::array<char const *, 6> const kinds{"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"};
for (auto const *event : simulation::Events.sequence())
{
if (event == nullptr)
continue;
auto const &eventname{event->name()};
if (std::any_of(kinds.begin(), kinds.end(), [&](char const *Kind) { return eventname == name + ':' + Kind; }))
{
result.loose.push_back(eventname);
continue;
}
if (result.includes > 0 && eventname.compare(0, name.size(), name) == 0)
continue;
auto const targets{event->target_nodes()};
if (std::find(targets.begin(), targets.end(), &Track) != targets.end())
result.events.push_back(eventname);
}
return result;
}
void editor_track::rename(TTrack &Track, std::string const &Name)
{
auto const old{Track.name()};
if (old == Name)
return;
simulation::Paths.rename(&Track, Name);
Track.m_name = Name;
Track.mark_dirty();
for (auto *cell : simulation::Memory.sequence())
if (cell != nullptr && cell->Track == &Track)
{
cell->asTrackName = Name;
cell->mark_dirty();
}
if (old.empty() || old == "none")
return;
for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle)
{
std::string file;
std::vector<std::string> values;
auto const index{track_parameter(handle, file, values)};
if (index < 0 || lower_name(values[index]) != old)
continue;
values[index] = Name;
scene::Layers.modify(handle, editor_includes::compose_directive(file, values));
}
}
bool editor_track::is_supported(TTrack const &Track)
{
if ((Track.iCategoryFlag & 0x80) != 0)
return false;
// lanes are generated from their road, and change only along with it
if (Track.m_road != nullptr)
return false;
if (Track.eType == tt_Normal)
return Track.Segment != nullptr;
if (Track.eType == tt_Switch)
return Track.SwitchExtension != nullptr && Track.m_paths.size() >= 2;
return false;
}
bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason)
{
if (false == is_supported(Track))
{
Reason = "Editing of this path type isn't supported";
return false;
}
if (Global.NvRenderer)
{
Reason = "Path geometry can't be rebuilt with the experimental renderer";
return false;
}
std::string why;
if (false == scene::Layers.editable(&Track, &why))
{
Reason = "The path can't be changed: " + why;
return false;
}
Reason.clear();
return true;
}
void editor_track::commit(std::vector<TTrack *> const &Tracks)
{
if (Tracks.empty())
return;
std::vector<TTrack *> affected;
for (auto *track : Tracks)
{
add_unique(affected, track);
for (auto *neighbour : neighbours(*track))
add_unique(affected, neighbour);
}
for (auto *track : Tracks)
{
disconnect(*track);
simulation::Region->erase_and_unregister(track);
}
for (auto *track : Tracks)
{
for (auto &path : track->m_paths)
if (false == path.pieces.empty() &&
(glm::distance(path.pieces.front().points[segment_data::point::start], path.points[segment_data::point::start]) > 0.02 ||
glm::distance(path.pieces.back().points[segment_data::point::end], path.points[segment_data::point::end]) > 0.02))
path.pieces.clear();
track->init_segments(false);
track->update_location();
track->m_area.radius = -1.f;
simulation::Region->insert_and_register(track);
}
for (auto *track : Tracks)
join(*track);
for (auto *track : Tracks)
for (auto *neighbour : neighbours(*track))
add_unique(affected, neighbour);
for (auto *track : affected)
update_transition(*track);
for (auto *track : affected)
rebuild_geometry(*track);
for (auto *track : Tracks)
track->mark_dirty();
for (auto *track : affected)
for (auto *vehicle : track->Dynamics)
vehicle->Move(0.000001);
if (s_observer != nullptr)
s_observer->tracks_committed(Tracks);
}
void editor_track::commit_parameters(TTrack &Track)
{
Track.fTrackWidth2 = Track.fTrackWidth;
Track.init_segments(false);
std::vector<TTrack *> affected{&Track};
for (auto *neighbour : neighbours(Track))
add_unique(affected, neighbour);
for (auto *track : affected)
update_transition(*track);
for (auto *track : affected)
rebuild_geometry(*track);
Track.mark_dirty();
}
glm::dvec3 editor_track::point_position(TTrack const &Track, point_ref const &Point)
{
if (Point.path < 0 || Point.path >= static_cast<int>(Track.m_paths.size()))
return Track.location();
auto const &path{Track.m_paths[Point.path]};
auto const &start{path.points[segment_data::point::start]};
auto const &end{path.points[segment_data::point::end]};
switch (Point.kind)
{
case point_kind::start: return start;
case point_kind::end: return end;
case point_kind::control1:
{
auto const &control{path.points[segment_data::point::control1]};
return control == glm::dvec3{} ? start + (end - start) / 3.0 : start + control;
}
case point_kind::control2:
{
auto const &control{path.points[segment_data::point::control2]};
return control == glm::dvec3{} ? end + (start - end) / 3.0 : end + control;
}
}
return start;
}
void editor_track::move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position)
{
if (Point.path < 0 || Point.path >= static_cast<int>(Track.m_paths.size()))
return;
auto &path{Track.m_paths[Point.path]};
path.pieces.clear();
switch (Point.kind)
{
case point_kind::start:
case point_kind::end:
{
auto &point{path.points[point_index(Point.kind)]};
auto const oldposition{point};
point = Position;
for (auto &otherpath : Track.m_paths)
{
if (&otherpath == &path)
continue;
for (auto const index : {segment_data::point::start, segment_data::point::end})
if (glm::distance(otherpath.points[index], oldposition) < kSamePoint)
otherpath.points[index] = Position;
}
break;
}
case point_kind::control1:
{
auto control{Position - path.points[segment_data::point::start]};
path.points[segment_data::point::control1] = (glm::length(control) < 0.01 ? glm::dvec3{} : control);
break;
}
case point_kind::control2:
{
auto control{Position - path.points[segment_data::point::end]};
path.points[segment_data::point::control2] = (glm::length(control) < 0.01 ? glm::dvec3{} : control);
break;
}
}
}
void editor_track::translate(TTrack &Track, glm::dvec3 const &Offset)
{
for (auto &path : Track.m_paths)
path.translate(Offset);
}
void editor_track::rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle)
{
auto const c{std::cos(Angle)};
auto const s{std::sin(Angle)};
auto const rotate = [&](glm::dvec3 const &Vector) { return glm::dvec3{Vector.x * c + Vector.z * s, Vector.y, -Vector.x * s + Vector.z * c}; };
for (auto &path : Track.m_paths)
path.transform([&](glm::dvec3 const &Point) { return Pivot + rotate(Point - Pivot); }, rotate);
}
glm::dvec3 editor_track::pivot(TTrack const &Track)
{
return Track.location();
}
std::vector<TTrack *> editor_track::neighbours(TTrack const &Track)
{
std::vector<TTrack *> result;
add_unique(result, Track.trPrev);
add_unique(result, Track.trNext);
if (has_switch_paths(Track))
{
for (int i = 0; i < 2; ++i)
{
add_unique(result, Track.SwitchExtension->pPrevs[i]);
add_unique(result, Track.SwitchExtension->pNexts[i]);
}
}
result.erase(std::remove(result.begin(), result.end(), &Track), result.end());
return result;
}
std::vector<std::pair<TTrack *, editor_track::point_ref>> editor_track::connected_points(TTrack const &Track, glm::dvec3 const &Position)
{
std::vector<std::pair<TTrack *, point_ref>> result;
for (auto *neighbour : neighbours(Track))
{
for (int i = 0; i < static_cast<int>(neighbour->m_paths.size()); ++i)
{
for (auto const kind : {point_kind::start, point_kind::end})
{
if (glm::distance(neighbour->m_paths[i].points[point_index(kind)], Position) < kSamePoint)
result.emplace_back(neighbour, point_ref{i, kind});
}
}
}
return result;
}
namespace
{
// direction of the path at the end, into the path
glm::dvec2 end_direction(segment_data const &Path, editor_track::point_kind const Kind)
{
bool const start{Kind == editor_track::point_kind::start};
auto const &point{Path.points[start ? segment_data::point::start : segment_data::point::end]};
auto const &control{Path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
auto const direction{plan_of(control != glm::dvec3{} ? control : Path.points[start ? segment_data::point::end : segment_data::point::start] - point)};
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0};
}
} // namespace
std::vector<editor_track::joint> editor_track::joints(TTrack const &Track)
{
std::vector<joint> result;
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
{
for (auto const kind : {point_kind::start, point_kind::end})
{
point_ref const own{i, kind};
for (auto const &connection : connected_points(Track, point_position(Track, own)))
{
auto *other{connection.first};
std::string reason;
if (has_switch_paths(*other) || false == can_edit_geometry(*other, reason))
continue;
if (std::any_of(result.begin(), result.end(), [&](joint const &Joint) { return Joint.other == other && Joint.theirs == connection.second; }))
continue;
auto const &path{other->m_paths[connection.second.path]};
bool const start{connection.second.kind == point_kind::start};
auto control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
if (control == glm::dvec3{})
control = (path.points[start ? segment_data::point::end : segment_data::point::start] - path.points[start ? segment_data::point::start : segment_data::point::end]) / 3.0;
result.push_back({own, other, connection.second, end_direction(Track.m_paths[i], kind), control});
}
}
}
return result;
}
void editor_track::follow(TTrack const &Track, std::vector<joint> const &Joints)
{
for (auto const &joint : Joints)
{
if (joint.own.path >= static_cast<int>(Track.m_paths.size()) || joint.theirs.path >= static_cast<int>(joint.other->m_paths.size()))
continue;
auto &path{joint.other->m_paths[joint.theirs.path]};
auto &control{path.points[joint.theirs.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2]};
bool const straight{path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}};
auto const fixed{path.points[joint.theirs.kind == point_kind::start ? segment_data::point::end : segment_data::point::start]};
auto const before{plan_of(path.points[point_index(joint.theirs.kind)] - fixed)};
auto const target{point_position(Track, joint.own)};
move_point(*joint.other, joint.theirs, target);
auto const angle{signed_angle(joint.direction, end_direction(Track.m_paths[joint.own.path], joint.own.kind))};
if (straight && std::abs(angle) < 1e-6 && glm::length(before) > 1e-6)
{
auto const after{plan_of(target - fixed)};
auto const axis{glm::normalize(before)};
if (std::abs(axis.x * after.y - axis.y * after.x) < 1e-3 && glm::dot(axis, after) > 0.0)
continue;
}
auto const moved{turned(plan_of(joint.control), angle)};
control = {moved.x, joint.control.y, moved.y};
}
}
void editor_track::follow_grade(TTrack const &Track, std::vector<joint> const &Joints)
{
for (auto const &joint : Joints)
{
if (joint.own.path >= static_cast<int>(Track.m_paths.size()) || joint.theirs.path >= static_cast<int>(joint.other->m_paths.size()))
continue;
geometry::bezier const own{Track.m_paths[joint.own.path]};
auto const outward{joint.own.kind == point_kind::start ? -own.first(0.0) : own.first(1.0)};
auto &control{joint.other->m_paths[joint.theirs.path].points[joint.theirs.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2]};
control.y = geometry::grade_of(outward) * glm::length(geometry::plan_of(control));
}
}
namespace
{
struct surface_foot
{
double t{0.0};
double along{0.0};
double lateral{0.0};
glm::dvec3 base{0.0};
glm::dvec2 direction{0.0, 1.0};
double grade{0.0};
};
surface_foot foot_on(segment_data const &Path, glm::dvec3 const &Position)
{
bezier const curve{Path};
auto const distance = [&](double const T) { return glm::distance(plan_of(curve.point(T)), plan_of(Position)); };
int constexpr samples{200};
double best{std::numeric_limits<double>::max()};
int nearest{0};
for (int i = 0; i <= samples; ++i)
if (auto const d{distance(static_cast<double>(i) / samples)}; d < best)
{
best = d;
nearest = i;
}
auto low{std::max(0, nearest - 1) / static_cast<double>(samples)};
auto high{std::min(samples, nearest + 1) / static_cast<double>(samples)};
for (int i = 0; i < 40; ++i)
{
auto const a{low + (high - low) / 3.0};
auto const b{high - (high - low) / 3.0};
if (distance(a) < distance(b))
high = b;
else
low = a;
}
surface_foot foot;
foot.t = std::clamp((low + high) * 0.5, 0.0, 1.0);
foot.base = curve.point(foot.t);
auto const tangent{curve.first(foot.t)};
auto const plan{plan_of(tangent)};
auto const run{glm::length(plan)};
if (run > 1e-9)
{
foot.direction = plan / run;
foot.grade = tangent.y / run;
}
auto const offset{plan_of(Position) - plan_of(foot.base)};
foot.along = glm::dot(offset, foot.direction);
foot.lateral = glm::dot(offset, glm::dvec2{-foot.direction.y, foot.direction.x});
return foot;
}
double foot_roll(segment_data const &Path, surface_foot const &Foot)
{
return Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * Foot.t;
}
double surface_height(segment_data const &Path, surface_foot const &Foot)
{
return Foot.base.y + Foot.along * Foot.grade - Foot.lateral * std::tan(glm::radians(foot_roll(Path, Foot)));
}
} // namespace
void editor_track::conform_switch(TTrack &Track)
{
if (Track.eType != tt_Switch || Track.m_paths.size() != 2)
return;
auto const &main{Track.m_paths[0]};
auto const height = [&](glm::dvec3 const &Point) { return surface_height(main, foot_on(main, Point)); };
auto const slope = [&](glm::dvec3 const &Point, glm::dvec3 const &Control) {
auto const plan{plan_of(Control)};
auto const run{glm::length(plan)};
if (run < 1e-9)
return 0.0;
double constexpr step{0.05};
glm::dvec3 const offset{plan.x / run * step, 0.0, plan.y / run * step};
return (4.0 * height(Point + offset) - 3.0 * height(Point) - height(Point + 2.0 * offset)) / (2.0 * step) * run;
};
auto const conform = [&](segment_data &Path) {
auto &start{Path.points[segment_data::point::start]};
auto &end{Path.points[segment_data::point::end]};
auto &control1{Path.points[segment_data::point::control1]};
auto &control2{Path.points[segment_data::point::control2]};
if (control1 == glm::dvec3{} && control2 == glm::dvec3{})
{
control1 = (end - start) / 3.0;
control2 = (start - end) / 3.0;
}
start.y = height(start);
end.y = height(end);
control1.y = slope(start, control1);
control2.y = slope(end, control2);
bezier const curve{Path};
for (int i = 0; i < 2; ++i)
{
auto const foot{foot_on(main, i == 0 ? start : end)};
auto const along{glm::dot(plan_of(curve.first(i)), foot.direction) >= 0.0 ? 1.0 : -1.0};
Path.rolls[i] = static_cast<float>(foot_roll(main, foot) * along);
}
};
auto &path{Track.m_paths[1]};
conform(path);
for (auto &piece : path.pieces)
conform(piece);
}
double editor_track::switch_station(TTrack const &Track, glm::dvec3 const &Position)
{
if (Track.m_paths.empty())
return 0.0;
auto const &main{Track.m_paths[0]};
auto const foot{foot_on(main, Position)};
bezier const curve{main};
int constexpr steps{64};
double length{0.0};
auto previous{plan_of(curve.point(0.0))};
for (int i = 1; i <= steps; ++i)
{
auto const next{plan_of(curve.point(foot.t * i / steps))};
length += glm::distance(previous, next);
previous = next;
}
return length + foot.along;
}
std::vector<int> editor_track::hold_controls(TTrack &Track)
{
std::vector<int> result;
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
{
auto &path{Track.m_paths[i]};
auto &control1{path.points[segment_data::point::control1]};
auto &control2{path.points[segment_data::point::control2]};
if (control1 != glm::dvec3{} || control2 != glm::dvec3{})
continue;
auto const chord{path.points[segment_data::point::end] - path.points[segment_data::point::start]};
if (glm::length(chord) < 1e-6)
continue;
control1 = chord / 3.0;
control2 = -chord / 3.0;
result.push_back(i);
}
return result;
}
void editor_track::settle_controls(TTrack &Track, std::vector<int> const &Paths)
{
for (auto const index : Paths)
{
if (index < 0 || index >= static_cast<int>(Track.m_paths.size()))
continue;
auto &path{Track.m_paths[index]};
auto const chord{path.points[segment_data::point::end] - path.points[segment_data::point::start]};
auto const length{glm::length(chord)};
if (length < 1e-6)
continue;
auto const axis{chord / length};
auto const &control1{path.points[segment_data::point::control1]};
auto const &control2{path.points[segment_data::point::control2]};
auto const off = [&](glm::dvec3 const &Control) { return glm::length(Control - axis * glm::dot(Control, axis)); };
if (glm::dot(control1, axis) <= 0.0 || glm::dot(control2, axis) >= 0.0 || off(control1) > 1e-3 || off(control2) > 1e-3)
continue;
path.points[segment_data::point::control1] = glm::dvec3{};
path.points[segment_data::point::control2] = glm::dvec3{};
}
}
editor_track::point_ref editor_track::end_of(point_ref const &Control)
{
switch (Control.kind)
{
case point_kind::control1: return {Control.path, point_kind::start};
case point_kind::control2: return {Control.path, point_kind::end};
default: return Control;
}
}
std::vector<std::pair<TTrack *, editor_track::point_ref>> editor_track::partners(TTrack &Track, point_ref const &End)
{
auto const position{point_position(Track, End)};
auto result{connected_points(Track, position)};
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
{
if (i == End.path)
continue;
for (auto const kind : {point_kind::start, point_kind::end})
if (glm::distance(Track.m_paths[i].points[point_index(kind)], position) < kSamePoint)
result.emplace_back(&Track, point_ref{i, kind});
}
return result;
}
void editor_track::align_controls(TTrack const &Track, point_ref const &Control, std::vector<std::pair<TTrack *, point_ref>> const &Partners)
{
auto const own{point_position(Track, Control) - point_position(Track, end_of(Control))};
if (glm::length(own) < 1e-6)
return;
auto const direction{glm::normalize(own)};
for (auto const &partner : Partners)
{
auto &path{partner.first->m_paths[partner.second.path]};
bool const start{partner.second.kind == point_kind::start};
auto &control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
auto const length{control != glm::dvec3{} ? glm::length(control) : glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end]) / 3.0};
auto const same{partner.first == &Track && partner.second.kind == end_of(Control).kind};
control = (same ? direction : -direction) * length;
}
}
bool editor_track::is_connected(TTrack const &Track, point_ref const &Point)
{
if (false == editor_track::is_end(Point.kind))
return false;
return false == connected_points(Track, point_position(Track, Point)).empty();
}
editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector<TTrack const *> const &Exclude)
{
return find_free_end(&Track, Track.iCategoryFlag & 15, Position, Radius, Exclude);
}
editor_track::snap_target editor_track::find_free_end(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector<TTrack const *> const &Exclude)
{
auto const found{free_ends(Self, Category, Position, Radius, Exclude)};
return found.empty() ? snap_target{} : found.front();
}
std::vector<editor_track::snap_target> editor_track::free_ends(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector<TTrack const *> const &Exclude)
{
std::vector<snap_target> result;
auto const excluded = [&](TTrack const *Other) { return std::find(Exclude.begin(), Exclude.end(), Other) != Exclude.end(); };
auto const sections{simulation::Region->sections(Position, static_cast<float>(Radius))};
for (auto *section : sections)
{
for (auto const &cell : section->m_cells)
{
for (auto *other : cell.m_directories.paths)
{
if (other == Self || excluded(other) || other->m_editorremoved || (other->iCategoryFlag & 15) != Category || false == is_supported(*other))
continue;
for (int i = 0; i < static_cast<int>(other->m_paths.size()); ++i)
{
auto const &path{other->m_paths[i]};
for (auto const kind : {point_kind::start, point_kind::end})
{
auto const &point{path.points[point_index(kind)]};
auto const distance{glm::distance(point, Position)};
if (distance > Radius)
continue;
if (std::any_of(result.begin(), result.end(), [&](snap_target const &Found) { return Found.track == other && Found.point == point_ref{i, kind}; }))
continue;
auto const connections{connected_points(*other, point)};
if (std::any_of(connections.begin(), connections.end(), [&](auto const &Connection) { return false == excluded(Connection.first); }))
continue;
glm::dvec3 direction;
if (kind == point_kind::start)
{
auto const &control{path.points[segment_data::point::control1]};
direction = control != glm::dvec3{} ? control : path.points[segment_data::point::end] - point;
}
else
{
auto const &control{path.points[segment_data::point::control2]};
direction = control != glm::dvec3{} ? control : path.points[segment_data::point::start] - point;
}
if (glm::length(direction) < 1e-6)
continue;
snap_target target;
target.track = other;
target.point = point_ref{i, kind};
target.position = point;
target.direction = glm::normalize(direction);
target.distance = distance;
result.push_back(target);
}
}
}
}
}
std::sort(result.begin(), result.end(), [](snap_target const &A, snap_target const &B) { return A.distance < B.distance; });
return result;
}
void editor_track::snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent)
{
if (Target.track == nullptr || false == editor_track::is_end(Point.kind))
return;
move_point(Track, Point, Target.position);
if (false == Aligntangent)
return;
auto &path{Track.m_paths[Point.path]};
auto const &start{path.points[segment_data::point::start]};
auto const &end{path.points[segment_data::point::end]};
auto &control{path.points[Point.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2]};
auto const length{control != glm::dvec3{} ? glm::length(control) : glm::distance(start, end) / 3.0};
control = -Target.direction * length;
}
std::string editor_track::material_name(material_handle const Material)
{
if (Material == null_handle)
return "none";
auto name{GfxRenderer->Material(Material)->GetName()};
if (name.find(paths::textures) == 0)
name.erase(0, std::string{paths::textures}.size());
return name;
}
material_handle editor_track::fetch_material(std::string const &Name)
{
if (Name.empty() || Name == "none")
return null_handle;
auto name{Name};
replace_slashes(name);
return GfxRenderer->Fetch_Material(name);
}
float editor_track::texture_height(TTrack const &Track)
{
return (Track.fTexHeight1 - Track.fTexHeightOffset) * (Track.iCategoryFlag & 4 ? -1.f : 1.f);
}
void editor_track::texture_height(TTrack &Track, float const Height)
{
Track.fTexHeight1 = Height * (Track.iCategoryFlag & 4 ? -1.f : 1.f) + Track.fTexHeightOffset;
}
double editor_track::velocity(TTrack const &Track)
{
if (Track.SwitchExtension && Track.eType == tt_Switch)
return Track.SwitchExtension->fVelocity;
return Track.fVelocity;
}
void editor_track::velocity(TTrack &Track, double const Velocity)
{
if (Track.SwitchExtension && Track.eType == tt_Switch)
{
Track.SwitchExtension->fVelocity = static_cast<float>(Velocity);
if (Velocity <= -2.0)
Track.fVelocity = Track.SwitchExtension->CurrentIndex ? -Velocity : -1.0;
else
Track.fVelocity = Velocity;
return;
}
Track.fVelocity = Velocity;
}
void editor_track::damage(TTrack &Track, int const Damage)
{
Track.iDamageFlag = Damage;
if (Damage & 128)
Track.iAction |= 0x80;
else
Track.iAction &= ~0x80;
}
void editor_track::disconnect(TTrack &Track)
{
for (auto *neighbour : neighbours(Track))
{
if (has_switch_paths(*neighbour))
{
auto &extension{*neighbour->SwitchExtension};
for (int i = 0; i < 2; ++i)
{
if (extension.pPrevs[i] == &Track)
extension.pPrevs[i] = nullptr;
if (extension.pNexts[i] == &Track)
extension.pNexts[i] = nullptr;
}
bind_switch_path(*neighbour, extension.CurrentIndex);
}
else
{
if (neighbour->trPrev == &Track)
neighbour->trPrev = nullptr;
if (neighbour->trNext == &Track)
neighbour->trNext = nullptr;
}
}
Track.trPrev = nullptr;
Track.trNext = nullptr;
if (has_switch_paths(Track))
{
auto &extension{*Track.SwitchExtension};
for (int i = 0; i < 2; ++i)
{
extension.pPrevs[i] = nullptr;
extension.pNexts[i] = nullptr;
}
}
}
void editor_track::join(TTrack &Track)
{
TTrack *other;
int endpointid;
switch (Track.eType)
{
case tt_Normal:
{
if (Track.trPrev == nullptr)
{
std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_0(), &Track);
connect(Track, true, other, endpointid);
}
if (Track.trNext == nullptr)
{
std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_1(), &Track);
connect(Track, false, other, endpointid);
}
break;
}
case tt_Switch:
{
for (int i = 0; i < 2; ++i)
{
auto const &segment{Track.SwitchExtension->Segments[i]};
for (auto const &point : {segment->FastGetPoint_0(), segment->FastGetPoint_1()})
{
std::tie(other, endpointid) = simulation::Region->find_path(point, &Track);
if (other != nullptr && other->eType == tt_Normal)
join(*other);
}
}
break;
}
default:
break;
}
}
void editor_track::connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid)
{
if (Other == nullptr)
return;
switch (Endpointid)
{
case 0:
if (Prevside)
Track.ConnectPrevPrev(Other, 0);
else
Track.ConnectNextPrev(Other, 0);
break;
case 1:
if (Prevside)
Track.ConnectPrevNext(Other, 1);
else
Track.ConnectNextNext(Other, 1);
break;
case 2:
case 3:
case 4:
case 5:
{
if (false == has_switch_paths(*Other))
break;
auto const path{(Endpointid - 2) / 2};
auto const atend{((Endpointid - 2) % 2) != 0};
bind_switch_path(*Other, path);
if (Prevside)
{
if (atend)
Track.ConnectPrevNext(Other, path ? 3 : 1);
else
Track.ConnectPrevPrev(Other, path ? 2 : 0);
}
else
{
if (atend)
Track.ConnectNextNext(Other, path ? 3 : 1);
else
Track.ConnectNextPrev(Other, path ? 2 : 0);
}
store_switch_path(*Other, path);
bind_switch_path(*Other, Other->SwitchExtension->CurrentIndex);
break;
}
default:
break;
}
}
void editor_track::update_transition(TTrack &Track)
{
Track.iTrapezoid &= ~2;
auto const *next{Track.trNext};
if (next == nullptr || Track.iNextDirection != 0)
return;
if (Track.m_visible && next->m_visible && Track.eType == tt_Normal && next->eType == tt_Normal)
{
if (Track.fTrackWidth != next->fTrackWidth || Track.fTexHeight1 != next->fTexHeight1 || Track.fTexWidth != next->fTexWidth || Track.fTexSlope != next->fTexSlope)
Track.iTrapezoid |= 2;
}
}
void editor_track::bind_switch_path(TTrack &Switch, int const Path)
{
auto &extension{*Switch.SwitchExtension};
auto const path{Path & 1};
Switch.Segment = extension.Segments[path];
Switch.trNext = extension.pNexts[path];
Switch.trPrev = extension.pPrevs[path];
Switch.iNextDirection = extension.iNextDirection[path];
Switch.iPrevDirection = extension.iPrevDirection[path];
}
void editor_track::store_switch_path(TTrack &Switch, int const Path)
{
auto &extension{*Switch.SwitchExtension};
auto const path{Path & 1};
extension.pNexts[path] = Switch.trNext;
extension.pPrevs[path] = Switch.trPrev;
extension.iNextDirection[path] = Switch.iNextDirection;
extension.iPrevDirection[path] = Switch.iPrevDirection;
if (Switch.eType == tt_Switch)
{
extension.pPrevs[path ^ 1] = Switch.trPrev;
extension.iPrevDirection[path ^ 1] = Switch.iPrevDirection;
}
}
bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error, bool const Switches)
{
Chain = {};
if (From == nullptr || To == nullptr)
{
Error = "Select the first and the last path of the fragment";
return false;
}
auto const regular = [](TTrack const *Track) { return Track->eType == tt_Normal && is_supported(*Track); };
if (false == regular(From) || false == regular(To))
{
Error = "The fragment has to consist of regular paths, without switches";
return false;
}
Chain.tracks = {From};
Chain.forward = {true};
if (From != To)
{
bool found{false};
for (auto const throughend : {true, false})
{
Chain.tracks = {From};
Chain.forward = {throughend};
Chain.switches.clear();
TTrack *previous{From};
TTrack *current{throughend ? From->trNext : From->trPrev};
while (current != nullptr && current != From && Chain.tracks.size() + Chain.switches.size() < 10000)
{
if (Switches && current->eType == tt_Switch && has_switch_paths(*current) && is_supported(*current))
{
// only along the main track, the way in decides the way out
auto const &extension{*current->SwitchExtension};
bool const atstart{extension.pPrevs[0] == previous};
if (atstart == (extension.pNexts[0] == previous))
break;
Chain.switches.push_back({current, atstart, Chain.tracks.size()});
previous = current;
current = atstart ? extension.pNexts[0] : extension.pPrevs[0];
continue;
}
if (false == regular(current))
break;
auto const entersatstart{current->trPrev == previous};
Chain.tracks.push_back(current);
Chain.forward.push_back(entersatstart);
if (current == To)
{
found = true;
break;
}
previous = current;
current = entersatstart ? current->trNext : current->trPrev;
}
if (found)
break;
}
if (false == found)
{
Chain = {};
Error = Switches ? "The selected paths aren't connected along the main tracks of the switches" : "The selected paths aren't connected by a chain of regular paths (switches end the chain)";
return false;
}
}
for (auto const &passage : Chain.switches)
{
if ((passage.track->iCategoryFlag & 15) != (From->iCategoryFlag & 15))
{
Error = "The fragment mixes different kinds of paths";
Chain = {};
return false;
}
Chain.length += bezier{passage.track->m_paths.front()}.plan_length();
}
return complete_chain(Chain, Error);
}
bool editor_track::complete_chain(chain &Chain, std::string &Error)
{
auto *from{Chain.tracks.front()};
auto *to{Chain.tracks.back()};
auto const category{from->iCategoryFlag & 15};
for (auto const *track : Chain.tracks)
{
if ((track->iCategoryFlag & 15) != category)
{
Error = "The fragment mixes different kinds of paths";
Chain = {};
return false;
}
Chain.length += track->Length();
Chain.velocity = std::max(Chain.velocity, velocity(*track));
auto radius{path_radius(track->m_paths.front(), 0.5)};
if (radius == 0.0 && track->m_paths.front().radius != 0.f)
radius = std::abs(track->m_paths.front().radius);
if (radius > 0.0 && (Chain.radius == 0.0 || radius < Chain.radius))
Chain.radius = radius;
}
auto const &first{from->m_paths.front()};
auto const firstforward{Chain.forward.front()};
Chain.start = first.points[firstforward ? segment_data::point::start : segment_data::point::end];
Chain.start_direction = firstforward ? path_tangent(first, false) : -path_tangent(first, true);
adjoining_direction(firstforward ? from->trPrev : from->trNext, Chain.start, false, Chain.start_direction, Chain.start_radius);
Chain.start_direction = glm::normalize(Chain.start_direction);
auto const &last{to->m_paths.front()};
auto const lastforward{Chain.forward.back()};
Chain.end = last.points[lastforward ? segment_data::point::end : segment_data::point::start];
Chain.end_direction = lastforward ? path_tangent(last, true) : -path_tangent(last, false);
adjoining_direction(lastforward ? to->trNext : to->trPrev, Chain.end, true, Chain.end_direction, Chain.end_radius);
Chain.end_direction = glm::normalize(Chain.end_direction);
Error.clear();
return true;
}
std::vector<editor_track::chain> editor_track::chain_parts(chain const &Chain)
{
std::vector<chain> parts;
std::size_t first{0};
auto const add = [&](std::size_t const End) {
if (End <= first)
return;
chain part;
part.tracks.assign(Chain.tracks.begin() + first, Chain.tracks.begin() + End);
part.forward.assign(Chain.forward.begin() + first, Chain.forward.begin() + End);
std::string error;
if (complete_chain(part, error))
parts.push_back(std::move(part));
first = End;
};
for (auto const &passage : Chain.switches)
add(passage.before);
add(Chain.tracks.size());
return parts;
}
namespace
{
bool plan_straight(segment_data const &Path, editor_track::straight_tolerance const &Tolerance)
{
auto const chord{plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])};
auto const length{glm::length(chord)};
if (length < 1e-3)
return false;
auto const &control1{Path.points[segment_data::point::control1]};
auto const &control2{Path.points[segment_data::point::control2]};
if (Path.radius != 0.f && std::abs(Path.radius) < Tolerance.radius)
return false;
if (control1 == glm::dvec3{} && control2 == glm::dvec3{})
return true;
auto const direction{chord / length};
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
if (std::abs(cross(direction, plan_of(Path.points[index]))) > Tolerance.offset)
return false;
auto const tangentstart{plan_of(path_tangent(Path, false))};
auto const tangentend{plan_of(path_tangent(Path, true))};
if (glm::length(tangentstart) < 1e-9 || glm::length(tangentend) < 1e-9)
return false;
auto const a{glm::normalize(tangentstart)};
auto const b{glm::normalize(tangentend)};
auto const turn{std::abs(std::atan2(cross(a, b), glm::dot(a, b)))};
auto const deviation{std::max(std::abs(cross(direction, a)), std::abs(cross(direction, b)))};
return turn * Tolerance.radius <= length && deviation * Tolerance.radius <= length;
}
glm::dvec2 direction_at(TTrack const &Track, glm::dvec3 const &Joint, bool const Arriving, bool &Found)
{
Found = false;
for (auto const &path : Track.m_paths)
{
for (auto const atend : {false, true})
{
if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > kSamePoint)
continue;
auto const tangent{plan_of(path_tangent(path, atend))};
if (glm::length(tangent) < 1e-9)
continue;
Found = true;
auto const unit{glm::normalize(tangent)};
return (atend == Arriving) ? unit : -unit;
}
}
return {0.0, 1.0};
}
TTrack *neighbour_at(TTrack const &Track, int const Path, bool const Atend)
{
if (has_switch_paths(Track))
return Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]; // NOSONAR
return Atend ? Track.trNext : Track.trPrev;
}
void rigid_move(TTrack &Track, glm::dvec3 const &Pivot, glm::dvec3 const &Target, double const Angle)
{
for (auto &path : Track.m_paths)
{
for (auto const index : {segment_data::point::start, segment_data::point::end})
{
auto &point{path.points[index]};
auto const moved{plan_of(Target) + turned(plan_of(point - Pivot), Angle)};
point = {moved.x, point.y + Target.y - Pivot.y, moved.y};
}
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
{
auto &control{path.points[index]};
auto const moved{turned(plan_of(control), Angle)};
control = {moved.x, control.y, moved.y};
}
}
}
void follow_joint(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Angle)
{
for (auto &path : Track.m_paths)
{
for (auto const atend : {false, true})
{
auto &point{path.points[atend ? segment_data::point::end : segment_data::point::start]};
if (glm::distance(point, From) > kSamePoint)
continue;
point = To;
auto &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]};
auto const moved{turned(plan_of(control), Angle)};
control = {moved.x, control.y, moved.y};
}
}
}
} // namespace
void editor_track::place(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Angle)
{
rigid_move(Track, From, To, Angle);
}
bool editor_track::is_straight(TTrack const &Track, straight_tolerance const &Tolerance)
{
if (false == is_supported(Track) || Track.m_editorremoved)
return false;
if (false == std::any_of(Track.m_paths.begin(), Track.m_paths.end(), [&](segment_data const &Path) { return plan_straight(Path, Tolerance); }))
return false;
if (Track.eType != tt_Normal || Track.trPrev == nullptr || Track.trNext == nullptr)
return true;
auto const &path{Track.m_paths.front()};
auto const &start{path.points[segment_data::point::start]};
auto const &end{path.points[segment_data::point::end]};
auto const length{glm::length(plan_of(end - start))};
auto const direction{glm::normalize(plan_of(end - start))};
bool foundbefore;
bool foundafter;
auto const before{direction_at(*Track.trPrev, start, true, foundbefore)};
auto const after{direction_at(*Track.trNext, end, false, foundafter)};
if (false == foundbefore || false == foundafter)
return true;
auto const kinkbefore{signed_angle(before, direction)};
auto const kinkafter{signed_angle(direction, after)};
if (std::abs(kinkbefore) <= Tolerance.angle || std::abs(kinkafter) <= Tolerance.angle || (kinkbefore > 0.0) != (kinkafter > 0.0))
return true;
return length / ((std::abs(kinkbefore) + std::abs(kinkafter)) * 0.5) >= Tolerance.radius;
}
editor_track::straight editor_track::find_straight(TTrack &Track, straight_tolerance const &Tolerance)
{
straight result;
if (false == is_straight(Track, Tolerance))
return result;
int firstpath{0};
while (false == plan_straight(Track.m_paths[firstpath], Tolerance))
++firstpath;
auto const &first{Track.m_paths[firstpath]};
auto const origin{first.points[segment_data::point::start]};
auto const direction{glm::normalize(plan_of(first.points[segment_data::point::end] - origin))};
auto const lateral = [&](glm::dvec3 const &Point) { return std::abs(cross(direction, plan_of(Point - origin))); };
auto const accepts = [&](TTrack const *Other, glm::dvec3 const &Joint) {
if (Other == nullptr || false == is_supported(*Other) || Other->m_editorremoved)
return -1;
for (int i = 0; i < static_cast<int>(Other->m_paths.size()); ++i)
{
auto const &path{Other->m_paths[i]};
if (false == plan_straight(path, Tolerance))
continue;
auto const &start{path.points[segment_data::point::start]};
auto const &end{path.points[segment_data::point::end]};
if (glm::distance(start, Joint) > kSamePoint && glm::distance(end, Joint) > kSamePoint)
continue;
auto const otherdirection{glm::normalize(plan_of(end - start))};
if (std::abs(cross(direction, otherdirection)) > Tolerance.angle)
continue;
if (lateral(start) > Tolerance.offset || lateral(end) > Tolerance.offset)
continue;
return i;
}
return -1;
};
std::deque<std::pair<TTrack *, int>> run{{&Track, firstpath}};
for (auto const forward : {true, false})
{
TTrack *current{&Track};
int path{firstpath};
bool atend{forward};
while (true)
{
auto const &exit{current->m_paths[path].points[atend ? segment_data::point::end : segment_data::point::start]};
auto *next{neighbour_at(*current, path, atend)};
if (std::any_of(run.begin(), run.end(), [&](auto const &Member) { return Member.first == next; }))
break;
auto const nextpath{accepts(next, exit)};
if (nextpath < 0)
break;
if (forward)
run.emplace_back(next, nextpath);
else
run.emplace_front(next, nextpath);
atend = glm::distance(next->m_paths[nextpath].points[segment_data::point::start], exit) <= kSamePoint;
current = next;
path = nextpath;
}
}
double low{0.0};
double high{0.0};
glm::dvec3 lowpoint{origin};
glm::dvec3 highpoint{origin};
for (auto const &member : run)
{
result.tracks.push_back(member.first);
result.paths.push_back(member.second);
for (auto const index : {segment_data::point::start, segment_data::point::end})
{
auto const &point{member.first->m_paths[member.second].points[index]};
auto const along{glm::dot(plan_of(point - origin), direction)};
if (along < low)
{
low = along;
lowpoint = point;
}
if (along > high)
{
high = along;
highpoint = point;
}
}
}
result.start = lowpoint;
result.end = highpoint;
result.direction = direction;
result.length = high - low;
result.grade = result.length > 0.0 ? (highpoint.y - lowpoint.y) / result.length : 0.0;
auto azimuth{glm::degrees(std::atan2(direction.x, direction.y))};
if (azimuth < 0.0)
azimuth += 360.0;
result.azimuth = azimuth;
return result;
}
std::vector<editor_track::straight> editor_track::find_straights(double const Minimumlength, straight_tolerance const &Tolerance)
{
std::vector<straight> result;
std::unordered_set<TTrack const *> visited;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || visited.count(track) > 0 || false == is_straight(*track, Tolerance))
continue;
auto line{find_straight(*track, Tolerance)};
for (auto const *member : line.tracks)
visited.insert(member);
if (line.length >= Minimumlength)
result.push_back(std::move(line));
}
std::sort(result.begin(), result.end(), [](straight const &A, straight const &B) { return A.length > B.length; });
return result;
}
bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve)
{
Curve = {};
auto const curved = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && false == is_straight(*Other, Tolerance); };
if (false == curved(&Track))
return false;
std::deque<std::pair<TTrack *, bool>> run{{&Track, true}};
auto const known = [&](TTrack const *Other) { return std::any_of(run.begin(), run.end(), [&](auto const &Member) { return Member.first == Other; }); };
auto const own_turn = [](TTrack const &Other, bool const Forward) {
auto const &path{Other.m_paths.front()};
auto const begin{plan_of(path_tangent(path, false))};
auto const finish{plan_of(path_tangent(path, true))};
if (glm::length(begin) < 1e-9 || glm::length(finish) < 1e-9)
return 0.0;
auto const turn{signed_angle(glm::normalize(begin), glm::normalize(finish))};
return Forward ? turn : -turn;
};
auto const side{own_turn(Track, true)};
auto const reversing = [&](TTrack const &Other, bool const Forward) { return own_turn(Other, Forward) * side < 0.0 && std::abs(own_turn(Other, Forward)) > 1e-4; };
for (auto const throughend : {true, false})
{
TTrack *previous{&Track};
TTrack *current{throughend ? Track.trNext : Track.trPrev};
while (current != nullptr && false == known(current))
{
if (current->eType == tt_Switch && has_switch_paths(*current) && is_supported(*current) && false == current->m_editorremoved)
{
auto const &extension{*current->SwitchExtension};
bool const atstart{extension.pPrevs[0] == previous};
if (atstart == (extension.pNexts[0] == previous))
break;
auto *beyond{atstart ? extension.pNexts[0] : extension.pPrevs[0]};
if (false == curved(beyond) || known(beyond) || reversing(*current, throughend == atstart))
break;
if (throughend)
run.emplace_back(current, atstart);
else
run.emplace_front(current, false == atstart);
previous = current;
current = beyond;
continue;
}
if (false == curved(current))
break;
auto const entersatstart{current->trPrev == previous};
if (reversing(*current, throughend == entersatstart))
break;
if (throughend)
run.emplace_back(current, entersatstart);
else
run.emplace_front(current, false == entersatstart);
previous = current;
current = entersatstart ? current->trNext : current->trPrev;
}
}
return analyse_curve({run.begin(), run.end()}, Gauge, Curve);
}
bool editor_track::analyse_curve(std::vector<std::pair<TTrack *, bool>> const &Run, double const Gauge, curve &Curve)
{
Curve = {};
if (Run.empty())
return false;
auto const &run{Run};
std::vector<double> turns;
std::vector<double> lengths;
std::vector<bool> varying;
std::vector<double> curvatures;
auto const incoming = [&](std::size_t const Index) {
auto const &path{run[Index].first->m_paths.front()};
auto const tangent{glm::normalize(plan_of(run[Index].second ? path_tangent(path, false) : -path_tangent(path, true)))};
return tangent;
};
auto const outgoing = [&](std::size_t const Index) {
auto const &path{run[Index].first->m_paths.front()};
auto const tangent{glm::normalize(plan_of(run[Index].second ? path_tangent(path, true) : -path_tangent(path, false)))};
return tangent;
};
auto const joint = [&](std::size_t const Index, bool const Atend) {
auto const &path{run[Index].first->m_paths.front()};
return path.points[(run[Index].second == Atend) ? segment_data::point::end : segment_data::point::start];
};
std::vector<double> kinks(run.size() + 1, 0.0);
{
auto *before{run.front().second ? run.front().first->trPrev : run.front().first->trNext};
bool found{false};
if (before != nullptr)
{
auto const direction{direction_at(*before, joint(0, false), true, found)};
if (found)
kinks.front() = signed_angle(direction, incoming(0));
}
auto *after{run.back().second ? run.back().first->trNext : run.back().first->trPrev};
if (after != nullptr)
{
auto const direction{direction_at(*after, joint(run.size() - 1, true), false, found)};
if (found)
kinks.back() = signed_angle(outgoing(run.size() - 1), direction);
}
for (std::size_t i = 1; i < run.size(); ++i)
kinks[i] = signed_angle(outgoing(i - 1), incoming(i));
}
for (std::size_t i = 0; i < run.size(); ++i)
{
auto const &path{run[i].first->m_paths.front()};
auto const length{run[i].first->eType == tt_Switch ? bezier{path}.plan_length() : run[i].first->Length()};
auto const internal{signed_angle(incoming(i), outgoing(i))};
turns.push_back(internal + kinks[i] * 0.5 + kinks[i + 1] * 0.5);
lengths.push_back(length);
auto const radiusstart{path_radius(path, 0.0)};
auto const radiusend{path_radius(path, 1.0)};
auto const curvaturestart{radiusstart > 0.0 ? 1.0 / radiusstart : 0.0};
auto const curvatureend{radiusend > 0.0 ? 1.0 / radiusend : 0.0};
auto const middle{path_radius(path, 0.5)};
double curvature{middle > 0.0 ? 1.0 / middle : 0.0};
if (curvature == 0.0 && path.radius != 0.f)
curvature = 1.0 / std::abs(path.radius);
if (curvature == 0.0 && length > 1e-3)
curvature = std::abs(turns.back()) / length;
curvatures.push_back(curvature);
varying.push_back(std::abs(curvaturestart - curvatureend) > 0.1 * std::max(curvaturestart, curvatureend));
for (auto const roll : path.rolls)
Curve.cant = std::max(Curve.cant, Gauge * std::sin(std::abs(glm::radians(static_cast<double>(roll)))));
}
auto const peak{*std::max_element(curvatures.begin(), curvatures.end())};
// old sceneries make a transition of short arcs of growing radius, a longer arc of a larger radius is a part of a compound curve
constexpr double kShortestCompoundArc{30.0};
for (std::size_t first = 0; first < run.size();)
{
auto last{first + 1};
while (last < run.size() && false == varying[first] && false == varying[last] && std::abs(curvatures[last] - curvatures[first]) <= 0.05 * curvatures[first])
++last;
auto const length{std::accumulate(lengths.begin() + first, lengths.begin() + last, 0.0)};
if (curvatures[first] < 0.9 * peak && (varying[first] || length < kShortestCompoundArc || curvatures[first] < 0.1 * peak))
for (auto i = first; i < last; ++i)
varying[i] = true;
first = last;
}
double smallest{0.0};
for (std::size_t i = 0; i < run.size(); ++i)
if (false == varying[i] && curvatures[i] > 0.0 && (smallest == 0.0 || 1.0 / curvatures[i] < smallest))
smallest = 1.0 / curvatures[i];
if (smallest == 0.0 && peak > 0.0)
smallest = 1.0 / peak;
int sign{0};
for (auto const turn : turns)
{
Curve.turn += turn;
int const current{turn > 1e-9 ? 1 : turn < -1e-9 ? -1 : 0};
if (current != 0 && sign != 0 && current != sign)
++Curve.reversals;
if (current != 0)
sign = current;
}
for (std::size_t i = 0; i < run.size() && varying[i]; ++i)
Curve.transition_in += lengths[i];
for (std::size_t i = run.size(); i > 0 && varying[i - 1]; --i)
Curve.transition_out += lengths[i - 1];
if (Curve.transition_in + Curve.transition_out >= std::accumulate(lengths.begin(), lengths.end(), 0.0))
{
Curve.transition_in = 0.0;
Curve.transition_out = 0.0;
}
Curve.radius = smallest;
std::vector<curve::arc_part> arcs;
double between{0.0};
for (std::size_t i = 0; i < run.size(); ++i)
{
if (varying[i] || curvatures[i] <= 0.0)
{
if (false == arcs.empty())
between += lengths[i];
continue;
}
auto const radius{1.0 / curvatures[i]};
if (false == arcs.empty() && std::abs(arcs.back().radius - radius) < 0.05 * radius)
arcs.back().turn += (lengths[i] + between) / radius;
else
arcs.push_back({radius, lengths[i] / radius, arcs.empty() ? 0.0 : between});
between = 0.0;
}
if (arcs.size() >= 2 && Curve.reversals == 0)
{
auto const [low, high]{std::minmax_element(arcs.begin(), arcs.end(), [](curve::arc_part const &A, curve::arc_part const &B) { return A.radius < B.radius; })};
if (high->radius - low->radius > 0.1 * low->radius)
{
Curve.compound = true;
Curve.radius = arcs.front().radius;
Curve.arcs = arcs;
}
}
if (Curve.reversals == 0)
{
if (auto const designed{alignment_store::vertex_of(Run)})
{
auto const &shape{designed->shape};
Curve.designed = shape;
Curve.radius = shape.radius;
Curve.transition_in = shape.transition_in;
Curve.transition_out = shape.transition_out;
Curve.cant = shape.cant;
Curve.compound = shape.compound && false == shape.arcs.empty();
Curve.arcs.clear();
if (Curve.compound)
{
Curve.arcs.push_back({shape.radius, shape.share, 0.0});
for (auto const &arc : shape.arcs)
Curve.arcs.push_back({arc.radius, arc.share, arc.transition});
}
}
}
Curve.from = run.front().first;
Curve.to = run.back().first;
return true;
}
bool editor_track::find_run(TTrack &Track, double const Limit, chain &Chain, std::string &Error)
{
auto const category{Track.iCategoryFlag & 15};
auto const regular = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && (Other->iCategoryFlag & 15) == category; };
if (false == regular(&Track))
{
Error = "A line is made of regular paths, a switch ends it";
return false;
}
straight_tolerance const tolerance;
TTrack *ends[2]{&Track, &Track};
std::vector<TTrack const *> visited{&Track};
for (int side = 0; side < 2; ++side)
{
TTrack *previous{&Track};
TTrack *current{side == 0 ? Track.trNext : Track.trPrev};
double length{0.0};
// past the limit the line goes on to the next straight, so it doesn't end inside a curve
while (regular(current) && std::find(visited.begin(), visited.end(), current) == visited.end() && (length < Limit || (length < 3.0 * Limit && false == is_straight(*ends[side], tolerance))))
{
visited.push_back(current);
ends[side] = current;
length += current->Length();
auto const entersatstart{current->trPrev == previous};
previous = current;
current = entersatstart ? current->trNext : current->trPrev;
}
}
return find_chain(ends[1], ends[0], Chain, Error);
}
std::vector<editor_track::line_vertex> editor_track::recognize_line(chain const &Chain, straight_tolerance const &Tolerance, double const Gauge, alignment::transition_shape const Shape)
{
std::vector<line_vertex> result;
auto const count{Chain.tracks.size()};
auto const path = [&](std::size_t const Index) -> segment_data const & { return Chain.tracks[Index]->m_paths.front(); };
auto const tangent_in = [&](std::size_t const Index) { return glm::normalize(plan_of(Chain.forward[Index] ? path_tangent(path(Index), false) : -path_tangent(path(Index), true))); };
auto const tangent_out = [&](std::size_t const Index) { return glm::normalize(plan_of(Chain.forward[Index] ? path_tangent(path(Index), true) : -path_tangent(path(Index), false))); };
auto const point_in = [&](std::size_t const Index) { return plan_of(path(Index).points[Chain.forward[Index] ? segment_data::point::start : segment_data::point::end]); };
auto const point_out = [&](std::size_t const Index) { return plan_of(path(Index).points[Chain.forward[Index] ? segment_data::point::end : segment_data::point::start]); };
std::vector<bool> straight(count);
std::vector<double> turns(count);
for (std::size_t i = 0; i < count; ++i)
{
straight[i] = is_straight(*Chain.tracks[i], Tolerance);
turns[i] = signed_angle(tangent_in(i), tangent_out(i));
}
auto const kLongestTurn{0.85 * glm::pi<double>()};
auto const curvature = [&](std::size_t const Index, double const T) {
auto const radius{path_radius(path(Index), T)};
return radius > 0.0 ? 1.0 / radius : 0.0;
};
auto const eases = [&](std::size_t const Index, bool const Atend) {
double peak{0.0};
for (auto const t : {0.0, 0.25, 0.5, 0.75, 1.0})
peak = std::max(peak, curvature(Index, t));
return peak > 0.0 && curvature(Index, Chain.forward[Index] == Atend ? 1.0 : 0.0) < 0.05 * peak;
};
auto const add_curve = [&](std::size_t const First, std::size_t const Last) {
auto const a{point_in(First)};
auto const b{point_out(Last)};
auto const directionin{tangent_in(First)};
auto const directionout{tangent_out(Last)};
auto const determinant{cross(directionin, directionout)};
if (std::abs(determinant) < 1e-6)
return;
line_vertex vertex;
vertex.position = a + directionin * (cross(b - a, directionout) / determinant);
std::vector<std::pair<TTrack *, bool>> run;
for (auto i = First; i <= Last; ++i)
run.emplace_back(Chain.tracks[i], Chain.forward[i]);
analyse_curve(run, Gauge, vertex.shape);
auto &shape{vertex.shape};
if (false == shape.designed.has_value() && false == shape.compound && shape.reversals == 0)
{
auto const pieces = [&](double const Length, bool const Fromstart) {
std::size_t count{0};
double covered{0.0};
while (covered < Length - 1e-6 && count <= Last - First)
{
covered += Chain.tracks[Fromstart ? First + count : Last - count]->Length();
++count;
}
return count;
};
auto const in{shape.transition_in > 0.0 ? pieces(shape.transition_in, true) : 0};
auto const out{shape.transition_out > 0.0 ? pieces(shape.transition_out, false) : 0};
auto const turn_of = [&](std::size_t const From, std::size_t const To) { return std::abs(std::accumulate(turns.begin() + From, turns.begin() + To + 1, 0.0)); };
auto const length = [&](double const Turn) { return Shape == alignment::transition_shape::clothoid ? 2.0 * shape.radius * Turn : 2.0 * shape.radius * std::tan(Turn); };
if (Last > First && shape.transition_in <= 0.0 && shape.transition_out <= 0.0 && eases(First, false) && eases(Last, true))
{
auto joint{First};
for (auto k = First; k < Last; ++k)
if (curvature(k, Chain.forward[k] ? 1.0 : 0.0) > curvature(joint, Chain.forward[joint] ? 1.0 : 0.0))
joint = k;
double tangentin;
double tangentout;
if (alignment::spiral_tangents(Shape, turn_of(First, joint), turn_of(joint + 1, Last), tangentin, tangentout))
{
shape.radius = 0.5 * (glm::distance(vertex.position, a) / tangentin + glm::distance(b, vertex.position) / tangentout);
shape.transition_in = length(turn_of(First, joint));
shape.transition_out = length(turn_of(joint + 1, Last));
}
}
else if (in + out <= Last - First)
{
auto const arc{turn_of(First + in, Last - out)};
if (arc > 1e-6 && arc < 2.0 * glm::pi<double>() - 1e-3)
{
shape.radius = glm::distance(point_in(First + in), point_out(Last - out)) / (2.0 * std::sin(arc * 0.5));
if (in > 0)
shape.transition_in = length(turn_of(First, First + in - 1));
if (out > 0)
shape.transition_out = length(turn_of(Last - out + 1, Last));
}
}
}
result.push_back(vertex);
};
std::size_t i{0};
while (i < count)
{
if (straight[i])
{
if (i + 1 < count && straight[i + 1] && std::abs(signed_angle(tangent_out(i), tangent_in(i + 1))) > 1e-3)
{
line_vertex vertex;
vertex.position = point_out(i);
vertex.kink = true;
result.push_back(vertex);
}
++i;
continue;
}
auto const first{i};
int sign{0};
while (i < count && false == straight[i])
{
int const current{turns[i] > 1e-9 ? 1 : turns[i] < -1e-9 ? -1 : 0};
// a reverse curve: the next one starts here
if (current != 0 && sign != 0 && current != sign)
break;
if (i > first && (eases(i - 1, true) || eases(i, false)))
break;
if (current != 0)
sign = current;
++i;
}
auto const total{std::abs(std::accumulate(turns.begin() + first, turns.begin() + i, 0.0))};
auto const parts{std::max(1.0, std::ceil(total / kLongestTurn))};
auto start{first};
double turned{0.0};
double part{1.0};
for (auto k = first; k + 1 < i && part < parts; ++k)
{
turned += std::abs(turns[k]);
if (turned >= total * part / parts)
{
add_curve(start, k);
start = k + 1;
part += 1.0;
}
}
add_curve(start, i - 1);
}
return result;
}
namespace
{
// shorter remainders of a straight path are absorbed by moving the joint instead of splitting the path
double split_margin(double const Length)
{
return std::max(0.5, 0.002 * Length);
}
} // namespace
editor_track::straight_run editor_track::straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance)
{
straight_run run;
if (Chain.tracks.empty())
return run;
auto *edge{Atend ? Chain.tracks.back() : Chain.tracks.front()};
bool const forward{Atend ? Chain.forward.back() : Chain.forward.front()};
TTrack *current{forward == Atend ? edge->trNext : edge->trPrev};
auto joint{Atend ? Chain.end : Chain.start};
auto const origin{plan_of(joint)};
auto direction{glm::normalize(plan_of(Atend ? Chain.end_direction : -Chain.start_direction))};
while (current != nullptr && run.tracks.size() < 1000)
{
std::string reason;
if (current->eType != tt_Normal || current->m_editorremoved || false == can_edit_geometry(*current, reason))
break;
if (std::find(Chain.tracks.begin(), Chain.tracks.end(), current) != Chain.tracks.end() || std::find(run.tracks.begin(), run.tracks.end(), current) != run.tracks.end())
break;
auto const &path{current->m_paths.front()};
if (false == plan_straight(path, Tolerance))
break;
bool outward;
if (glm::distance(path.points[segment_data::point::start], joint) <= kSamePoint)
outward = true;
else if (glm::distance(path.points[segment_data::point::end], joint) <= kSamePoint)
outward = false;
else
break;
auto const &opposite{path.points[outward ? segment_data::point::end : segment_data::point::start]};
if (run.tracks.empty())
{
auto const own{glm::normalize(plan_of(opposite - joint))};
if (std::abs(cross(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0)
break;
direction = own;
}
auto const offset{plan_of(opposite) - origin};
auto const along{glm::dot(offset, direction)};
if (along <= run.length + 1e-3 || std::abs(cross(direction, offset)) > Tolerance.offset)
break;
run.tracks.push_back(current);
run.outward.push_back(outward);
run.length = along;
joint = opposite;
current = outward ? current->trNext : current->trPrev;
}
return run;
}
double editor_track::run_reserve(straight_run const &Run)
{
if (Run.tracks.empty())
return 0.0;
return std::max(0.0, Run.length - split_margin(chord_length(Run.tracks.back()->m_paths.front())) - 0.01);
}
TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector<std::pair<TTrack *, state>> &States, std::vector<TTrack *> &Created)
{
auto const length{glm::length(plan_of(Cut - Joint))};
auto *edge{Edge};
double covered{0.0};
for (std::size_t i = 0; i < Run.tracks.size(); ++i)
{
auto *track{Run.tracks[i]};
auto const outward{Run.outward[i]};
auto const &path{track->m_paths.front()};
auto const pathlength{chord_length(path)};
auto const margin{split_margin(pathlength)};
auto const remaining{length - covered};
if (remaining <= margin)
{
if (std::abs(remaining) > 1e-4)
{
States.emplace_back(track, capture(*track));
move_point(*track, {0, outward ? point_kind::start : point_kind::end}, Cut);
commit({track});
}
break;
}
if (remaining < pathlength - margin)
{
auto const &nearpoint{path.points[outward ? segment_data::point::start : segment_data::point::end]};
auto const &farpoint{path.points[outward ? segment_data::point::end : segment_data::point::start]};
auto const t{nearest_parameter(*track, nearpoint + (farpoint - nearpoint) * (remaining / pathlength))};
auto const before{capture(*track)};
auto *created{split_path(*track, t)};
if (created == nullptr)
break;
States.emplace_back(track, before);
Created.push_back(created);
edge = outward ? track : created;
break;
}
edge = track;
covered += pathlength;
}
return edge;
}
namespace
{
using route_node = std::tuple<TTrack *, int, bool>;
std::vector<route_node> route_next(route_node const &Node)
{
std::vector<route_node> result;
auto *track{std::get<0>(Node)};
auto const path{std::get<1>(Node)};
auto const forward{std::get<2>(Node)};
auto const &exit{track->m_paths[path].points[forward ? segment_data::point::end : segment_data::point::start]};
auto *next{neighbour_at(*track, path, forward)};
if (next == nullptr || next == track || next->m_editorremoved || false == editor_track::is_supported(*next) || (next->iCategoryFlag & 15) != (track->iCategoryFlag & 15))
return result;
for (int i = 0; i < static_cast<int>(next->m_paths.size()); ++i)
{
auto const &candidate{next->m_paths[i]};
if (geometry::plan_distance(candidate.points[segment_data::point::start], exit) <= kSamePoint)
result.emplace_back(next, i, true);
else if (geometry::plan_distance(candidate.points[segment_data::point::end], exit) <= kSamePoint)
result.emplace_back(next, i, false);
}
return result;
}
} // namespace
bool editor_track::find_route(TTrack *From, TTrack *To, route &Route, std::string &Error)
{
Route = {};
if (From == nullptr || To == nullptr || false == is_supported(*From) || false == is_supported(*To))
{
Error = "Select the first and the last path of the route";
return false;
}
if (From == To)
{
Route.spans.push_back({From, 0, true});
Error.clear();
return true;
}
std::vector<route_node> nodes;
std::vector<int> parents;
std::map<route_node, int> visited;
for (int i = 0; i < static_cast<int>(From->m_paths.size()); ++i)
for (auto const forward : {true, false})
{
route_node const node{From, i, forward};
visited.emplace(node, static_cast<int>(nodes.size()));
nodes.push_back(node);
parents.push_back(-1);
}
int goal{-1};
for (std::size_t index = 0; index < nodes.size() && goal < 0 && nodes.size() < 200000; ++index)
{
for (auto const &next : route_next(nodes[index]))
{
if (std::get<0>(next) == From || visited.count(next) > 0)
continue;
visited.emplace(next, static_cast<int>(nodes.size()));
nodes.push_back(next);
parents.push_back(static_cast<int>(index));
if (std::get<0>(next) == To)
{
goal = static_cast<int>(nodes.size()) - 1;
break;
}
}
}
if (goal < 0)
{
Error = "No route connects the selected paths";
return false;
}
for (int index = goal; index >= 0; index = parents[index])
Route.spans.push_back({std::get<0>(nodes[index]), std::get<1>(nodes[index]), std::get<2>(nodes[index])});
std::reverse(Route.spans.begin(), Route.spans.end());
Error.clear();
return true;
}
editor_track::route editor_track::run_route(TTrack &Track, double const Maximum)
{
route result;
if (false == is_supported(Track))
return result;
auto const walk = [&](route_node Node) {
std::vector<route_node> nodes;
double length{0.0};
while (nodes.size() < 5000 && length < Maximum)
{
auto candidates{route_next(Node)};
if (candidates.empty())
break;
auto chosen{candidates.front()};
for (auto const &candidate : candidates)
if (std::get<1>(candidate) == 0)
chosen = candidate;
auto *track{std::get<0>(chosen)};
if (track == &Track || std::any_of(nodes.begin(), nodes.end(), [&](route_node const &Other) { return std::get<0>(Other) == track; }))
break;
nodes.push_back(chosen);
length += bezier(track->m_paths[std::get<1>(chosen)]).plan_length(8);
Node = chosen;
}
return nodes;
};
auto const ahead{walk({&Track, 0, true})};
auto const behind{walk({&Track, 0, false})};
for (auto node = behind.rbegin(); node != behind.rend(); ++node)
if (std::none_of(ahead.begin(), ahead.end(), [&](route_node const &Other) { return std::get<0>(Other) == std::get<0>(*node); }))
result.spans.push_back({std::get<0>(*node), std::get<1>(*node), false == std::get<2>(*node)});
result.spans.push_back({&Track, 0, true});
for (auto const &node : ahead)
result.spans.push_back({std::get<0>(node), std::get<1>(node), std::get<2>(node)});
return result;
}
editor_track::route editor_track::route_of(chain const &Chain)
{
route result;
for (std::size_t i = 0; i < Chain.tracks.size(); ++i)
result.spans.push_back({Chain.tracks[i], 0, Chain.forward[i]});
return result;
}
std::vector<editor_track::route_sample> editor_track::sample_route(route &Route, double const Step)
{
std::vector<route_sample> samples;
double chainage{0.0};
for (std::size_t i = 0; i < Route.spans.size(); ++i)
{
auto &span{Route.spans[i]};
auto const &path{span.track->m_paths[span.path]};
// a path made of several curves is sampled along them, its own curve only stands in for them in older versions
std::vector<segment_data const *> parts;
if (path.pieces.empty())
parts.push_back(&path);
else
for (auto const &piece : path.pieces)
parts.push_back(&piece);
if (false == span.forward)
std::reverse(parts.begin(), parts.end());
span.from = chainage;
for (auto const *part : parts)
{
bezier const curve{*part};
auto const count{std::max(2, static_cast<int>(std::ceil(curve.plan_length(16) / std::max(0.1, Step))))};
auto previous{plan_of(curve.point(span.forward ? 0.0 : 1.0))};
for (int j = (samples.empty() ? 0 : 1); j <= count; ++j)
{
auto const t{span.forward ? static_cast<double>(j) / count : 1.0 - static_cast<double>(j) / count};
auto const position{curve.point(t)};
chainage += glm::distance(previous, plan_of(position));
previous = plan_of(position);
auto derivative{curve.first(t)};
if (false == span.forward)
derivative = -derivative;
auto const second{curve.second(t)};
auto const planar{std::hypot(derivative.x, derivative.z)};
route_sample sample;
sample.chainage = chainage;
sample.position = position;
sample.span = i;
sample.cant = 1.5 * std::sin(std::abs(glm::radians(part->rolls[0] + (part->rolls[1] - part->rolls[0]) * t)));
if (planar > 1e-9)
{
sample.direction = plan_of(derivative) / planar;
sample.grade = derivative.y / planar;
sample.curvature = (derivative.x * second.z - derivative.z * second.x) / (planar * planar * planar);
}
samples.push_back(sample);
}
}
span.to = chainage;
}
Route.length = chainage;
return samples;
}
namespace
{
template <typename Getter>
auto interpolate(std::vector<editor_track::route_sample> const &Samples, double const Chainage, Getter const &Get) -> decltype(Get(Samples.front()))
{
if (Samples.empty())
return {};
auto const next{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })};
if (next == Samples.begin())
return Get(Samples.front());
if (next == Samples.end())
return Get(Samples.back());
auto const &before{*std::prev(next)};
auto const run{next->chainage - before.chainage};
return run > 1e-9 ? Get(before) + (Get(*next) - Get(before)) * (Chainage - before.chainage) / run : Get(before);
}
void set_heights(segment_data &Path, double const Startheight, double const Endheight, double const Startgrade, double const Endgrade)
{
auto &start{Path.points[segment_data::point::start]};
auto &end{Path.points[segment_data::point::end]};
auto &control1{Path.points[segment_data::point::control1]};
auto &control2{Path.points[segment_data::point::control2]};
if (control1 == glm::dvec3{} && control2 == glm::dvec3{})
{
control1 = (end - start) / 3.0;
control2 = (start - end) / 3.0;
}
start.y = Startheight;
end.y = Endheight;
control1.y = Startgrade * std::hypot(control1.x, control1.z);
control2.y = -Endgrade * std::hypot(control2.x, control2.z);
}
// the path lies in the plane through Origin which rises with Grade along Direction (unit, in plan) and is level across it
void plane_heights(segment_data &Path, glm::dvec3 const &Origin, glm::dvec2 const &Direction, double const Grade)
{
auto &start{Path.points[segment_data::point::start]};
auto &end{Path.points[segment_data::point::end]};
auto &control1{Path.points[segment_data::point::control1]};
auto &control2{Path.points[segment_data::point::control2]};
if (control1 == glm::dvec3{} && control2 == glm::dvec3{})
{
control1 = (end - start) / 3.0;
control2 = (start - end) / 3.0;
}
auto const height = [&](glm::dvec3 const &Point) { return Origin.y + Grade * glm::dot(plan_of(Point - Origin), Direction); };
auto const first{start + control1};
auto const second{end + control2};
start.y = height(start);
end.y = height(end);
control1.y = height(first) - start.y;
control2.y = height(second) - end.y;
}
// a path of a switch on the plane of the cant of the path the route runs along: across from each of its points the height of
// that path less its cant times the distance to the left of it. The plane follows the curve of a switch bent into a curve
void cone_heights(segment_data &Path, segment_data const &Along)
{
bezier const along{Along};
auto const nearest = [&](glm::dvec3 const &Point) {
auto const distance = [&](double const T) { return geometry::plan_distance(along.point(T), Point); };
double best{0.0};
for (int i = 0; i <= 64; ++i)
if (distance(i / 64.0) < distance(best))
best = i / 64.0;
double step{1.0 / 64.0};
for (int i = 0; i < 30; ++i)
{
step *= 0.5;
if (best - step >= 0.0 && distance(best - step) < distance(best))
best -= step;
else if (best + step <= 1.0 && distance(best + step) < distance(best))
best += step;
}
return best;
};
auto const height = [&](glm::dvec3 const &Point) {
auto const t{nearest(Point)};
auto const on{along.point(t)};
auto direction{plan_of(along.first(t))};
direction = glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{1.0, 0.0};
glm::dvec2 const left{-direction.y, direction.x};
auto const lateral{glm::dot(plan_of(Point) - plan_of(on), left)};
auto const roll{glm::radians(static_cast<double>(Along.rolls[0] + (Along.rolls[1] - Along.rolls[0]) * t))};
return on.y - lateral * std::tan(roll);
};
auto &start{Path.points[segment_data::point::start]};
auto &end{Path.points[segment_data::point::end]};
auto &control1{Path.points[segment_data::point::control1]};
auto &control2{Path.points[segment_data::point::control2]};
if (control1 == glm::dvec3{} && control2 == glm::dvec3{})
{
control1 = (end - start) / 3.0;
control2 = (start - end) / 3.0;
}
start.y = height(start);
end.y = height(end);
control1.y = 0.0;
control2.y = 0.0;
// the inner points of the curve on the plane at a third and two thirds of it
auto const h1{height(bezier{Path}.point(1.0 / 3.0))};
auto const h2{height(bezier{Path}.point(2.0 / 3.0))};
auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * start.y + end.y / 3.0};
auto const y2{3.0 * h2 - 1.5 * h1 + start.y / 3.0 - 5.0 / 6.0 * end.y};
control1.y = y1 - start.y;
control2.y = y2 - end.y;
}
glm::dvec2 start_direction(segment_data const &Path)
{
auto const &start{Path.points[segment_data::point::start]};
auto const &control{Path.points[segment_data::point::control1]};
glm::dvec2 direction{control.x, control.z};
if (glm::length(direction) < 1e-6)
direction = {Path.points[segment_data::point::end].x - start.x, Path.points[segment_data::point::end].z - start.z};
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{1.0, 0.0};
}
} // namespace
double editor_track::sampled_elevation(std::vector<route_sample> const &Samples, double const Chainage)
{
return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position.y; });
}
double editor_track::sampled_grade(std::vector<route_sample> const &Samples, double const Chainage)
{
return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.grade; });
}
glm::dvec3 editor_track::sampled_position(std::vector<route_sample> const &Samples, double const Chainage)
{
return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position; });
}
bool editor_track::adjoining_grade(route const &Route, bool const Atend, double &Grade, TTrack **Adjoining, int *Path, bool *Entered)
{
if (Route.spans.empty())
return false;
auto const &span{Atend ? Route.spans.back() : Route.spans.front()};
auto const outward{Atend ? span.forward : false == span.forward};
auto const next{route_next({span.track, span.path, outward})};
if (next.empty())
return false;
auto const &[track, path, forward]{next.front()};
if (Adjoining != nullptr)
*Adjoining = track;
if (Path != nullptr)
*Path = path;
if (Entered != nullptr)
*Entered = forward;
auto const tangent{path_tangent(track->m_paths[path], false == forward)};
auto const plan{std::hypot(tangent.x, tangent.z)};
if (plan < 1e-9)
return false;
auto const along{(forward ? 1.0 : -1.0) * tangent.y / plan};
Grade = Atend ? along : -along;
return true;
}
std::vector<editor_track::height_gap> editor_track::apply_profile(route &Route, std::function<double(double)> const &Elevation, std::function<double(double)> const &Grade, std::vector<double> const &Breaks, std::vector<std::pair<TTrack *, state>> &States, std::vector<TTrack *> &Created)
{
double constexpr margin{0.25};
auto const remember = [&](TTrack *Track) {
if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; }) && std::find(Created.begin(), Created.end(), Track) == Created.end())
States.emplace_back(Track, capture(*Track));
};
// the parameter of the split at a distance along the path in the plan, as split_path takes it: a straight is the cubic with
// the controls at its ends there, not with the parameter spread evenly
auto const parameter = [](TTrack const &Track, double const Distance) {
bezier const curve{Track.m_paths.front()};
return nearest_parameter(Track, curve.point(curve.parameter(Distance)));
};
sample_route(Route, 1.0);
std::vector<route_span> spans;
for (auto const &span : Route.spans)
{
if (span.track->eType != tt_Normal)
{
spans.push_back(span);
continue;
}
// a piece shorter than the margin, between two breaks, goes with the one before it
std::vector<double> cuts;
for (auto const chainage : Breaks)
if (chainage > span.from + margin && chainage < span.to - margin && (cuts.empty() || chainage > cuts.back() + margin))
cuts.push_back(chainage);
// and no piece longer than a track may be
{
auto bounds{cuts};
bounds.insert(bounds.begin(), span.from);
bounds.push_back(span.to);
for (std::size_t i = 0; i + 1 < bounds.size(); ++i)
{
auto const length{bounds[i + 1] - bounds[i]};
auto const count{static_cast<int>(std::ceil(length / geometry::longest_path))};
for (int k = 1; k < count; ++k)
cuts.push_back(bounds[i] + length * k / count);
}
std::sort(cuts.begin(), cuts.end());
}
if (cuts.empty())
{
spans.push_back(span);
continue;
}
remember(span.track);
// the pieces named after the path: name_1, name_2...
auto const original{span.track->name()};
int number{0};
auto const next_name = [&]() { return original.empty() || original == "none" ? std::string{} : original + "_" + std::to_string(++number); };
if (span.forward)
{
auto *piece{span.track};
auto origin{span.from};
spans.push_back({piece, 0, true});
for (auto const cut : cuts)
{
auto const t{parameter(*piece, cut - origin)};
auto *created{split_path(*piece, t, next_name())};
if (created == nullptr)
continue;
Created.push_back(created);
spans.push_back({created, 0, true});
piece = created;
origin = cut;
}
}
else
{
std::vector<TTrack *> order;
for (auto cut = cuts.begin(); cut != cuts.end(); ++cut)
{
auto const t{parameter(*span.track, span.to - *cut)};
auto *created{split_path(*span.track, t, next_name())};
if (created == nullptr)
continue;
Created.push_back(created);
order.push_back(created);
}
for (auto *piece : order)
spans.push_back({piece, 0, false});
spans.push_back({span.track, 0, false});
}
}
Route.spans = spans;
sample_route(Route, 1.0);
auto const on_route = [&](TTrack const *Track) {
return std::any_of(Route.spans.begin(), Route.spans.end(), [&](route_span const &Span) { return Span.track == Track; });
};
struct joint
{
TTrack *track;
int path;
bool atend;
TTrack *neighbour;
};
std::vector<joint> joints;
if (false == Route.spans.empty())
for (auto const &[span, atend] : {std::pair{Route.spans.front(), false == Route.spans.front().forward}, std::pair{Route.spans.back(), Route.spans.back().forward}})
if (auto *neighbour{neighbour_at(*span.track, span.path, atend)}; neighbour != nullptr && false == on_route(neighbour))
joints.push_back({span.track, span.path, atend, neighbour});
std::vector<TTrack *> changed;
for (auto const &span : Route.spans)
{
remember(span.track);
auto const startchainage{span.forward ? span.from : span.to};
auto const endchainage{span.forward ? span.to : span.from};
auto const sign{span.forward ? 1.0 : -1.0};
auto &path{span.track->m_paths[span.path]};
set_heights(path, Elevation(startchainage), Elevation(endchainage), Grade(startchainage) * sign, Grade(endchainage) * sign);
// the pieces of a path made of several curves follow the grade line along them, else the path would part from them and lose them
std::vector<double> lengths;
double total{0.0};
for (auto const &piece : path.pieces)
{
lengths.push_back(bezier(piece).plan_length());
total += lengths.back();
}
if (total > 1e-6)
{
auto const at = [&](double const Distance) { return span.forward ? span.from + Distance * (span.to - span.from) / total : span.to - Distance * (span.to - span.from) / total; };
double along{0.0};
for (std::size_t k = 0; k < path.pieces.size(); ++k)
{
auto const first{at(along)};
auto const last{at(along + lengths[k])};
set_heights(path.pieces[k], Elevation(first), Elevation(last), Grade(first) * sign, Grade(last) * sign);
along += lengths[k];
}
}
add_unique(changed, span.track);
if (span.track->eType == tt_Normal)
continue;
auto const grade{Grade((span.from + span.to) * 0.5) * sign};
for (int i = 0; i < static_cast<int>(span.track->m_paths.size()); ++i)
{
for (auto const atend : {false, true})
{
auto *neighbour{neighbour_at(*span.track, i, atend)};
if (neighbour != nullptr && false == on_route(neighbour) && std::none_of(joints.begin(), joints.end(), [&](joint const &Joint) { return Joint.track == span.track && Joint.neighbour == neighbour; }))
joints.push_back({span.track, i, atend, neighbour});
}
if (i == span.path)
continue;
// in a cant the other path lies on its plane, else on the plane of the grade
if (std::abs(path.rolls[0]) > 1e-3f || std::abs(path.rolls[1]) > 1e-3f)
{
cone_heights(span.track->m_paths[i], path);
for (auto &piece : span.track->m_paths[i].pieces)
cone_heights(piece, path);
continue;
}
plane_heights(span.track->m_paths[i], path.points[segment_data::point::start], start_direction(path), grade);
for (auto &piece : span.track->m_paths[i].pieces)
plane_heights(piece, path.points[segment_data::point::start], start_direction(path), grade);
}
}
commit(changed);
sample_route(Route, 1.0);
std::vector<height_gap> gaps;
std::vector<TTrack *> raised;
for (auto const &joint : joints)
{
auto const &end{joint.track->m_paths[joint.path].points[joint.atend ? segment_data::point::end : segment_data::point::start]};
double nearest{std::numeric_limits<double>::max()};
double gap{0.0};
glm::dvec3 *closest{nullptr};
for (auto &path : joint.neighbour->m_paths)
for (auto const index : {segment_data::point::start, segment_data::point::end})
{
auto &point{path.points[index]};
auto const distance{std::hypot(point.x - end.x, point.z - end.z)};
if (distance < nearest)
{
nearest = distance;
gap = point.y - end.y;
closest = &point;
}
}
if (nearest > kSamePoint || std::abs(gap) <= 0.001)
continue;
// a regular path off the route takes the height of the route at the joint, keeping its other end
std::string reason;
bool const closed{joint.neighbour->eType == tt_Normal && can_edit_geometry(*joint.neighbour, reason)};
if (closed)
{
remember(joint.neighbour);
closest->y = end.y;
add_unique(raised, joint.neighbour);
}
gaps.push_back({joint.track, joint.neighbour, gap, closed});
}
commit(raised);
return gaps;
}
void editor_track::keep_heights(chain const &Chain, std::vector<segment_data> &Pieces)
{
auto existing{route_of(Chain)};
auto const samples{sample_route(existing, 1.0)};
if (samples.empty() || existing.length <= 1e-6)
return;
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)
return;
auto const scale{existing.length / total};
double chainage{0.0};
for (std::size_t i = 0; i < Pieces.size(); ++i)
{
auto const from{chainage * scale};
auto const to{(chainage + lengths[i]) * scale};
set_heights(Pieces[i], sampled_elevation(samples, from), sampled_elevation(samples, to), sampled_grade(samples, from) * scale, sampled_grade(samples, to) * scale);
chainage += lengths[i];
}
}
std::optional<editor_track::switch_template> editor_track::template_of_switch(TTrack *Track)
{
if (Track == nullptr || Track->m_editorremoved || Track->eType != tt_Switch || Track->m_paths.size() < 2 || (Track->iCategoryFlag & 0x80) != 0)
return std::nullopt;
auto const &main{Track->m_paths[0]};
auto const origin{main.points[segment_data::point::start]};
auto const chord{plan_of(main.points[segment_data::point::end] - origin)};
if (glm::length(chord) < 1.0)
return std::nullopt;
auto const axis{glm::normalize(chord)};
glm::dvec2 const normal{-axis.y, axis.x};
auto const tolocal = [&](glm::dvec3 const &Vector) {
auto const planar{plan_of(Vector)};
return glm::dvec3{glm::dot(planar, normal), Vector.y, glm::dot(planar, axis)};
};
switch_template entry;
entry.source = Track;
for (int i = 0; i < 2; ++i)
{
auto const &path{Track->m_paths[i]};
auto &local{entry.local[i]};
local = path;
local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal);
}
if (glm::length(plan_of(entry.local[1].points[segment_data::point::start])) > kSamePoint)
return std::nullopt;
if (entry.local[1].points[segment_data::point::end].x < 0.0)
{
for (auto &path : entry.local)
{
auto const mirror = [](glm::dvec3 const &Point) { return glm::dvec3{-Point.x, Point.y, Point.z}; };
path.transform(mirror, mirror);
path.rolls = {-path.rolls[0], -path.rolls[1]};
for (auto &piece : path.pieces)
piece.rolls = {-piece.rolls[0], -piece.rolls[1]};
}
}
entry.length = entry.local[0].points[segment_data::point::end].z;
auto const tangent{path_tangent(entry.local[1], true)};
auto const angle{std::atan2(std::abs(tangent.x), tangent.z)};
entry.ratio = angle > 1e-6 ? 1.0 / std::tan(angle) : 0.0;
entry.radius = path_radius(entry.local[1], 0.25);
entry.count = 1;
entry.label = format("L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(Track->m_material1).c_str());
return entry;
}
std::vector<editor_track::switch_template> editor_track::find_switch_templates()
{
std::vector<switch_template> result;
std::map<std::string, std::size_t> lookup;
for (auto *track : simulation::Paths.sequence())
{
auto entry{template_of_switch(track)};
if (false == entry.has_value())
continue;
auto const key{format("%.2f|%.2f|%.2f|%s|%s", entry->length, entry->local[1].points[segment_data::point::end].x, entry->local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str())};
auto const found{lookup.find(key)};
if (found != lookup.end())
{
++result[found->second].count;
continue;
}
lookup.emplace(key, result.size());
result.push_back(std::move(*entry));
}
std::sort(result.begin(), result.end(), [](switch_template const &A, switch_template const &B) { return A.count > B.count; });
return result;
}
std::vector<segment_data> editor_track::place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade)
{
std::vector<segment_data> result;
glm::dvec2 const normal{-Direction.y, Direction.x};
auto const side{static_cast<double>(Side)};
auto const vector = [&](glm::dvec3 const &Local) {
auto const planar{Direction * Local.z + normal * (Local.x * side)};
return glm::dvec3{planar.x, Local.y + Grade * Local.z, planar.y};
};
auto const put = [&](segment_data const &Local) {
segment_data path{Local};
path.transform([&](glm::dvec3 const &Point) { return Origin + vector(Point); }, vector);
if (Side < 0)
{
path.rolls = {-Local.rolls[0], -Local.rolls[1]};
for (auto &piece : path.pieces)
piece.rolls = {-piece.rolls[0], -piece.rolls[1]};
}
result.push_back(path);
};
for (auto const &local : Template.local)
put(local);
if (Template.twofold())
{
for (auto const &local : Template.second)
put(local);
put(Template.link);
}
return result;
}
std::optional<editor_track::switch_template> editor_track::template_of(switch_design::design const &Design, std::string &Error)
{
auto const shape{switch_design::build(Design, Error)};
if (shape.paths.size() < 2)
return std::nullopt;
switch_template entry;
entry.type = Design.type;
entry.rail = Design.rail.empty() ? std::string{"-"} : Design.rail;
entry.length = shape.length;
entry.ratio = shape.ratio;
entry.radius = shape.radius;
entry.a = shape.a;
entry.b = shape.b;
entry.own = Design.own;
entry.movable = Design.movable;
entry.heel = Design.heel;
entry.bladethrow = Design.bladethrow;
entry.local = {shape.paths[0], shape.paths[1]};
entry.local[1].radius = static_cast<float>(shape.radius);
if (shape.paths.size() >= 5)
{
entry.second = {shape.paths[2], shape.paths[3]};
entry.link = shape.paths[4];
entry.stagger = shape.stagger;
}
auto const decimals = [](double const Value) { return format("%.3f", Value); };
entry.label = Design.name + " a " + decimals(entry.a) + " b " + decimals(entry.b) + (Design.note.empty() ? std::string{} : " " + Design.note);
return entry;
}
glm::dvec2 editor_track::slip_curve::point(double const Along) const
{
auto const arc = [](glm::dvec2 const &From, double const Heading, double const Radius, double const Length) {
if (Radius <= 0.0)
return From + glm::dvec2{std::cos(Heading), std::sin(Heading)} * Length;
auto const to{Heading + Length / Radius};
return From + Radius * glm::dvec2{std::sin(to) - std::sin(Heading), std::cos(Heading) - std::cos(to)};
};
auto const along{Along - lead};
auto result{arc({0.0, 0.0}, 0.0, 0.0, std::min(Along, lead))};
if (along > 0.0)
result = arc(result, entry, radius, std::min(along, ends));
if (along > ends)
result = arc(result, heading(lead + ends), middle, std::min(along - ends, between));
if (along > ends + between)
result = arc(result, heading(lead + ends + between), radius, std::min(along - ends - between, ends));
if (along > 2.0 * ends + between)
result = arc(result, angle, 0.0, along - 2.0 * ends - between);
return result;
}
double editor_track::slip_curve::heading(double const Along) const
{
if (Along < lead)
return 0.0;
if (Along > length() - lead)
return angle;
auto const along{Along - lead};
auto const inside{std::clamp(along - ends, 0.0, between)};
return entry + (std::min(along, ends) + std::max(0.0, along - ends - between)) / radius + (middle > 0.0 ? inside / middle : 0.0);
}
double editor_track::slip_curve::tangent() const
{
auto const end{point(length())};
return end.x - end.y / std::tan(angle);
}
double editor_track::slip_curve::offset() const
{
return glm::distance(point(length() * 0.5), glm::dvec2{tangent(), 0.0});
}
editor_track::slip_curve editor_track::slip_of(double const Radius, double const Middle, double const Angle, double const Pass, double const Lead, double const Entry)
{
if (Entry > 0.0 && Angle * 0.5 <= Entry + 1e-6)
return slip_of(Radius, Middle, Angle, Pass);
auto const make = [&](double const Turn) {
return slip_curve{Radius, Radius * Turn, Middle, Middle * (Angle - 2.0 * (Turn + Entry)), Angle, 0.0, Lead, Entry};
};
if (Middle < 0.0 && Angle >= 1e-6)
{
// a straight in the middle, as long as the arcs of Radius turning by a half of Angle pass the crossing as near
auto low{0.0};
auto high{4.0 * Pass / std::tan(Angle * 0.5)};
for (int i = 0; i < 60; ++i)
{
auto const between{(low + high) * 0.5};
(slip_curve{Radius, Radius * (Angle * 0.5 - Entry), 0.0, between, Angle, 0.0, Lead, Entry}.offset() > Pass ? high : low) = between;
}
return {Radius, Radius * (Angle * 0.5 - Entry), 0.0, (low + high) * 0.5, Angle, 0.0, Lead, Entry};
}
if (Middle <= 0.0 || Angle < 1e-6)
return make(Angle * 0.5 - Entry);
// the curve passes the crossing nearer as its end arcs turn further, the middle one being flatter
auto low{0.0};
auto high{Angle * 0.5 - Entry};
for (int i = 0; i < 60; ++i)
{
auto const turn{(low + high) * 0.5};
((make(turn).offset() > Pass) == (Middle > Radius) ? low : high) = turn;
}
return make((low + high) * 0.5);
}
std::string editor_track::rail_of(TTrack const *Track)
{
if (Track == nullptr || (Track->iCategoryFlag & 15) != 1)
return {};
auto const &profile{Track->m_profile1.first};
if (profile == "s60" || profile == "uic60")
return "60E1";
if (profile == "s49")
return "49E1";
if (profile == "s42")
return "S42";
return {};
}
std::vector<editor_track::switch_template> editor_track::standard_switch_templates()
{
std::vector<switch_template> result;
auto designs{switch_design::catalogue()};
auto const own{switch_design::own()};
designs.insert(designs.end(), own.begin(), own.end());
for (std::size_t i = 0; i < designs.size(); ++i)
{
std::string error;
auto entry{template_of(designs[i], error)};
if (false == entry.has_value())
{
WriteLog("Editor: switch template \"" + designs[i].name + "\" left out: " + error, logtype::generic);
continue;
}
entry->design = static_cast<int>(i);
result.push_back(std::move(*entry));
}
// the slips and crossings of Id-4: the angle comes from the straights clicked, the half length from the crossing to the joint
// to a and the label; the slips with the blades outside of the diamond have a straight in the middle of their curves, along which
// the near rails touch. The asymmetric slips of Id-4 (1:9/1:7,5, 1:9/1:6,6) are left out, both of their curves being the same here
struct slipping
{
char const *type;
char const *name;
double radius;
double middle;
double half;
};
slipping const slips[] = {
{"Rkp", "60E1-190-1:9", 190.0, 0.0, 16.615},
{"Rkp", "49E1-190-1:9", 190.0, 0.0, 16.615},
{"Rkp", "49E1-190-1:6,6", 190.0, -1.0, 17.396},
{"Rkp", "49E1-300-1:9", 300.0, -1.0, 22.639},
{"Rkp", "49E1-500-1:9", 500.0, -1.0, 27.693},
{"Rkp", "S42-230-1:9,51", 230.0, 0.0, 18.642},
{"Rkp", "S42-265-1:10", 265.0, 0.0, 19.192},
{"Rkpd", "60E1-190-1:9", 190.0, 0.0, 16.615},
{"Rkpd", "49E1-190-1:9", 190.0, 0.0, 16.615},
{"Rkpd", "49E1-190-1:6,6", 190.0, -1.0, 17.396},
{"Rkpd", "49E1-300-1:9", 300.0, -1.0, 22.639},
{"Rkpd", "49E1-500-1:9", 500.0, -1.0, 27.693},
{"Rkpd", "49E1-190-1:9 gauge 1524", 190.0, 0.0, 17.420},
{"Rkpd", "S42-205-1:9", 205.0, 0.0, 17.591},
{"Rkpd", "S42-205-1:9 II", 205.0, 0.0, 18.394},
{"Rkpd", "S42-230-1:9,51", 230.0, 0.0, 18.642},
{"Rkpd", "S42-230-1:9,51 Ia", 230.0, 0.0, 18.342},
{"Rkpd", "S42-265-1:10", 265.0, 0.0, 19.192},
};
auto const ratio_of = [](std::string const &Name) {
auto const at{Name.find("1:")};
auto number{Name.substr(at + 2)};
std::replace(number.begin(), number.end(), ',', '.');
return std::stod(number);
};
for (auto const &entry : slips)
{
switch_template slip;
slip.double_slip = true;
slip.type = entry.type;
slip.radius = entry.radius;
slip.middle = entry.middle;
slip.a = entry.half;
slip.ratio = ratio_of(entry.name);
slip.rail = std::string{entry.name}.substr(0, std::string{entry.name}.find('-'));
slip.label = std::string{entry.type} + ' ' + entry.name + format(" a %.3f", entry.half) + (slip.type == "Rkp" ? " single slip" : " double slip") +
(entry.middle < 0.0 ? ", the blades outside of the diamond" : "") +
(slip.type == "Rkp" ? " (click at a crossing of two straights, on the side of the joined tracks)" : " (click at a crossing of two straights)");
result.push_back(slip);
}
struct crossing
{
char const *name;
double half;
};
crossing const crossings[] = {
{"60E1-1:9", 16.615},
{"60E1-1:4,444", 10.907},
{"49E1-1:2,9008", 6.904},
{"49E1-1:3,224", 7.920},
{"49E1-1:3,683", 9.881},
{"49E1-1:4,44", 10.907},
{"49E1-1:6,6", 17.396},
{"49E1-1:7,5", 18.512},
{"49E1-1:9", 16.615},
{"49E1-1:9 long", 17.016},
{"49E1-1:18,5", 32.409},
{"S42-1:2", 5.743},
{"S42-1:2,9008", 8.250},
{"S42-1:3,429", 9.681},
{"S42-1:3,43", 8.421},
{"S42-1:4,44", 11.788},
{"S42-1:4,444 gauge 1524", 12.189},
{"S42-1:7", 12.932},
{"S42-1:9", 17.591},
{"S42-1:10", 19.192},
};
for (auto const &entry : crossings)
{
switch_template plain;
plain.double_slip = true;
plain.type = "Skrz";
plain.a = entry.half;
plain.ratio = ratio_of(entry.name);
plain.rail = std::string{entry.name}.substr(0, std::string{entry.name}.find('-'));
plain.label = std::string{"Skrz "} + entry.name + format(" a %.3f", entry.half) + " crossing (click at a crossing of two straights)";
result.push_back(plain);
}
return result;
}
namespace
{
// definition of a path in the scenery text, up to its points: type, length, parameters, environment and textures
void write_header(std::ostream &Text, char const *Type, double const Length, double const Sound, TTrack const &Style, material_handle const Second)
{
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"};
Text << Type << ' ' << Length << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << Sound << ' ' << Style.iQualityFlag << ' ' << (Style.iDamageFlag & 127) << ' ' << environment << ' ';
if (Style.m_visible)
Text << "vis " << editor_track::material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << editor_track::material_name(Second) << ' ' << editor_track::texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' ';
else
Text << "unvis ";
}
void write_points(std::ostream &Text, segment_data const &Path)
{
auto const &start{Path.points[segment_data::point::start]};
auto const &control1{Path.points[segment_data::point::control1]};
auto const &control2{Path.points[segment_data::point::control2]};
auto const &end{Path.points[segment_data::point::end]};
Text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' ';
}
void write_ending(std::ostream &Text, TTrack const &Style)
{
if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty())
Text << "railprofile " << Style.m_profile1.first << ' ';
// a piece of the track made from another one stays in its track circuit and under the same overhead line
for (auto const *isolated : Style.Isolated)
Text << "isolated " << isolated->asName << ' ';
if (Style.fOverhead != -1.0)
Text << "overhead " << Style.fOverhead << ' ';
if (Style.m_sleeper_enabled && false == Style.m_sleeper_model_name.empty())
Text << "sleepermodel " << Style.m_sleeper_frequency << ' ' << Style.m_sleeper_model_name << ' ' << (Style.m_sleeper_skin_name.empty() ? std::string{"none"} : Style.m_sleeper_skin_name) << ' ' << Style.m_sleeper_offset.x << ' ' << Style.m_sleeper_offset.y << ' ' << Style.m_sleeper_offset.z << ' ' << Style.m_sleeper_ballast_z << ' ';
if (false == Style.m_timber_model_name.empty())
Text << "timbermodel " << Style.m_timber_model_name << ' ' << (Style.m_timber_skin_name.empty() ? std::string{"none"} : Style.m_timber_skin_name) << ' ';
Text << "endtrack\n";
}
material_handle ballast_of(TTrack const &Style)
{
return Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2;
}
} // namespace
TTrack *editor_track::create_turntable(segment_data const &Path, TTrack const *Style, std::string const &Name)
{
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
auto const length{glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end])};
if (Style != nullptr && (Style->iCategoryFlag & 15) == 1 && Style->m_visible)
write_header(text, "turn", length, Style->fSoundDistance, *Style, null_handle);
else
text << "turn " << length << " 1.435 0.15 25 20 0 flat vis rail_screw_used1 4 none 0.2 0.5 1.3 ";
write_points(text, Path);
text << "endtrack\n";
auto *track{load_text(text.str(), Style, Name)};
track->m_paths = {Path};
return track;
}
TTrack *editor_track::create_switch(switch_template const &Template, std::vector<segment_data> const &Paths, TTrack const &Style, std::string const &Name)
{
if (Paths.size() < 2)
return nullptr;
TTrack *track{nullptr};
if (Template.source != nullptr)
{
track = clone(*Template.source, Name);
}
else
{
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
write_header(text, "switch", Template.length, 10.0, Style, Style.m_material1);
write_points(text, Paths[0]);
write_points(text, Paths[1]);
if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle)
text << "trackbed " << material_name(trackbed) << ' ';
write_ending(text, Style);
track = load_path(text.str(), Style, Name);
}
track->SwitchExtension->m_frogs = true;
track->SwitchExtension->m_movablenose = Template.movable;
track->SwitchExtension->m_bladeheel = Template.heel;
track->SwitchExtension->m_bladethrow = Template.bladethrow;
track->m_paths.assign(Paths.begin(), Paths.begin() + 2);
return track;
}
bool editor_track::touches(TTrack const &Track, glm::dvec3 const &Point)
{
for (auto const &path : Track.m_paths)
for (auto const index : {segment_data::point::start, segment_data::point::end})
if (glm::distance(path.points[index], Point) <= kSamePoint)
return true;
return false;
}
TTrack *editor_track::outside_neighbour(straight const &Line, bool const Atend)
{
auto const &point{Atend ? Line.end : Line.start};
for (std::size_t i = 0; i < Line.tracks.size(); ++i)
{
auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]};
for (auto const end : {false, true})
{
if (glm::distance(path.points[end ? segment_data::point::end : segment_data::point::start], point) > kSamePoint)
continue;
auto *neighbour{neighbour_at(*Line.tracks[i], Line.paths[i], end)};
if (neighbour != nullptr && std::find(Line.tracks.begin(), Line.tracks.end(), neighbour) == Line.tracks.end())
return neighbour;
}
}
return nullptr;
}
std::vector<TTrack *> editor_track::straight_affected(std::vector<straight> const &Lines)
{
std::vector<TTrack *> result;
for (auto const &line : Lines)
for (auto *track : line.tracks)
add_unique(result, track);
auto const members{result};
auto const member = [&](TTrack const *Track) { return std::find(members.begin(), members.end(), Track) != members.end(); };
for (auto *track : members)
{
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
{
for (auto const atend : {false, true})
{
auto *neighbour{neighbour_at(*track, i, atend)};
if (neighbour == nullptr || member(neighbour))
continue;
add_unique(result, neighbour);
if (false == has_switch_paths(*neighbour))
continue;
for (auto *next : neighbours(*neighbour))
if (false == member(next))
add_unique(result, next);
}
}
}
return result;
}
void editor_track::move_straights(std::vector<straight> const &Lines, std::vector<std::pair<glm::dvec3, glm::dvec3>> const &Ends)
{
std::vector<TTrack *> members;
for (auto const &line : Lines)
for (auto *track : line.tracks)
add_unique(members, track);
auto const member = [&](TTrack const *Track) { return std::find(members.begin(), members.end(), Track) != members.end(); };
for (std::size_t i = 0; i < Lines.size() && i < Ends.size(); ++i)
move_straight(Lines[i], Ends[i].first, Ends[i].second, member);
}
void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function<bool(TTrack const *)> const &Member)
{
if (Line.tracks.empty() || Line.length < 1e-6)
return;
auto const &direction{Line.direction};
auto const newplan{plan_of(End - Start)};
auto const newlength{glm::length(newplan)};
if (newlength < 1e-3)
return;
auto const newdirection{newplan / newlength};
auto const angle{std::atan2(cross(direction, newdirection), glm::dot(direction, newdirection))};
auto const along = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - Line.start), direction); };
struct interval
{
double from;
double to;
bool rigid;
};
std::vector<interval> intervals;
double rigidlength{0.0};
for (std::size_t i = 0; i < Line.tracks.size(); ++i)
{
auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]};
auto const a{along(path.points[segment_data::point::start])};
auto const b{along(path.points[segment_data::point::end])};
auto const rigid{has_switch_paths(*Line.tracks[i])};
intervals.push_back({std::min(a, b), std::max(a, b), rigid});
if (rigid)
rigidlength += std::abs(b - a);
}
std::sort(intervals.begin(), intervals.end(), [](interval const &A, interval const &B) { return A.from < B.from; });
auto const scale{Line.length - rigidlength > 1e-6 ? (newlength - rigidlength) / (Line.length - rigidlength) : 1.0};
auto const remap = [&](double const Along) {
double mapped{0.0};
double previous{0.0};
for (auto const &span : intervals)
{
if (Along <= span.from)
return mapped + (Along - previous) * scale;
mapped += (span.from - previous) * scale;
previous = span.from;
auto const factor{span.rigid ? 1.0 : scale};
if (Along <= span.to)
return mapped + (Along - previous) * factor;
mapped += (span.to - previous) * factor;
previous = span.to;
}
return mapped + (Along - previous) * scale;
};
auto const transform = [&](glm::dvec3 const &Point) {
auto const a{along(Point)};
auto const offset{cross(direction, plan_of(Point - Line.start))};
auto const mapped{remap(a)};
auto const oldheight{Line.start.y + (Line.end.y - Line.start.y) * a / Line.length};
auto const newheight{Start.y + (End.y - Start.y) * mapped / newlength};
auto const moved{plan_of(Start) + newdirection * mapped + glm::dvec2{-newdirection.y, newdirection.x} * offset};
return glm::dvec3{moved.x, Point.y - oldheight + newheight, moved.y};
};
auto const &member{Member};
std::vector<std::pair<glm::dvec3, glm::dvec3>> joints;
for (std::size_t i = 0; i < Line.tracks.size(); ++i)
{
auto &track{*Line.tracks[i]};
auto const before{track.m_paths};
if (has_switch_paths(track))
{
auto const pivot{before[Line.paths[i]].points[segment_data::point::start]};
rigid_move(track, pivot, transform(pivot), angle);
}
else
{
for (auto &path : track.m_paths)
{
for (auto const index : {segment_data::point::start, segment_data::point::end})
path.points[index] = transform(path.points[index]);
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
{
auto &control{path.points[index]};
auto const moved{turned(plan_of(control), angle) * scale};
control = {moved.x, control.y * scale, moved.y};
}
}
}
for (std::size_t j = 0; j < before.size(); ++j)
for (auto const index : {segment_data::point::start, segment_data::point::end})
joints.emplace_back(before[j].points[index], track.m_paths[j].points[index]);
}
std::vector<TTrack *> moved;
for (auto *track : Line.tracks)
{
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
{
for (auto const atend : {false, true})
{
auto *neighbour{neighbour_at(*track, i, atend)};
if (neighbour == nullptr || member(neighbour) || std::find(moved.begin(), moved.end(), neighbour) != moved.end())
continue;
moved.push_back(neighbour);
for (auto const &joint : joints)
{
bool touches{false};
for (auto const &path : neighbour->m_paths)
for (auto const index : {segment_data::point::start, segment_data::point::end})
touches |= glm::distance(path.points[index], joint.first) <= kSamePoint;
if (false == touches)
continue;
if (has_switch_paths(*neighbour))
{
auto const before{neighbour->m_paths};
rigid_move(*neighbour, joint.first, joint.second, angle);
for (auto *next : neighbours(*neighbour))
{
if (member(next) || next == neighbour)
continue;
for (std::size_t j = 0; j < before.size(); ++j)
for (auto const index : {segment_data::point::start, segment_data::point::end})
follow_joint(*next, before[j].points[index], neighbour->m_paths[j].points[index], angle);
}
}
else
{
follow_joint(*neighbour, joint.first, joint.second, angle);
}
break;
}
}
}
}
}
std::vector<TTrack *> editor_track::relay(chain const &Chain, std::vector<segment_data> const &Pieces, TTrack **Last)
{
std::vector<TTrack *> created;
auto const count{Chain.tracks.size()};
if (count == 0 || Pieces.size() < count)
return created;
std::vector<TTrack *> tracks;
std::size_t previous{count};
for (std::size_t i = 0; i < Pieces.size(); ++i)
{
auto const index{i * count / Pieces.size()};
TTrack *track;
bool forward;
if (index != previous)
{
track = Chain.tracks[index];
forward = Chain.forward[index];
}
else
{
track = clone(*Chain.tracks[index]);
created.push_back(track);
forward = true;
}
previous = index;
track->m_paths.resize(1);
track->m_paths.front() = forward ? Pieces[i] : reversed(Pieces[i]);
tracks.push_back(track);
}
commit(tracks);
if (Last != nullptr)
*Last = tracks.back();
return created;
}
TTrack *editor_track::clone(TTrack const &Template, std::string const &Name)
{
std::ostringstream text;
Template.export_as_text_(text);
auto content{text.str()};
auto const type{content.find("track ")};
if (type == 0)
content.erase(0, 6);
auto *track{load_path(content, Template, Name)};
track->m_friction = Template.m_friction;
return track;
}
TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path, std::string const &Name)
{
auto const category{Style.iCategoryFlag & 15};
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
write_header(text, category == 2 ? "road" : category == 4 ? "river" : "normal", glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end]), Style.fSoundDistance, Style, ballast_of(Style));
write_points(text, Path);
if (auto const speed{velocity(Style)}; speed > 0.0)
text << "velocity " << speed << ' ';
write_ending(text, Style);
auto *track{load_path(text.str(), Style, Name)};
track->m_paths = {Path};
return track;
}
glm::dvec3 editor_track::point_at(TTrack const &Track, double const T)
{
auto const &path{Track.m_paths.front()};
auto const p0{path.points[segment_data::point::start]};
auto const p3{path.points[segment_data::point::end]};
auto const p1{p0 + path.points[segment_data::point::control1]};
auto const p2{p3 + path.points[segment_data::point::control2]};
auto const u{1.0 - T};
return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3;
}
double editor_track::nearest_parameter(TTrack const &Track, glm::dvec3 const &Point)
{
if (Track.m_paths.empty())
return 0.5;
auto const distance = [&](double const T) { return geometry::plan_distance(point_at(Track, T), Point); };
double best{0.5};
for (int i = 0; i <= 200; ++i)
if (distance(i / 200.0) < distance(best))
best = i / 200.0;
double step{1.0 / 200.0};
for (int i = 0; i < 30; ++i)
{
step *= 0.5;
if (best - step > 0.0 && distance(best - step) < distance(best))
best -= step;
else if (best + step < 1.0 && distance(best + step) < distance(best))
best += step;
}
return best;
}
TTrack *editor_track::split_path(TTrack &Track, double const T, std::string const &Name)
{
if (Track.eType != tt_Normal || Track.m_paths.empty() || T <= 0.001 || T >= 0.999)
return nullptr;
auto const path{Track.m_paths.front()};
auto const p0{path.points[segment_data::point::start]};
auto const p3{path.points[segment_data::point::end]};
segment_data first{path};
segment_data second{path};
auto const roll{static_cast<float>(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * T)};
if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{})
{
// the parameter is the one of the cubic with the controls at the ends, as everywhere else, not a share of the length
auto const u{1.0 - T};
auto const middle{p0 * (u * u * u + 3.0 * u * u * T) + p3 * (3.0 * u * T * T + T * T * T)};
first.points[segment_data::point::end] = middle;
second.points[segment_data::point::start] = middle;
}
else
{
auto const p1{p0 + path.points[segment_data::point::control1]};
auto const p2{p3 + path.points[segment_data::point::control2]};
auto const p01{glm::mix(p0, p1, T)};
auto const p12{glm::mix(p1, p2, T)};
auto const p23{glm::mix(p2, p3, T)};
auto const p012{glm::mix(p01, p12, T)};
auto const p123{glm::mix(p12, p23, T)};
auto const middle{glm::mix(p012, p123, T)};
first.points[segment_data::point::control1] = p01 - p0;
first.points[segment_data::point::control2] = p012 - middle;
first.points[segment_data::point::end] = middle;
second.points[segment_data::point::start] = middle;
second.points[segment_data::point::control1] = p123 - middle;
second.points[segment_data::point::control2] = p23 - p3;
}
first.rolls[1] = roll;
second.rolls[0] = roll;
auto *created{create_path(Track, second, Name)};
created->iDamageFlag = Track.iDamageFlag;
created->iAction |= Track.iAction & 0x80;
created->fVelocity = Track.fVelocity;
created->fVerticalRadius = Track.fVerticalRadius;
created->m_friction = Track.m_friction;
Track.m_paths.front() = first;
// the events stay where the trains run into them: those of the runs towards the end at the start, in the original,
// those of the runs towards the start at the end, in the new piece; those of a stop in both. the ones bound
// by the name of the path stay with it
for (auto const index : {0, 3})
for (auto const &event : events(Track, index))
if (false == event.first.empty())
events(*created, index).push_back(event);
for (auto const index : {1, 4})
{
auto &own{events(Track, index)};
for (auto const &event : own)
if (false == event.first.empty())
events(*created, index).push_back(event);
own.erase(std::remove_if(own.begin(), own.end(), [](auto const &Event) { return false == Event.first.empty(); }), own.end());
}
std::string missing;
bind_events(Track, missing);
bind_events(*created, missing);
if (s_observer != nullptr)
s_observer->track_split(Track, *created, first);
commit({&Track, created});
return created;
}
TTrack *editor_track::load_path(std::string const &Text, TTrack const &Template, std::string const &Name)
{
return load_text(Text, &Template, Name);
}
TTrack *editor_track::create_path(path_style const &Style, segment_data const &Path)
{
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
auto const length{glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end])};
text << "normal " << length << " 1.435 0.15 25 20 0 flat vis " << Style.rail << " 6 " << Style.ballast << " 0.2 0.5 1.1 ";
write_points(text, Path);
if (Style.velocity > 0.0)
text << "velocity " << Style.velocity << ' ';
if (false == Style.rail_profile.empty())
text << "railprofile " << Style.rail_profile << ' ';
text << "endtrack\n";
auto *track{load_text(text.str(), nullptr, {})};
track->m_paths = {Path};
return track;
}
TTrack *editor_track::load_text(std::string const &Text, TTrack const *Template, std::string const &Name)
{
cParser parser(Text, cParser::buffer_TEXT);
scene::node_data data;
data.type = "track";
auto const base{false == Name.empty() ? Name : Template == nullptr || Template->name().empty() || Template->name() == "none" ? std::string{"editor_track"} : Template->name()};
data.name = Name;
data.layer = Template != nullptr ? Template->layer() : scene::Layers.active();
for (int i = 1; data.name.empty() || simulation::Paths.find(data.name) != nullptr; ++i)
data.name = base + "_" + std::to_string(i);
auto *track = new TTrack(data);
if (Template != nullptr)
{
track->m_rangesquaredmin = Template->m_rangesquaredmin;
track->m_rangesquaredmax = Template->m_rangesquaredmax;
}
track->Load(&parser, glm::dvec3{});
for (auto *events : {&track->m_events0, &track->m_events1, &track->m_events2, &track->m_events0all, &track->m_events1all, &track->m_events2all})
events->clear();
track->m_events = false;
simulation::Paths.insert(track);
return track;
}
void editor_track::retire(TTrack &Track)
{
auto const adjoining{neighbours(Track)};
disconnect(Track);
simulation::Region->erase_and_unregister(&Track);
Track.m_editorremoved = true;
// the name is free for the path laid in its place, as the switches of a crossover laid again keep theirs
simulation::Paths.rename(&Track, std::string{});
for (auto *neighbour : adjoining)
{
update_transition(*neighbour);
rebuild_geometry(*neighbour);
}
auto crossings{Track.m_crossings};
for (auto *crossing : Track.m_crossings)
for (auto *neighbour : neighbours(*crossing))
if (std::find(crossings.begin(), crossings.end(), neighbour) == crossings.end())
crossings.push_back(neighbour);
for (auto *crossing : crossings)
if (crossing != &Track && false == crossing->m_editorremoved)
rebuild_geometry(*crossing);
if (s_observer != nullptr)
s_observer->track_retired(Track);
}
void editor_track::revive(TTrack &Track)
{
Track.m_editorremoved = false;
if (false == simulation::Paths.rename(&Track, Track.name()))
WriteLog("Editor: the path " + Track.name() + " came back, but another one has its name now", logtype::generic);
}
void editor_track::rebuild_geometry(TTrack &Track)
{
auto const rebuild = [](TTrack &Path) {
auto &section{simulation::Region->section(Path.location())};
Path.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{});
};
auto crossings{Track.m_crossings};
rebuild(Track);
for (auto *crossing : Track.m_crossings)
if (std::find(crossings.begin(), crossings.end(), crossing) == crossings.end())
crossings.push_back(crossing);
for (std::size_t i = 0, count = crossings.size(); i < count; ++i)
for (auto *neighbour : neighbours(*crossings[i]))
if (std::find(crossings.begin(), crossings.end(), neighbour) == crossings.end())
crossings.push_back(neighbour);
for (auto *crossing : crossings)
if (crossing != &Track && false == crossing->m_editorremoved)
rebuild(*crossing);
}
std::string editor_track::switch_template::title() const
{
std::istringstream words(label);
std::vector<std::string> kept;
std::vector<std::string> all;
for (std::string word; words >> word;)
all.push_back(word);
for (std::size_t i = 0; i < all.size(); ++i)
{
if (all[i].front() == '(')
break;
auto const number = [](std::string const &Word) {
char *end{nullptr};
std::strtod(Word.c_str(), &end);
return end != Word.c_str() && *end == '\0';
};
if ((all[i] == "a" || all[i] == "b") && i + 1 < all.size() && number(all[i + 1]))
{
++i;
continue;
}
kept.push_back(all[i]);
}
std::string result;
for (auto const &word : kept)
result += (result.empty() ? "" : " ") + word;
return result.empty() ? label : result;
}