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"