diff --git a/application/editormode.h b/application/editormode.h index 0878e9ec..b78582c4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1554,7 +1554,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void insert_curved_switch(int const Direction, int const Side); // a crossover between two tracks running side by side along a curve (editormodeconnectioncurve.cpp) struct curve_mapping; - static bool curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks); + // Beyond: the straights past the ends of the curve go with it up to that far, and a straight track that near the curve is taken + // as its part + static bool curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks, double const Beyond = 0.0); static void cut_curve(std::vector &Candidates, std::vector const &Frame, glm::dvec3 const &First, glm::dvec3 const &Second, std::vector> &States, std::vector &Created, std::vector &Removed); void insert_curved_connection(); struct curved_crossover @@ -2453,6 +2455,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool m_profiles_loaded{false}; void profile_library_load(); void profile_library_mark(std::vector Layers); + // the last step takes the kept grade lines back to the library given, as they were before it changed them + void profiles_to_history(std::vector Library); + // the kept grade lines back to the library given, the step changing them given up before it went to the history + void profiles_restore(std::vector Library); // the tracks whose grade line was fitted, by name, with the shape they had then, kept as "//$f" comment lines: a track // changed since, by hand or taken back, isn't fitted any more struct fitted_mark diff --git a/application/editormodeconnectioncurve.cpp b/application/editormodeconnectioncurve.cpp index d1900c06..0aae4951 100644 --- a/application/editormodeconnectioncurve.cpp +++ b/application/editormodeconnectioncurve.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editormodeshared.h" +#include "application/editormodetrackshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorSwitchStore.hpp" @@ -36,6 +37,37 @@ namespace // vertical curve: the ramps break the grade line without one double constexpr kRampShare{0.9}; double constexpr kOtherReach{10.0}; // m, from the release to the other track +double constexpr kStraightReach{track_mode::kCrossoverStraights}; + +// the first curved track a straight track leads to along its straight, either way, within the reach +TTrack *curve_ahead(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Reach) +{ + TTrack *found{nullptr}; + auto nearest{Reach}; + for (auto const throughend : {true, false}) + { + TTrack const *previous{&Track}; + TTrack *current{throughend ? Track.trNext : Track.trPrev}; + double distance{0.0}; + for (int count = 0; current != nullptr && count < 1000 && distance < nearest; ++count) + { + if (current->eType != tt_Normal || current->m_editorremoved || current->m_paths.empty()) + break; + if (false == editor_track::is_straight(*current, Tolerance)) + { + found = current; + nearest = distance; + break; + } + auto const &path{current->m_paths.front()}; + distance += plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]); + auto *next{current->trPrev == previous ? current->trNext : current->trPrev}; + previous = current; + current = next; + } + } + return found; +} } // namespace @@ -111,19 +143,55 @@ struct editor_mode::curve_mapping }; // the curve the track is a part of, as a frame of samples, and its tracks -bool editor_mode::curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks) +bool editor_mode::curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks, double const Beyond) { + auto *base{&Track}; + if (Beyond > 0.0 && Track.eType == tt_Normal && editor_track::is_straight(Track, Tolerance)) + base = curve_ahead(Track, Tolerance, Beyond); + if (base == nullptr) + return false; editor_track::curve curve; editor_track::chain chain; std::string error; - if (false == editor_track::find_curve(Track, Tolerance, Gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + if (false == editor_track::find_curve(*base, Tolerance, Gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) return false; for (auto const &part : editor_track::chain_parts(chain)) - if (std::find(part.tracks.begin(), part.tracks.end(), &Track) != part.tracks.end()) + if (std::find(part.tracks.begin(), part.tracks.end(), base) != part.tracks.end()) { chain = part; break; } + if (Beyond > 0.0) + { + // the straights past both ends, as far as the reach, taken along the curve: the one before it from its far end + auto const taken = [&](editor_track::straight_run const &Run) { + std::size_t count{0}; + for (double reached{0.0}; count < Run.tracks.size() && reached < Beyond; ++count) + { + auto const &path{Run.tracks[count]->m_paths.front()}; + reached += plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]); + } + return count; + }; + auto const before{editor_track::straight_beyond(chain, false, Tolerance)}; + auto const after{editor_track::straight_beyond(chain, true, Tolerance)}; + editor_track::chain extended; + for (auto i{taken(before)}; i-- > 0;) + { + extended.tracks.push_back(before.tracks[i]); + extended.forward.push_back(false == before.outward[i]); + } + extended.tracks.insert(extended.tracks.end(), chain.tracks.begin(), chain.tracks.end()); + extended.forward.insert(extended.forward.end(), chain.forward.begin(), chain.forward.end()); + for (std::size_t i = 0, count = taken(after); i < count; ++i) + { + extended.tracks.push_back(after.tracks[i]); + extended.forward.push_back(after.outward[i]); + } + chain = std::move(extended); + } + if (std::find(chain.tracks.begin(), chain.tracks.end(), &Track) == chain.tracks.end()) + return false; Frame = sample_chain(chain); Tracks = chain.tracks; return Frame.size() >= 2; @@ -166,7 +234,12 @@ void editor_mode::cut_curve(std::vector &Candidates, std::vector From + 0.01 && where < To - 0.01) { editor_track::retire(*candidate); - Removed.push_back(candidate); + // a piece split off in this step goes as if it had never been made: kept among the ones made and the ones taken out + // too, an undo would retire it and then bring it back + if (auto const made{std::find(Created.begin(), Created.end(), candidate)}; made != Created.end()) + Created.erase(made); + else + Removed.push_back(candidate); } } } @@ -181,12 +254,33 @@ void editor_mode::insert_curved_connection() }; if (tool.connection != static_cast(connection::kind::crossover)) return fail(STR("Along a curve only a crossover can be laid")); + // the curve pressed on with the straights past its ends: from a transition the crossover goes on onto the straight + auto pressedframe{tool.frame}; + auto pressedtracks{tool.frame_tracks}; + auto *pressed{tool.frame_tracks.front()}; + { + auto nearest{std::numeric_limits::max()}; + for (auto *track : tool.frame_tracks) + if (false == track->m_editorremoved) + if (auto const distance{plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, tool.point)), tool.point)}; distance < nearest) + { + nearest = distance; + pressed = track; + } + std::vector frame; + std::vector tracks; + if (curve_of(*pressed, m_straights.tolerance, m_route.design.norms.gauge, frame, tracks, kStraightReach)) + { + pressedframe = std::move(frame); + pressedtracks = std::move(tracks); + } + } // the other track: one beside this curve near the release, running along with it TTrack *other{nullptr}; auto best{kOtherReach}; for (auto *track : simulation::Paths.sequence()) { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || std::find(tool.frame_tracks.begin(), tool.frame_tracks.end(), track) != tool.frame_tracks.end()) + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || std::find(pressedtracks.begin(), pressedtracks.end(), track) != pressedtracks.end()) continue; auto const distance{plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, tool.mouse)), tool.mouse)}; if (distance < best) @@ -199,12 +293,12 @@ void editor_mode::insert_curved_connection() return fail(STR("Drag onto the other track")); std::vector otherframe; std::vector othertracks; - if (false == curve_of(*other, m_straights.tolerance, m_route.design.norms.gauge, otherframe, othertracks)) + if (false == curve_of(*other, m_straights.tolerance, m_route.design.norms.gauge, otherframe, othertracks, kStraightReach)) return fail(STR("The other track isn't a curve running along this one")); // the inner track is the frame: the plane of its cant carries the crossover, the outer track goes up onto it - auto innerframe{tool.frame}; - auto innertracks{tool.frame_tracks}; - auto station{tool.along}; + auto innerframe{pressedframe}; + auto innertracks{pressedtracks}; + auto station{nearest_station(innerframe, tool.point)}; auto const at{sample_at(innerframe, station)}; auto const across{sample_at(otherframe, nearest_station(otherframe, at.position))}; glm::dvec2 const left{-at.tangent.y, at.tangent.x}; @@ -271,7 +365,9 @@ void editor_mode::insert_curved_connection() std::vector> states; std::vector created; std::vector removed; - auto *style{tool.frame_tracks.front()}; + auto *style{pressed}; + // the kept grade lines before the outer track went up, the design of which goes up with it + std::optional> library; auto const undo_all = [&]() { for (auto *track : created) editor_track::retire(*track); @@ -285,6 +381,8 @@ void editor_mode::insert_curved_connection() editor_track::revive(*track); editor_track::commit({track}); } + if (library.has_value()) + profiles_restore(std::move(*library)); straight_refresh(); }; @@ -298,7 +396,7 @@ void editor_mode::insert_curved_connection() low = std::min(low, path.points[end].z); high = std::max(high, path.points[end].z); } - auto *outertrack{swapped ? tool.frame_tracks.front() : other}; + auto *outertrack{swapped ? pressed : other}; for (auto const &entry : plan.switches) WriteLog(format("Editor: curved crossover - planned switch on %d: %.3f %.3f -> %.3f %.3f; extent %.3f..%.3f", entry.line, entry.paths.front().points[segment_data::point::start].x, entry.paths.front().points[segment_data::point::start].z, entry.paths.front().points[segment_data::point::end].x, entry.paths.front().points[segment_data::point::end].z, low, high), logtype::generic); std::size_t raised{0}; @@ -360,6 +458,8 @@ void editor_mode::insert_curved_connection() // the design of the outer track before, kept or read from the track, to go up with it auto *first{route.spans.front().track}; auto *last{route.spans.back().track}; + profile_library_load(); + library = m_profiles; profile_take(route, first, last); auto const before{m_profile.line}; std::vector> raisestates; @@ -431,7 +531,7 @@ void editor_mode::insert_curved_connection() closest = track; } } - if (closest == nullptr || false == curve_of(*closest, m_straights.tolerance, m_route.design.norms.gauge, outerframe, outertracks)) + if (closest == nullptr || false == curve_of(*closest, m_straights.tolerance, m_route.design.norms.gauge, outerframe, outertracks, kStraightReach)) { undo_all(); return fail(STR("The outer track can't be followed after it went up")); @@ -439,7 +539,7 @@ void editor_mode::insert_curved_connection() } // the inner curve again: the outer track may be split where the ramps end if (false == swapped) - innertracks = tool.frame_tracks; + innertracks = pressedtracks; std::vector switches; for (auto const &entry : plan.switches) @@ -532,6 +632,9 @@ void editor_mode::insert_curved_connection() switch_store::put(entry, entry.layer); } push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + // the design of the outer track goes back down with it + if (library.has_value()) + profiles_to_history(std::move(*library)); straight_refresh(); first_step_done(first_step::turnout); m_node = switches.front(); @@ -723,13 +826,16 @@ bool editor_mode::slide_curved_connection(TTrack &Switch, double const Distance) std::vector removed; TTrack *first{nullptr}; TTrack *second{nullptr}; + // the design of the outer track comes down with the lift taken back, an undo puts both back + profile_library_load(); + auto library{m_profiles}; if (false == take_out_crossover(*crossover, states, created, removed, first, second)) return false; // the new places of the press and of the release, along the two curves, or the two straights std::vector firstframe, secondframe; std::vector firsttracks, secondtracks; glm::dvec3 press, release; - if (curve_of(*first, m_straights.tolerance, m_route.design.norms.gauge, firstframe, firsttracks) && curve_of(*second, m_straights.tolerance, m_route.design.norms.gauge, secondframe, secondtracks)) + if (curve_of(*first, m_straights.tolerance, m_route.design.norms.gauge, firstframe, firsttracks, kStraightReach) && curve_of(*second, m_straights.tolerance, m_route.design.norms.gauge, secondframe, secondtracks, kStraightReach)) { auto const towards{nearest_station(firstframe, firstend) >= nearest_station(firstframe, firststart) ? 1.0 : -1.0}; press = sample_at(firstframe, nearest_station(firstframe, firststart) + towards * Distance).position; @@ -747,6 +853,8 @@ bool editor_mode::slide_curved_connection(TTrack &Switch, double const Distance) switch_store::follow(); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); records_to_history(); + if (std::abs(crossover->record.lift) > 0.002) + profiles_to_history(std::move(library)); // laid again if (false == lay_crossover(shape, press, release, 3.0, names)) { diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index f0fa9942..1f1ee201 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -249,6 +249,24 @@ void editor_mode::profile_library_mark(std::vector Layers) } } +void editor_mode::profiles_to_history(std::vector Library) +{ + if (m_history.empty() || m_history.back().action != EditorSnapshot::Action::TrackEdit || m_history.back().profiles) + return; + m_history.back().profiles = true; + m_history.back().profile_library = std::move(Library); +} + +void editor_mode::profiles_restore(std::vector Library) +{ + std::vector layers; + for (auto const *library : {&std::as_const(m_profiles), &std::as_const(Library)}) + for (auto const &entry : *library) + layers.push_back(entry.layer); + m_profiles = std::move(Library); + profile_library_mark(std::move(layers)); +} + std::uint64_t editor_mode::fitted_shape(TTrack const &Track) { // the points of the paths to a centimetre: what's read back from the files is the same shape diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index f8ac7a8f..c2cd7e2b 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -667,16 +667,23 @@ void editor_mode::route_lay() std::vector> states; std::vector created; std::vector removed; + // the designs of the outer tracks come down with the lifts taken back, an undo puts them back + profile_library_load(); + auto library{m_profiles}; + bool lifted{false}; for (auto *switchtrack : switches) if (auto const crossover{crossover_of(*switchtrack)}) { TTrack *first{nullptr}; TTrack *second{nullptr}; + lifted = lifted || std::abs(crossover->record.lift) > 0.002; take_out_crossover(*crossover, states, created, removed, first, second); } switch_store::follow(); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); records_to_history(); + if (lifted) + profiles_to_history(std::move(library)); show_track_tab(tab); // the line between the same points again: taking the lift back may have cut its paths auto const end_at = [&](glm::dvec3 const &Point) -> TTrack * { diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index a108f90c..865ed645 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -110,6 +110,17 @@ bool editor_mode::start_switch_placement() } tool.frame = sample_chain(chain); tool.frame_tracks = chain.tracks; + // a crossover along the curve goes on onto the straights past its ends, its first switch drawn so as it's dragged + if (tool.connection >= 0) + { + std::vector frame; + std::vector tracks; + if (curve_of(*track, m_straights.tolerance, m_route.design.norms.gauge, frame, tracks, kCrossoverStraights)) + { + tool.frame = std::move(frame); + tool.frame_tracks = std::move(tracks); + } + } if (tool.frame.size() < 2) return false; tool.curved = true; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 4d7c8c6e..ac5e1646 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -280,6 +280,20 @@ void editor_mode::trim_history() void editor_mode::push_track_snapshot(std::vector> States, std::vector Created, std::vector Removed) { + // a path made and taken out again within the step was never there for an undo or a redo: an undo retiring the paths made and + // then reviving the ones taken out would bring it back + for (auto made{Created.begin()}; made != Created.end();) + { + auto const gone{std::find(Removed.begin(), Removed.end(), *made)}; + if (gone == Removed.end()) + { + ++made; + continue; + } + Removed.erase(gone); + States.erase(std::remove_if(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == *made; }), States.end()); + made = Created.erase(made); + } if (States.empty() && Created.empty() && Removed.empty()) return; diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index 688c9fac..00e5399b 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -208,6 +208,9 @@ double const kLaySnapRadius{5.0}; float const kSnapPixels{16.0f}; double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch double const kSlipReach{12.0}; // m; a slip goes to the crossing nearest the cursor, if it's this close +// m, of the straights past the ends of a curve a crossover laid along it reaches onto: from a transition it goes on onto the +// straight after it +double const kCrossoverStraights{250.0}; double const kSkewSeat{2.0}; // m; the first point laid with Alt this close to a straight track goes onto its axis double const kSkewReach{30.0}; // m; Alt while laying turns the straight to a straight track this close to its start, or one it crosses double const kSkewRatios[]{4.8, 6.6, 7.5, 9.0, 12.0, 14.0, 18.5, 26.5};