From aca659754bbfdeff53bf147cf0cedba298afb224 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 25 Oct 2026 17:26:05 +0100 Subject: [PATCH] fix(editor): Ctrl+Z after a crossover laid along a curve leaves nothing behind, and such a crossover reaches from a transition onto the straight: the pieces the cut of the curve split off and then took out under a switch in the same step went into the history both as made and as taken out, so an undo retired them and brought them back, loose on top of the tracks put back (the curve seen chopped along its old path); now such a piece is neither, and push_track_snapshot drops a path that is both for every tool. The design of the grade line of the outer track, raised with the crossover, goes back with the undo as well (the step keeps the library of the designs), also when a crossover is moved or taken out for the route laid again. The frame a crossover along a curve is planned in goes on 250 m along the straights past the ends of the curve, and the release may fall on the straight of the other track: pressed near the end of the last transition and dragged towards the straight, its first switch ran past the end of the curve and the crossover was refused ("A switch of the connection runs off the end of its straight"); the first switch is drawn along that frame as it's dragged. The insert takes the style and the name of the track pressed on --- application/editormode.h | 8 +- application/editormodeconnectioncurve.cpp | 136 +++++++++++++++++++--- application/editormodeprofile.cpp | 18 +++ application/editormoderoute.cpp | 7 ++ application/editormodeswitchplace.cpp | 11 ++ application/editormodetrack.cpp | 14 +++ application/editormodetrackshared.h | 3 + 7 files changed, 182 insertions(+), 15 deletions(-) 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};