From b07050fa5283c86778bdf7ec760c8234b334ed54 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 5 Oct 2026 01:21:11 +0200 Subject: [PATCH] feat(tracks): switches keep their neighbours joined on replacing, flipping and moving; events copied on splitting; circuits, overhead line, sleepers and events of the paths; curve design through the switches of a line; changes in files included more than once or with parameters reported instead of written; split of a straight at the point found, not a share of its length --- editor/editorAlignment.cpp | 65 ++++-- editor/editorAlignment.hpp | 5 +- editor/editorTrack.cpp | 421 +++++++++++++++++++++++++++++++++++-- editor/editorTrack.hpp | 81 ++++++- scene/scenelayerssave.cpp | 35 ++- 5 files changed, 567 insertions(+), 40 deletions(-) diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 29a85b01..5bc319f3 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -269,6 +269,11 @@ result fit_between(design const &Design) auto const start{plan_of(Design.start)}; auto const end{plan_of(Design.end)}; + if (glm::length(Design.start_direction) < 1e-9 || glm::length(Design.end_direction) < 1e-9) + { + r.errors.emplace_back("The direction of an end of the fragment is undefined, its end path has zero length"); + return r; + } auto const startdirection{glm::normalize(Design.start_direction)}; auto const enddirection{glm::normalize(Design.end_direction)}; auto const count{Design.vertices.size()}; @@ -872,7 +877,8 @@ std::size_t minimum_pieces(result const &Result, design const &Design) return count; } -std::vector pieces(result const &Result, design const &Design, std::size_t const Count) +std::vector pieces(result const &Result, design const &Design, std::size_t const Count, std::vector const &Breaks, std::vector const &Counts, + std::vector *Intervals) { struct span { @@ -920,23 +926,56 @@ std::vector pieces(result const &Result, design const &Design, std } spans.erase(spans.begin() + i); } - while (false == spans.empty() && spans.size() < Count) - { - auto longest = std::max_element(spans.begin(), spans.end(), [&](span const &A, span const &B) { return extent(A) < extent(B); }); - span second{*longest}; - if (longest->generic) + // splits the span at the chainage + auto const split = [&](std::vector::iterator Span, double const Chainage) { + span second{*Span}; + if (Span->generic) { - auto const middle{(longest->start + longest->end) * 0.5}; - second.start = middle; - longest->end = middle; + second.start = Chainage; + Span->end = Chainage; } else { - auto const middle{(longest->from + longest->to) * 0.5}; - second.from = middle; - longest->to = middle; + auto const local{Chainage - Result.elements[Span->element].chainage}; + second.from = local; + Span->to = local; } - spans.insert(std::next(longest), second); + spans.insert(std::next(Span), second); + }; + double constexpr margin{0.01}; + for (auto const chainage : Breaks) + { + auto inside{std::find_if(spans.begin(), spans.end(), [&](span const &Span) { return chainage_from(Span) < chainage - margin && chainage_to(Span) > chainage + margin; })}; + if (inside != spans.end()) + split(inside, chainage); + } + auto const interval = [&](span const &Span) { + return static_cast(std::upper_bound(Breaks.begin(), Breaks.end(), (chainage_from(Span) + chainage_to(Span)) * 0.5) - Breaks.begin()); + }; + for (std::size_t k = 0; k < Counts.size(); ++k) + { + while (true) + { + auto const inside{static_cast(std::count_if(spans.begin(), spans.end(), [&](span const &Span) { return interval(Span) == k; }))}; + if (inside == 0 || inside >= Counts[k]) + break; + auto longest{spans.end()}; + for (auto candidate = spans.begin(); candidate != spans.end(); ++candidate) + if (interval(*candidate) == k && (longest == spans.end() || extent(*candidate) > extent(*longest))) + longest = candidate; + split(longest, (chainage_from(*longest) + chainage_to(*longest)) * 0.5); + } + } + while (false == spans.empty() && spans.size() < Count) + { + auto longest = std::max_element(spans.begin(), spans.end(), [&](span const &A, span const &B) { return extent(A) < extent(B); }); + split(longest, (chainage_from(*longest) + chainage_to(*longest)) * 0.5); + } + if (Intervals != nullptr) + { + Intervals->clear(); + for (auto const &piece : spans) + Intervals->push_back(interval(piece)); } std::vector result; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index fcf6132b..c57b386f 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -173,6 +173,9 @@ double elevation(result const &Result, double const Chainage); double grade(result const &Result, double const Chainage); std::size_t minimum_pieces(result const &Result, design const &Design); -std::vector pieces(result const &Result, design const &Design, std::size_t const Count); +// Breaks: ascending chainages at which pieces have to end. Counts: least number of the pieces between the consecutive breaks, +// one more than the breaks; Intervals receives the number of the interval of each piece +std::vector pieces(result const &Result, design const &Design, std::size_t const Count, std::vector const &Breaks = {}, std::vector const &Counts = {}, + std::vector *Intervals = nullptr); } diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 562f6391..d4dcb9c4 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -15,6 +15,7 @@ http: #include "world/Track.h" #include "vehicle/DynObj.h" #include "scene/scene.h" +#include "scene/scenelayers.h" #include "simulation/simulation.h" #include "rendering/renderer.h" #include "utilities/Globals.h" @@ -145,9 +146,82 @@ editor_track::state editor_track::capture(TTrack const &Track) 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); 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; @@ -170,6 +244,19 @@ void editor_track::apply(TTrack &Track, state const &State) 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; }); + }); } bool editor_track::is_supported(TTrack const &Track) @@ -198,6 +285,12 @@ bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason) 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; } @@ -387,6 +480,62 @@ std::vector> editor_track::connecte 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)) @@ -395,12 +544,16 @@ bool editor_track::is_connected(TTrack const &Track, point_ref const &Point) } 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) { snap_target result; result.distance = std::numeric_limits::max(); auto const excluded = [&](TTrack const *Other) { return std::find(Exclude.begin(), Exclude.end(), Other) != Exclude.end(); }; - auto const category{Track.iCategoryFlag & 15}; auto const sections{simulation::Region->sections(Position, static_cast(Radius))}; for (auto *section : sections) { @@ -408,7 +561,7 @@ editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, gl { for (auto *other : cell.m_directories.paths) { - if (other == &Track || excluded(other) || (other->iCategoryFlag & 15) != category || false == is_supported(*other)) + if (other == Self || excluded(other) || (other->iCategoryFlag & 15) != Category || false == is_supported(*other)) continue; for (int i = 0; i < static_cast(other->m_paths.size()); ++i) { @@ -693,7 +846,7 @@ void editor_track::store_switch_path(TTrack &Switch, int const Path) } } -bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error) +bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error, bool const Switches) { Chain = {}; if (From == nullptr || To == nullptr) @@ -717,10 +870,25 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin { 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() < 10000 && regular(current)) + 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); @@ -738,12 +906,29 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin if (false == found) { Chain = {}; - Error = "The selected paths aren't connected by a chain of regular paths (switches end the 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(); + } - auto const category{From->iCategoryFlag & 15}; + 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) @@ -761,24 +946,45 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin Chain.radius = radius; } - auto const &first{From->m_paths.front()}; + 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); + 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 &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); + 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 { @@ -874,6 +1080,11 @@ void follow_joint(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, d } // 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) @@ -1034,7 +1245,15 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance 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; @@ -1165,6 +1384,102 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance 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 { @@ -1649,25 +1964,39 @@ std::vector editor_track::apply_profile(route &Route, 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}; - for (auto const &path : joint.neighbour->m_paths) + glm::dvec3 *closest{nullptr}; + for (auto &path : joint.neighbour->m_paths) for (auto const index : {segment_data::point::start, segment_data::point::end}) { - auto const &point{path.points[index]}; + 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) - gaps.push_back({joint.track, joint.neighbour, gap}); + 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; } @@ -1844,7 +2173,7 @@ void write_header(std::ostream &Text, char const *Type, double const Length, dou { 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 << ' ' << 0 << ' ' << environment << ' '; + 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 @@ -1864,6 +2193,13 @@ 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"; } @@ -2203,7 +2539,9 @@ TTrack *editor_track::split_path(TTrack &Track, double const T) 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{}) { - auto const middle{glm::mix(p0, p3, T)}; + // 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; } @@ -2228,6 +2566,24 @@ TTrack *editor_track::split_path(TTrack &Track, double const T) 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}); @@ -2235,18 +2591,43 @@ TTrack *editor_track::split_path(TTrack &Track, double const T) } 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{Template.name().empty() || Template.name() == "none" ? std::string{"editor_track"} : Template.name()}; + 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.layer(); + 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); - track->m_rangesquaredmin = Template.m_rangesquaredmin; - track->m_rangesquaredmax = Template.m_rangesquaredmax; + 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(); diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index d45ecf54..6ed9d70b 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -20,6 +20,7 @@ http: #include "utilities/Classes.h" class TTrack; +class basic_event; class editor_track { @@ -52,10 +53,29 @@ class editor_track float texheight{0.6f}; float texwidth{0.9f}; float texslope{0.9f}; + std::vector isolated; + float overhead{-1.f}; + bool sleepers{false}; + float sleeper_frequency{0.6f}; + std::string sleeper_model; + std::string sleeper_skin; + glm::vec3 sleeper_offset{0.f}; + float sleeper_ballast{0.f}; + std::array>, 6> events; }; static state capture(TTrack const &Track); static void apply(TTrack &Track, state const &State); + // lists of the events of the path, in the order of event_keyword(): event0, event1, event2, eventall0, eventall1, eventall2 + static std::vector> &events(TTrack &Track, int const Index); + static char const *event_keyword(int const Index); + // looks the events up by their names; the names of those which don't exist are given in Missing + static void bind_events(TTrack &Track, std::string &Missing); + // track circuits the path belongs to, made when they don't exist yet + static void isolated(TTrack &Track, std::vector const &Names); + // -1: normal, 0: no current, above 0: speed limit with the pantographs lowered + static void overhead(TTrack &Track, float const Overhead); + static bool can_edit_geometry(TTrack const &Track, std::string &Reason); static bool is_supported(TTrack const &Track); @@ -71,6 +91,19 @@ class editor_track static std::vector neighbours(TTrack const &Track); static std::vector> connected_points(TTrack const &Track, glm::dvec3 const &Position); static bool is_connected(TTrack const &Track, point_ref const &Point); + // end of a regular neighbouring path joined to an end of the track, as it was when taken + struct joint + { + point_ref own; + TTrack *other{nullptr}; + point_ref theirs; + glm::dvec2 direction{0.0, 1.0}; // of the track at its end, into it + glm::dvec3 control{0.0}; // of the neighbour at its end, never zero + }; + // joints the editor can change: switches and paths which can't be edited aren't taken + static std::vector joints(TTrack const &Track); + // moves the joined ends to where the ends of the track are now, turning them as the track turned there + static void follow(TTrack const &Track, std::vector const &Joints); struct snap_target { @@ -81,6 +114,8 @@ class editor_track double distance{0.0}; }; static snap_target find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); + // nearest end of a path of the category which nothing else joins; Self may be null + static snap_target find_free_end(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); static void snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent); struct chain @@ -96,8 +131,21 @@ class editor_track double length{0.0}; double velocity{-1.0}; double radius{0.0}; + // switches the chain runs through along their main track, each before the regular path of the index given + struct passage + { + TTrack *track{nullptr}; + bool forward{true}; + std::size_t before{0}; + }; + std::vector switches; }; - static bool find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error); + // with Switches the chain may run through the switches along their main tracks; its ends are regular paths + static bool find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error, bool const Switches = false); + // the regular parts of a chain between its switches, empty ones skipped + static std::vector chain_parts(chain const &Chain); + // moves the track as a whole, From going to To, turned by Angle in the plan + static void place(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Angle); struct straight { @@ -140,6 +188,18 @@ class editor_track double split{0.5}; }; static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); + // curve made of the given paths, each with the flag of being run from its start + static bool analyse_curve(std::vector> const &Run, double const Gauge, curve &Curve); + // regular paths in line through the track, up to switches, free ends or about Limit metres each way + static bool find_run(TTrack &Track, double const Limit, chain &Chain, std::string &Error); + // the chain read as an alignment: a vertex at the intersection of the tangents of each curve + struct line_vertex + { + glm::dvec2 position{0.0}; + curve shape; + bool kink{false}; // the straights meet with no curve between them + }; + static std::vector recognize_line(chain const &Chain, straight_tolerance const &Tolerance, double const Gauge); // regular straight paths continuing a chain end in line, which the chain can take over struct straight_run { @@ -184,15 +244,17 @@ class editor_track static glm::dvec3 sampled_position(std::vector const &Samples, double const Chainage); // grade of the path which adjoins the end of the route, positive rising along the route static bool adjoining_grade(route const &Route, bool const Atend, double &Grade); - // path off the route whose end no longer meets the adjoining path in height + // path off the route whose end no longer met the adjoining path in height struct height_gap { TTrack *track{nullptr}; TTrack *neighbour{nullptr}; double gap{0.0}; // neighbour - track + bool closed{false}; // the end of the neighbour was brought to the track }; // sets elevations of the paths of the route; regular paths are split at the chainages in Breaks, - // switches are tilted as a whole in the plane of their grade, branches included + // switches are tilted as a whole in the plane of their grade, branches included, and the regular paths + // off the route get their ends joined to the switches brought to the height there static std::vector apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created); // carries elevations of the chain over to the new pieces, in proportion of the length static void keep_heights(chain const &Chain, std::vector &Pieces); @@ -214,6 +276,16 @@ class editor_track static std::vector find_switch_templates(); static TTrack *load_path(std::string const &Text, TTrack const &Template, std::string const &Name = {}); static TTrack *create_path(TTrack const &Style, segment_data const &Path); + // parameters of a path laid where there's no other path to copy them from + struct path_style + { + double velocity{100.0}; + std::string rail{"rail_screw_used1"}; + std::string ballast{"1435mm/tpbps-new2"}; + std::string rail_profile; + }; + // the path goes to the layer receiving the items created in the editor + static TTrack *create_path(path_style const &Style, segment_data const &Path); static TTrack *split_path(TTrack &Track, double const T); static double nearest_parameter(TTrack const &Track, glm::dvec3 const &Point); // point of the first path at the parameter of nearest_parameter() @@ -248,6 +320,8 @@ class editor_track private: static observer *s_observer; + // sets the length, the ends and their directions of the chain of the tracks given + static bool complete_chain(chain &Chain, std::string &Error); static void disconnect(TTrack &Track); static void join(TTrack &Track); static void connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid); @@ -256,5 +330,6 @@ class editor_track static void store_switch_path(TTrack &Switch, int const Path); static void rebuild_geometry(TTrack &Track); static TTrack *clone(TTrack const &Template); + static TTrack *load_text(std::string const &Text, TTrack const *Template, std::string const &Name); static void move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function const &Member); }; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index f92f6a71..f85f25ed 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -792,6 +792,8 @@ save_result node_layers::save(std::vector const &Rootstatements) edit.node = Node; state.patches[Layer].edits.emplace_back(std::move(edit)); }; + // changes which can't be written, as their definition stands for more than one node + std::vector unwritten; // traction and event launchers are rewritten when the editor moved them along with the track they belong to std::vector patchednodes; auto const patch_node = [&](basic_node *Node, auto const &Patch) { @@ -801,10 +803,15 @@ save_result node_layers::save(std::vector const &Rootstatements) return true; } auto const &source{lookup->second}; - if (false == is_output(resolve(source.layer)) || false == writable(source.layer)) + if (false == is_output(resolve(source.layer))) { return true; } + if (false == writable(source.layer)) + { + unwritten.push_back(Node->name()); + return true; + } if (false == load(source.layer)) { return false; @@ -876,15 +883,22 @@ save_result node_layers::save(std::vector const &Rootstatements) { continue; } - created[nodelayer].emplace_back(path, path_text(*path, layer(nodelayer).context_insert().offset)); + // a file included more than once would get a copy of the new path at every inclusion + auto const target{writable(nodelayer) ? nodelayer : root}; + created[target].emplace_back(path, path_text(*path, layer(target).context_insert().offset)); savedpaths.push_back(path); continue; } auto const &source{lookup->second}; - if ((false == path->m_editorremoved && false == path->dirty()) || false == writable(source.layer)) + if (false == path->m_editorremoved && false == path->dirty()) { continue; } + if (false == writable(source.layer)) + { + unwritten.push_back(path->name().empty() ? std::string{"(noname)"} : path->name()); + continue; + } if (false == load(source.layer)) { return fail(state.error); @@ -1636,6 +1650,21 @@ save_result node_layers::save(std::vector const &Rootstatements) result.message += file + (&file != &result.files.back() ? ", " : ""); } } + if (false == unwritten.empty()) + { + std::string names; + for (std::size_t i = 0; i < std::min(unwritten.size(), 5); ++i) + { + names += (i > 0 ? ", " : "") + unwritten[i]; + } + if (unwritten.size() > 5) + { + names += ", ..."; + } + auto const warning{std::to_string(unwritten.size()) + " changed nodes not saved, their files are included with parameters or more than once: " + names}; + WriteLog("Scenery save: " + warning, logtype::generic); + result.message = result.files.empty() ? warning : result.message + ". " + warning; + } return result; }