17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 19:09:25 +02:00
Files
maszyna/editor/editorTrack.cpp
maj00r a756beb34b feat(editor): track gizmo rings along the track: direction, grade and cant
The rotate gizmo stands along the track. The cross ring changes the grade about the middle or the selected
end, the joined tracks take the grade at the joints; the ring along the track changes the cant of the track
or of the selected end, the joined tracks take it at the joints and ramp it. On a control vector the cross
ring tilts it up or down. Ctrl steps by 5 degrees, 1 per mille, 5 mm.
2026-10-11 21:33:27 +02:00

2928 lines
103 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 "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 <algorithm>
#include <cmath>
#include <limits>
#include <deque>
#include <map>
#include <numeric>
#include <functional>
#include <sstream>
#include <tuple>
#include <unordered_set>
namespace
{
using geometry::bezier;
using geometry::chord_length;
using geometry::cross;
using geometry::plan_of;
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;
}
segment_data reversed(segment_data const &Path)
{
segment_data result{Path};
result.points[segment_data::point::start] = Path.points[segment_data::point::end];
result.points[segment_data::point::end] = Path.points[segment_data::point::start];
result.points[segment_data::point::control1] = Path.points[segment_data::point::control2];
result.points[segment_data::point::control2] = Path.points[segment_data::point::control1];
result.rolls = {-Path.rolls[1], -Path.rolls[0]};
return result;
}
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.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;
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.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;
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)
{
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]};
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.points[segment_data::point::start] += Offset;
path.points[segment_data::point::end] += 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)
{
for (auto const index : {segment_data::point::start, segment_data::point::end})
path.points[index] = Pivot + rotate(path.points[index] - Pivot);
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
path.points[index] = rotate(path.points[index]);
}
}
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;
move_point(*joint.other, joint.theirs, point_position(Track, joint.own));
auto const angle{signed_angle(joint.direction, end_direction(Track.m_paths[joint.own.path], joint.own.kind))};
auto const moved{turned(plan_of(joint.control), angle)};
joint.other->m_paths[joint.theirs.path].points[joint.theirs.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2] = {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));
}
}
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};
control = -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, 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}, high{0.0};
glm::dvec3 lowpoint{origin}, 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; }); };
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))
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 (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 = 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;
}
}
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)
{
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 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);
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 (current != 0)
sign = current;
++i;
}
add_curve(first, 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 (glm::distance(candidate.points[segment_data::point::start], exit) <= kSamePoint)
result.emplace_back(next, i, true);
else if (glm::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]};
bezier const curve{path};
auto const count{std::max(2, static_cast<int>(std::ceil(curve.plan_length(16) / std::max(0.1, Step))))};
span.from = chainage;
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(path.rolls[0] + (path.rolls[1] - path.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;
}
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)
{
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()};
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));
};
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;
}
std::vector<double> cuts;
for (auto const chainage : Breaks)
if (chainage > span.from + margin && chainage < span.to - margin)
cuts.push_back(chainage);
if (cuts.empty())
{
spans.push_back(span);
continue;
}
remember(span.track);
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{bezier(piece->m_paths.front()).parameter(cut - origin)};
auto *created{split_path(*piece, t)};
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{bezier(span.track->m_paths.front()).parameter(span.to - *cut)};
auto *created{split_path(*span.track, t)};
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;
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);
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)
plane_heights(span.track->m_paths[i], 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 switch 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::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())
{
if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2 || (track->iCategoryFlag & 0x80) != 0)
continue;
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)
continue;
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.points[segment_data::point::start] = tolocal(path.points[segment_data::point::start] - origin);
local.points[segment_data::point::end] = tolocal(path.points[segment_data::point::end] - origin);
local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]);
local.points[segment_data::point::control2] = tolocal(path.points[segment_data::point::control2]);
}
if (glm::length(plan_of(entry.local[1].points[segment_data::point::start])) > kSamePoint)
continue;
if (entry.local[1].points[segment_data::point::end].x < 0.0)
{
for (auto &path : entry.local)
{
for (auto &point : path.points)
point.x = -point.x;
path.rolls = {-path.rolls[0], -path.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);
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;
}
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());
lookup.emplace(key, result.size());
result.push_back(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};
};
for (auto const &local : Template.local)
{
segment_data path{local};
path.points[segment_data::point::start] = Origin + vector(local.points[segment_data::point::start]);
path.points[segment_data::point::end] = Origin + vector(local.points[segment_data::point::end]);
path.points[segment_data::point::control1] = vector(local.points[segment_data::point::control1]);
path.points[segment_data::point::control2] = vector(local.points[segment_data::point::control2]);
if (Side < 0)
path.rolls = {-local.rolls[0], -local.rolls[1]};
result.push_back(path);
}
return result;
}
std::vector<editor_track::switch_template> editor_track::standard_switch_templates()
{
struct definition
{
char const *name;
double radius;
double ratio;
double a;
double b;
};
definition const definitions[] = {
{"Rz 1:9 R190", 190.0, 9.0, 10.523, 16.615},
{"Rz 1:9 R300", 300.0, 9.0, 16.615, 16.615},
{"Rz 1:12 R500", 500.0, 12.0, 20.797, 0.0},
{"Rz 1:14 R760", 760.0, 14.0, 0.0, 0.0},
{"Rz 1:18,5 R1200", 1200.0, 18.5, 32.409, 34.943},
{"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0},
};
std::vector<switch_template> result;
for (auto const &definition : definitions)
{
auto const angle{std::atan(1.0 / definition.ratio)};
auto const a{definition.a > 0.0 ? definition.a : definition.radius * std::tan(angle * 0.5)};
auto const estimated{definition.b <= 0.0};
auto const b{estimated ? 1.87 * definition.ratio : definition.b};
auto const offset{b * std::tan(angle)};
switch_template entry;
entry.a = a;
entry.b = b;
entry.length = a + b;
entry.ratio = definition.ratio;
entry.radius = definition.radius;
auto &main{entry.local[0]};
main.points[segment_data::point::end] = {0.0, 0.0, a + b};
auto &diverging{entry.local[1]};
diverging.points[segment_data::point::end] = {offset, 0.0, a + b};
diverging.points[segment_data::point::control1] = {0.0, 0.0, a * 2.0 / 3.0};
diverging.points[segment_data::point::control2] = {-offset * 2.0 / 3.0, 0.0, -b * 2.0 / 3.0};
diverging.radius = static_cast<float>(definition.radius);
entry.label = std::string{definition.name} + " a " + std::to_string(a).substr(0, std::to_string(a).find('.') + 4) + " b " + std::to_string(b).substr(0, std::to_string(b).find('.') + 4) + (estimated ? " (b est.)" : "");
result.push_back(entry);
}
for (auto const &slipping : {std::pair<double, char const *>{190.0, "1:9"}, {300.0, "1:9"}, {500.0, "1:12"}})
{
switch_template slip;
slip.double_slip = true;
slip.radius = slipping.first;
slip.label = std::string{"Rkpd "} + slipping.second + " R" + std::to_string(static_cast<int>(slipping.first)) + " double slip (click at a crossing of two straights)";
result.push_back(slip);
}
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 << ' ';
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);
}
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->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, 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}, 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)
{
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);
return created;
}
TTrack *editor_track::clone(TTrack const &Template)
{
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)};
track->m_friction = Template.m_friction;
return track;
}
TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path)
{
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)};
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)
{
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)};
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;
for (auto *neighbour : adjoining)
{
update_transition(*neighbour);
rebuild_geometry(*neighbour);
}
if (s_observer != nullptr)
s_observer->track_retired(Track);
}
void editor_track::rebuild_geometry(TTrack &Track)
{
auto &section{simulation::Region->section(Track.location())};
Track.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{});
}