/* 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 #include #include #include #include #include #include #include #include #include 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 &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(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(Track), i); result.name = Track.name(); return result; } std::vector> &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 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(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(character - 'A' + 'a'); return Name; } int track_parameter(scene::instance_handle const Instance, std::string &File, std::vector &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 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(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 values; auto const index{track_parameter(handle, file, values)}; result.includes += (index >= 0 && lower_name(values[index]) == name) ? 1 : 0; } static std::array 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 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 const &Tracks) { if (Tracks.empty()) return; std::vector 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 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(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(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 editor_track::neighbours(TTrack const &Track) { std::vector 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> editor_track::connected_points(TTrack const &Track, glm::dvec3 const &Position) { std::vector> result; for (auto *neighbour : neighbours(Track)) { for (int i = 0; i < static_cast(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::joints(TTrack const &Track) { std::vector result; for (int i = 0; i < static_cast(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 const &Joints) { for (auto const &joint : Joints) { if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(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}; } } 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 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 const &Exclude) { auto const found{free_ends(Self, Category, Position, Radius, Exclude)}; return found.empty() ? snap_target{} : found.front(); } std::vector editor_track::free_ends(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude) { std::vector 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(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(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(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_parts(chain const &Chain) { std::vector 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]; 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(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> 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::find_straights(double const Minimumlength, straight_tolerance const &Tolerance) { std::vector result; std::unordered_set 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> run{{&Track, true}}; for (auto const throughend : {true, false}) { TTrack *previous{&Track}; TTrack *current{throughend ? Track.trNext : Track.trPrev}; while (curved(current) && std::none_of(run.begin(), run.end(), [&](auto const &Member) { return Member.first == current; })) { 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> const &Run, double const Gauge, curve &Curve) { Curve = {}; if (Run.empty()) return false; auto const &run{Run}; std::vector turns; std::vector lengths; std::vector varying; std::vector 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 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->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(roll))))); } auto const peak{*std::max_element(curvatures.begin(), curvatures.end())}; for (std::size_t i = 0; i < run.size(); ++i) if (curvatures[i] < 0.9 * peak) varying[i] = true; 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 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 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::recognize_line(chain const &Chain, straight_tolerance const &Tolerance, double const Gauge) { std::vector 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 straight(count); std::vector 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> 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> &States, std::vector &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; std::vector route_next(route_node const &Node) { std::vector 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(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 nodes; std::vector parents; std::map visited; for (int i = 0; i < static_cast(From->m_paths.size()); ++i) for (auto const forward : {true, false}) { route_node const node{From, i, forward}; visited.emplace(node, static_cast(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(nodes.size())); nodes.push_back(next); parents.push_back(static_cast(index)); if (std::get<0>(next) == To) { goal = static_cast(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 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::sample_route(route &Route, double const Step) { std::vector 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(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(j) / count : 1.0 - static_cast(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 auto interpolate(std::vector 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 const &Samples, double const Chainage) { return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position.y; }); } double editor_track::sampled_grade(std::vector const &Samples, double const Chainage) { return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.grade; }); } glm::dvec3 editor_track::sampled_position(std::vector 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::apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &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 spans; for (auto const &span : Route.spans) { if (span.track->eType != tt_Normal) { spans.push_back(span); continue; } std::vector 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 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 joints; std::vector 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(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 gaps; std::vector 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::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 &Pieces) { auto existing{route_of(Chain)}; auto const samples{sample_route(existing, 1.0)}; if (samples.empty() || existing.length <= 1e-6) return; std::vector 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::find_switch_templates() { std::vector result; std::map 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 editor_track::place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade) { std::vector result; glm::dvec2 const normal{-Direction.y, Direction.x}; auto const side{static_cast(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::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 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(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{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(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::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 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::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 editor_track::straight_affected(std::vector const &Lines) { std::vector 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(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 const &Lines, std::vector> const &Ends) { std::vector 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 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 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> 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 moved; for (auto *track : Line.tracks) { for (int i = 0; i < static_cast(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 editor_track::relay(chain const &Chain, std::vector const &Pieces) { std::vector created; auto const count{Chain.tracks.size()}; if (count == 0 || Pieces.size() < count) return created; std::vector 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::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(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::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 §ion{simulation::Region->section(Track.location())}; Track.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{}); }