From 667e433af929a8b22102d651f03d9434b25f6412 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 27 Oct 2026 19:40:56 +0100 Subject: [PATCH] feat(editor): a slip is selected, slid and deleted whole: any track of it picked shows all of it selected, editor_mode::slip_of finding its switches by its record, the tracks between them and its four ends. Dragged along, the slip moves along its first track with the straights out to the joints of its template and its crossing track, as far as that one goes on into tracks with their ends free, the straights of its first track before it and past it lengthened and shortened, a metre of them kept; one bent into a curve isn't slid, nor one whose crossing track is joined on beyond it. Deleted, it leaves its two tracks crossing from end to end in its place, kept in the scenery as a crossing Skrz, as a switch leaves its main track; Shift+Del takes all of it, Ctrl+Z brings it back. Selftest: a Rkp laid with its crossing track slid 5 m on and 3 m back, the joints of its template with it, walked through; deleted into a crossing Skrz 1:9 walked through, back with Ctrl+Z; a Rkpd at a crossing of straights with free ends slid 2 m, deleted into a crossing --- application/editormode.h | 29 +++++ application/editormoderecords.cpp | 128 ++++++++++++++++++++ application/editormodeswitchslide.cpp | 162 +++++++++++++++++++++++++- application/editormodetrack.cpp | 53 +++++++++ lang/editor_pl.po | 12 ++ 5 files changed, 383 insertions(+), 1 deletion(-) diff --git a/application/editormode.h b/application/editormode.h index 335c44c4..b46c1978 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1100,6 +1100,20 @@ class editor_mode : public application_mode, private editor_track::observer, pri TTrack *second{nullptr}; }; std::optional double_of(TTrack const &Track) const; + // a slip of the scenery the track is a part of: its switches, all its tracks, the switches among them, and its ends where it joins + // the tracks around, A and B of its first track through, C and D of the other one, with the way into the slip at each + struct slip_set + { + switch_store::record record; + std::vector switches; + std::vector parts; + glm::dvec2 crossing{0.0}; + std::array ends{}; + std::array into{}; + }; + std::optional slip_of(TTrack const &Track) const; + // the two tracks of the slip through it from end to end, as they cross where it's taken out + static std::array slip_throughs(slip_set const &Slip); // the paths of the set in the order editor_track::place_switch gives them static std::vector double_paths(double_switch const &Set); // the switch of the template, or the two switches of a double one with the plain track between them, committed and recorded @@ -1373,6 +1387,21 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status); bool slide_switch(TTrack &Track, double const Distance); bool switch_slides(TTrack const &Track) const; + // a slip slid along its first track: the slip and the tracks its crossing track goes on into with their ends free move whole, the + // straights of its first track before it and past it give and take the length. Low and High: how far back and on it may go + struct slip_slide + { + glm::dvec3 shift{0.0}; // along its first track, a metre of it + std::vector movers; + // where it joins the tracks of its first track before it and past it: at its ends A and B, or at the joints of its template + // with the straights out to them going along; those tracks, the end of each there + std::array joints{}; + std::array, 2> ends{}; + double low{0.0}; + double high{0.0}; + }; + bool slip_room(slip_set const &Slip, slip_slide &Slide, std::string &Reason) const; + bool slide_slip(slip_set const &Slip, double const Distance); void switch_slide_start(TTrack &Track); void switch_slide_update(); void switch_slide_finish(); diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp index c57d2f43..ee480e92 100644 --- a/application/editormoderecords.cpp +++ b/application/editormoderecords.cpp @@ -340,6 +340,134 @@ std::optional editor_mode::double_of(TTrack const &T return std::nullopt; } +std::optional editor_mode::slip_of(TTrack const &Track) const +{ + if (Track.m_editorremoved) + return std::nullopt; + auto const record_of = [](TTrack const &Each) -> std::optional { + if (Each.eType != tt_Switch || Each.m_editorremoved) + return std::nullopt; + auto found{switch_store::named(Each)}; + return found.has_value() && found->kind == "slip" ? found : std::nullopt; + }; + // the record of the switch, or of a switch of the slip the track between them is joined to + auto known{record_of(Track)}; + if (false == known.has_value() && Track.eType == tt_Normal && Track.m_paths.size() == 1) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + for (auto const &[other, point] : editor_track::connected_points(Track, Track.m_paths.front().points[index])) + if (false == known.has_value()) + known = record_of(*other); + if (false == known.has_value()) + return std::nullopt; + slip_set slip; + slip.record = *known; + for (auto const &name : known->parts) + { + auto *part{simulation::Paths.find(name)}; + if (part == nullptr || part->m_editorremoved || part->eType != tt_Switch || part->m_paths.size() < 2) + return std::nullopt; + slip.switches.push_back(part); + } + if (slip.switches.size() < 2) + return std::nullopt; + // where the straights of its first and its last switch meet: the joints of its tracks near it are inside the slip, its ends far off + auto const heading = [](segment_data const &Path) { + auto const &control{Path.points[segment_data::point::control1]}; + auto const direction{plan_of(control != glm::dvec3{} ? control : Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; + }; + auto const &first{slip.switches.front()->m_paths.front()}; + auto const &last{slip.switches.back()->m_paths.front()}; + auto const a{plan_of(first.points[segment_data::point::start])}; + auto const d{plan_of(last.points[segment_data::point::start])}; + auto const da{heading(first)}; + auto const dd{heading(last)}; + auto const determinant{geometry::cross(da, dd)}; + if (std::abs(determinant) < 1e-9) + return std::nullopt; + auto const crossing{a + da * (geometry::cross(d - a, dd) / determinant)}; + auto const inside{0.5 * glm::distance(a, crossing)}; + slip.parts = slip.switches; + for (std::size_t i = 0; i < slip.parts.size(); ++i) + for (auto const &path : slip.parts[i]->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const &position{path.points[index]}; + if (glm::distance(plan_of(position), crossing) > inside) + continue; + for (auto const &[other, point] : editor_track::connected_points(*slip.parts[i], position)) + if (false == other->m_editorremoved && std::find(slip.parts.begin(), slip.parts.end(), other) == slip.parts.end()) + slip.parts.push_back(other); + } + // its ends, each with the way into the slip there + std::vector> ends; + for (auto const *part : slip.parts) + for (auto const &path : part->m_paths) + for (auto const atend : {false, true}) + { + auto const &position{path.points[atend ? segment_data::point::end : segment_data::point::start]}; + if (glm::distance(plan_of(position), crossing) <= inside || + std::any_of(ends.begin(), ends.end(), [&](auto const &End) { return glm::distance(End.first, position) < geometry::same_point; })) + continue; + auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const into{plan_of(control != glm::dvec3{} ? control : path.points[atend ? segment_data::point::start : segment_data::point::end] - position)}; + if (glm::length(into) < 1e-9) + return std::nullopt; + ends.emplace_back(position, glm::normalize(into)); + } + if (ends.size() != 4) + return std::nullopt; + // A where its first switch starts, B on along its first track, D where its last switch starts, C the other one + auto const at = [&](glm::dvec2 const &Point) { + return static_cast(std::min_element(ends.begin(), ends.end(), [&](auto const &One, auto const &Other) { return glm::distance(plan_of(One.first), Point) < glm::distance(plan_of(Other.first), Point); }) - + ends.begin()); + }; + auto const ia{at(a)}; + auto const id{at(d)}; + if (ia == id) + return std::nullopt; + std::vector others; + for (std::size_t i = 0; i < 4; ++i) + if (i != ia && i != id) + others.push_back(i); + if (others.size() != 2) + return std::nullopt; + // of the other two the one farther on along the first track + auto const ahead = [&](std::size_t const Index) { return glm::dot(plan_of(ends[Index].first) - crossing, da); }; + auto const ib{ahead(others[0]) >= ahead(others[1]) ? others[0] : others[1]}; + auto const ic{ib == others[0] ? others[1] : others[0]}; + std::size_t const order[]{ia, ib, ic, id}; + for (std::size_t i = 0; i < 4; ++i) + { + slip.ends[i] = ends[order[i]].first; + slip.into[i] = ends[order[i]].second; + } + slip.crossing = (plan_of(slip.ends[0]) + plan_of(slip.ends[1]) + plan_of(slip.ends[2]) + plan_of(slip.ends[3])) * 0.25; + return slip; +} + +std::array editor_mode::slip_throughs(slip_set const &Slip) +{ + // from end to end, a straight where the slip is one, else heading out of the ends as the slip does + auto const through = [&](std::size_t const From, std::size_t const To) { + segment_data path; + path.points[segment_data::point::start] = Slip.ends[From]; + path.points[segment_data::point::end] = Slip.ends[To]; + auto const chord{plan_of(Slip.ends[To] - Slip.ends[From])}; + auto const length{glm::length(chord)}; + if (length > 1e-6 && std::abs(geometry::cross(Slip.into[From], chord / length)) + std::abs(geometry::cross(Slip.into[To], chord / length)) > 1e-5) + { + auto const handle{length / 3.0}; + auto const rise{Slip.ends[To].y - Slip.ends[From].y}; + path.points[segment_data::point::control1] = {Slip.into[From].x * handle, rise / 3.0, Slip.into[From].y * handle}; + path.points[segment_data::point::control2] = {Slip.into[To].x * handle, -rise / 3.0, Slip.into[To].y * handle}; + path.radius = static_cast(Slip.record.main_radius); + } + return path; + }; + return {through(0, 1), through(2, 3)}; +} + std::vector editor_mode::double_paths(double_switch const &Set) { auto link{Set.link->m_paths.front()}; diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp index 756af589..3000006b 100644 --- a/application/editormodeswitchslide.cpp +++ b/application/editormodeswitchslide.cpp @@ -702,10 +702,129 @@ bool editor_mode::move_double_paths(double_switch const &Set, std::vectorm_paths.front())) + { + Reason = STR_C("A slip bent into a curve isn't slid along it: delete it and lay it again"); + return false; + } + if (false == tracks_editable(Slip.parts, Reason)) + return false; + auto const chord{Slip.ends[1] - Slip.ends[0]}; + auto const run{glm::length(plan_of(chord))}; + if (run < 1e-6) + return false; + Slide.shift = chord / run; + Slide.movers = Slip.parts; + auto const outside = [&](TTrack const *Track) { return std::find(Slip.parts.begin(), Slip.parts.end(), Track) == Slip.parts.end(); }; + // its crossing track goes along where it goes on into tracks ending free, as when it was laid on one straight + for (std::size_t end = 2; end < 4; ++end) + { + for (auto const *part : Slip.parts) + for (auto const &[other, point] : editor_track::connected_points(*part, Slip.ends[end])) + { + if (false == outside(other) || std::find(Slide.movers.begin(), Slide.movers.end(), other) != Slide.movers.end()) + continue; + auto const free{other->eType == tt_Normal && other->m_paths.size() == 1 && + editor_track::connected_points(*other, editor_track::point_position(*other, {point.path, point.kind == editor_track::point_kind::start ? editor_track::point_kind::end : editor_track::point_kind::start})).empty()}; + if (false == free) + { + Reason = STR_C("The crossing track of the slip is joined on beyond it: the slip stays where the two tracks cross"); + return false; + } + Slide.movers.push_back(other); + } + } + // the straights of its first track out to the joints of its template, as long as the ones of its crossing track going along, go + // along too, the joints with them + auto const extension{Slide.movers.size() > Slip.parts.size() ? geometry::chord_length(Slide.movers.back()->m_paths.front()) : 0.0}; + auto const straight_along = [&](TTrack const &Track) { + return Track.eType == tt_Normal && Track.m_paths.size() == 1 && false == has_controls(Track.m_paths.front()) && + std::abs(geometry::cross(glm::normalize(plan_of(Track.m_paths.front().points[segment_data::point::end] - Track.m_paths.front().points[segment_data::point::start])), plan_of(Slide.shift))) < 1e-3; + }; + std::array joints{Slip.ends[0], Slip.ends[1]}; + for (std::size_t end = 0; end < 2 && extension > 0.0; ++end) + for (auto const *part : Slip.parts) + for (auto const &[other, point] : editor_track::connected_points(*part, Slip.ends[end])) + if (outside(other) && straight_along(*other) && std::abs(geometry::chord_length(other->m_paths.front()) - extension) < 0.01 && + std::find(Slide.movers.begin(), Slide.movers.end(), other) == Slide.movers.end()) + { + Slide.movers.push_back(other); + joints[end] = editor_track::point_position(*other, {point.path, point.kind == editor_track::point_kind::start ? editor_track::point_kind::end : editor_track::point_kind::start}); + } + Slide.joints = joints; + // the straights of its first track before it and past it give and take the length, a metre of them kept at least + double room[2]{1e4, 1e4}; + bool straight[2]{true, true}; + for (std::size_t end = 0; end < 2; ++end) + for (auto *track : Slide.movers) + for (auto const &[other, point] : editor_track::connected_points(*track, joints[end])) + { + if (std::find(Slide.movers.begin(), Slide.movers.end(), other) != Slide.movers.end() || Slide.ends[end].first != nullptr) + continue; + Slide.ends[end] = {other, point}; + straight[end] = other->eType == tt_Normal && other->m_paths.size() == 1 && false == has_controls(other->m_paths.front()) && + std::abs(geometry::cross(glm::normalize(plan_of(other->m_paths.front().points[segment_data::point::end] - other->m_paths.front().points[segment_data::point::start])), plan_of(Slide.shift))) < 1e-3; + room[end] = straight[end] ? geometry::chord_length(other->m_paths.front()) - 1.0 : 0.0; + } + // back, it shortens the track before it and lengthens the one past it; on, the other way + Slide.low = straight[1] ? -std::max(0.0, room[0]) : 0.0; + Slide.high = straight[0] ? std::max(0.0, room[1]) : 0.0; + return true; +} + +bool editor_mode::slide_slip(slip_set const &Slip, double const Distance) +{ + slip_slide slide; + std::string reason; + if (false == slip_room(Slip, slide, reason)) + { + m_switch.status = refused(reason); + ui()->set_status(m_switch.status, true); + return false; + } + auto const distance{std::clamp(Distance, slide.low, slide.high)}; + if (std::abs(distance) < 0.005) + return false; + auto const shift{slide.shift * distance}; + std::vector> states; + std::vector changed; + for (auto *mover : slide.movers) + { + states.emplace_back(mover, editor_track::capture(*mover)); + for (auto &path : mover->m_paths) + path.translate(shift); + changed.push_back(mover); + } + for (std::size_t end = 0; end < 2; ++end) + if (auto const &[other, point]{slide.ends[end]}; other != nullptr) + { + states.emplace_back(other, editor_track::capture(*other)); + editor_track::move_point(*other, point, slide.joints[end] + shift); + changed.push_back(other); + } + editor_track::commit(changed); + push_track_snapshot(std::move(states), {}, {}); + switch_store::follow(); + records_to_history(); + straight_refresh(); + m_switch.status = format(STR_C("Slip %s moved %.2f m along its track, its crossing track with it"), Slip.record.name.c_str(), distance); + ui()->set_status(m_switch.status, false); + return true; +} + bool editor_mode::slide_switch(TTrack &Track, double const Distance) { if (std::abs(Distance) < 0.005) return false; + // a slip slides whole + if (auto const slip{slip_of(Track)}) + return slide_slip(*slip, Distance); // a switch of a crossover along a curve takes the crossover with it if (crossover_of(Track).has_value()) return slide_curved_connection(Track, Distance); @@ -744,6 +863,12 @@ bool editor_mode::slide_switch(TTrack &Track, double const Distance) bool editor_mode::switch_slides(TTrack const &Track) const { + if (auto const slip{slip_of(Track)}) + { + slip_slide room; + std::string reason; + return slip_room(*slip, room, reason); + } if (Track.eType == tt_Normal) { auto const set{double_of(Track)}; @@ -757,6 +882,27 @@ void editor_mode::switch_slide_start(TTrack &Track) auto &slide{m_switch.slide}; slide = {}; std::string reason; + // a slip along its first track, from its first switch + if (auto const slip{slip_of(Track)}) + { + slip_slide room; + auto const axis{editor_track::main_axis(*slip->switches.front())}; + if (false == slip_room(*slip, room, reason) || false == axis.has_value()) + { + if (false == reason.empty()) + ui()->set_status(reason, true); + return; + } + auto const &mouse{ImGui::GetIO().MousePos}; + slide.active = true; + slide.track = slip->switches.front(); + slide.pressed = {mouse.x, mouse.y}; + slide.grip = axis->along(cursor_level(axis->origin.y)); + slide.length = 0.0; + slide.low = room.low; + slide.high = room.high; + return; + } auto const set{double_of(Track)}; auto &first{set.has_value() ? *set->first : Track}; auto const axis{editor_track::main_axis(first)}; @@ -856,7 +1002,21 @@ void editor_mode::draw_switch_slide() const auto paths{editor_track::slid(*slide.track, slide.distance)}; if (paths.empty()) return; - if (auto const set{double_of(*slide.track)}) + // a slip with all it takes along + if (auto const slip{slip_of(*slide.track)}) + { + slip_slide room; + std::string reason; + slip_room(*slip, room, reason); + paths.clear(); + for (auto const *mover : room.movers) + for (auto path : mover->m_paths) + { + path.translate(room.shift * slide.distance); + paths.push_back(path); + } + } + else if (auto const set{double_of(*slide.track)}) { auto const shift{paths.front().points[segment_data::point::start] - slide.track->m_paths.front().points[segment_data::point::start]}; paths = double_paths(*set); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 14753778..e9ba721f 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1650,6 +1650,53 @@ void editor_mode::delete_selected_track() straight_refresh(); return; } + // a slip goes whole, its two tracks left crossing in its place unless Shift asks for all of it + if (auto const slip{slip_of(*track)}) + { + for (auto const *part : slip->parts) + if (false == part->Dynamics.empty()) + { + ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true); + return; + } + switch_store::forget(slip->record.name); + auto &style{*slip->switches.front()}; + auto const velocity{speed_check::path_velocity(style, 0)}; + for (auto *part : slip->parts) + editor_track::retire(*part); + std::vector created; + if (false == Global.shiftState) + { + for (auto const &path : slip_throughs(*slip)) + if (auto *through{editor_track::create_path(style, path)}) + { + editor_track::velocity(*through, velocity); + created.push_back(through); + } + editor_track::commit(created); + // kept in the scenery as a crossing + auto const recorded = [](std::string const &Name) { + auto const &records{switch_store::records()}; + return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; }); + }; + std::string base; + for (int i = 1; base.empty() || recorded(base); ++i) + base = "editor_skrz" + std::to_string(i); + record_slip(created, "crossing", "Skrz", 0.0, slip->record.ratio > 0.0 ? std::atan(1.0 / slip->record.ratio) : 0.0, base); + ui()->set_status(STR_C("The slip is replaced by its two tracks crossing, Shift+Del removes all of it"), false); + } + if (auto const note{designs_trimmed(slip->parts)}; false == note.empty()) + ui()->set_status(note, false); + push_track_snapshot({}, created, slip->parts); + drives_deleted(slip->switches); + if (std::any_of(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), [&](TTrack const *Each) { return std::find(slip->parts.begin(), slip->parts.end(), Each) != slip->parts.end(); })) + m_route = {}; + m_node = nullptr; + ui()->set_node(nullptr); + m_track_point = {}; + straight_refresh(); + return; + } // retired first, so the infrastructure it carried and moves elsewhere is in the same undo step if (auto const known{switch_store::of(*track)}) switch_store::forget(known->name); @@ -2339,6 +2386,12 @@ void editor_mode::draw_build_overlay() const } } }; + // a slip is selected whole: all its tracks with the one picked + if (auto const *selected{selected_track()}; selected != nullptr && false == m_switch.slide.active) + if (auto const slip{slip_of(*selected)}) + for (auto const *part : slip->parts) + for (auto const &path : part->m_paths) + drawpath(path, overlay_color::highlight); if (false == GfxRenderer->Mouse_Hit() && false == ImGui::GetIO().WantCaptureMouse) { auto const centre{m_lay.active ? m_lay.mouse : cursor_ground()}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 81d50a8c..41365363 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -12432,3 +12432,15 @@ msgstr "wygięty w łuk, tylko w torze bocznym (11.4 pkt 4b)" msgid "Rkp or Rkpd at a crossing of two straights at its skos, or bent into a curve of a side track" msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowaniu dwóch prostych pod jego skosem albo wygięty w łuk toru bocznego" + +msgid "The slip is replaced by its two tracks crossing, Shift+Del removes all of it" +msgstr "Rozjazd krzyżowy zastąpiony skrzyżowaniem jego dwóch torów, Shift+Del usuwa wszystko" + +msgid "A slip bent into a curve isn't slid along it: delete it and lay it again" +msgstr "Rozjazdu krzyżowego wygiętego w łuk nie przesuwa się wzdłuż: usuń go i połóż od nowa" + +msgid "The crossing track of the slip is joined on beyond it: the slip stays where the two tracks cross" +msgstr "Tor przecinający rozjazdu krzyżowego jest dalej połączony: rozjazd zostaje tam, gdzie tory się przecinają" + +msgid "Slip %s moved %.2f m along its track, its crossing track with it" +msgstr "Rozjazd krzyżowy %s przesunięty o %.2f m wzdłuż toru, razem z torem przecinającym"