/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" #include "editor/editorGeometry.hpp" #include "world/Track.h" #include "utilities/translation.h" #include "imgui/imgui.h" #include #include #include #include namespace { using geometry::bezier; using geometry::has_controls; using geometry::cross; using geometry::grade_of; using geometry::plan_distance; using geometry::plan_of; struct switch_end_move { glm::dvec3 from{0.0}; glm::dvec3 to{0.0}; double shift{0.0}; }; std::optional> switch_end_moves(TTrack const &Track, segment_data const &Main, bool const Online, std::pair const &Room, double const Offset, std::string &Status) { auto const axis{editor_track::main_axis(Track)}; std::vector ends; for (auto const index : {segment_data::point::start, segment_data::point::end}) { auto const from{Track.m_paths.front().points[index]}; auto const to{Main.points[index]}; if (false == Online || glm::distance(from, to) < 0.005 || axis->lateral(to) > Offset) continue; auto const target{axis->along(to)}; if (target < Room.first - 0.01 || target > Room.second + 0.01) { Status = STR_C("The switch would run into another switch or past the end of the straight"); return std::nullopt; } ends.push_back({from, to, index == segment_data::point::start ? axis->along(from) - target : target - axis->along(from)}); } std::stable_sort(ends.begin(), ends.end(), [](switch_end_move const &A, switch_end_move const &B) { return A.shift > B.shift; }); return ends; } std::string switch_rejoin(TTrack &Track, std::vector const &Joints, std::vector const &Handled, editor_track::edit &Edit) { std::vector following; std::string loose; for (std::size_t i = 0; i < Joints.size(); ++i) { auto const &joint{Joints[i]}; if (Handled[i] || joint.other->m_editorremoved) continue; auto const &theirs{joint.other->m_paths[joint.theirs.path]}; auto const shift{glm::distance(editor_track::point_position(Track, joint.own), editor_track::point_position(*joint.other, joint.theirs))}; if (shift > 0.3 * bezier{theirs}.plan_length()) { loose += (loose.empty() ? "" : ", ") + name_of(*joint.other); continue; } following.push_back(joint); Edit.capture(*joint.other); } editor_track::follow(Track, following); return loose; } double constexpr kBranchStraight{500.0}; // how far the straight past the curve next to a switch is followed // direction out of the path at the end glm::dvec3 end_outward(segment_data const &Path, bool const Atend) { bezier const curve{Path}; auto direction{Atend ? curve.first(1.0) : -curve.first(0.0)}; if (glm::length(plan_of(direction)) < 1e-9) direction = Atend ? curve.p3 - curve.p0 : curve.p0 - curve.p3; return direction; } // straight from the start of the line to its first curve double opening(alignment::result const &Result) { return Result.elements.empty() || Result.elements.front().kind != alignment::element_kind::straight ? 0.0 : Result.elements.front().length; } // straight from the last curve of the line to its end double closing(alignment::result const &Result) { return Result.elements.empty() || Result.elements.back().kind != alignment::element_kind::straight ? 0.0 : Result.elements.back().length; } alignment::vertex scaled(alignment::vertex Vertex, double const Factor) { Vertex.radius *= Factor; Vertex.transition_in *= Factor; Vertex.transition_out *= Factor; for (auto &arc : Vertex.arcs) { arc.radius *= Factor; arc.transition *= Factor; } return Vertex; } // where the path of the index leaves the chain glm::dvec3 exit_of(editor_track::chain const &Chain, std::size_t const Index) { auto const &path{Chain.tracks[Index]->m_paths.front()}; return path.points[Chain.forward[Index] ? segment_data::point::end : segment_data::point::start]; } // raises the pieces by Rise at their start, less and less up to nothing at their end void lift(std::vector &Pieces, double const Rise) { std::vector lengths; double total{0.0}; for (auto const &piece : Pieces) { lengths.push_back(bezier{piece}.plan_length()); total += lengths.back(); } if (total <= 1e-6 || std::abs(Rise) <= 1e-6) return; auto const slope{Rise / total}; double along{0.0}; for (std::size_t i = 0; i < Pieces.size(); ++i) { auto &points{Pieces[i].points}; auto const controls{has_controls(Pieces[i])}; points[segment_data::point::start].y += Rise - slope * along; points[segment_data::point::end].y += Rise - slope * (along + lengths[i]); // the controls are kept from their ends if (controls) { points[segment_data::point::control1].y -= slope * glm::length(plan_of(points[segment_data::point::control1])); points[segment_data::point::control2].y += slope * glm::length(plan_of(points[segment_data::point::control2])); } along += lengths[i]; } } } // namespace bool editor_mode::switch_room(TTrack &Track, double &Low, double &High, std::string &Reason) { Low = 0.0; High = 0.0; if (Track.eType != tt_Switch || Track.m_paths.size() < 2 || has_controls(Track.m_paths.front())) return false; auto const axis{editor_track::main_axis(Track)}; if (false == axis.has_value()) return false; auto const line{editor_track::find_straight(Track, m_straights.tolerance)}; if (false == tracks_editable(line.tracks, Reason)) return false; std::vector> spans; for (std::size_t i = 0; i < line.tracks.size(); ++i) { if (line.tracks[i] == &Track) continue; auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; auto const a{axis->along(path.points[segment_data::point::start])}; auto const b{axis->along(path.points[segment_data::point::end])}; spans.emplace_back(std::min(a, b), std::max(a, b), line.tracks[i]->eType == tt_Normal); } std::sort(spans.begin(), spans.end()); double low{0.0}; double high{axis->length}; for (auto const &[from, to, plain] : spans) if (from > axis->length - 0.01) { if (false == plain || from > high + 0.01) break; high = std::max(high, to); } for (auto span{spans.rbegin()}; span != spans.rend(); ++span) { auto const &[from, to, plain] = *span; if (to < 0.01) { if (false == plain || to < low - 0.01) break; low = std::min(low, from); } } Low = low; High = high; return true; } bool editor_mode::switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status) { if (Shift > 0.0) { auto const a{Line.along(From)}; auto const b{Line.along(To)}; if (cut_straight(Line, std::min(a, b), std::max(a, b), Edit.states, Edit.created, Edit.removed, nullptr)) return true; Status = STR_C("The switch would overlap another switch, move it onto plain track"); return false; } TTrack const *style{nullptr}; bool filler{false}; for (auto const &[other, point] : editor_track::connected_points(Track, From)) { if (other->m_editorremoved) continue; if (other->eType == tt_Normal && std::find(Line.tracks.begin(), Line.tracks.end(), other) != Line.tracks.end()) { Edit.capture(*other); editor_track::move_point(*other, point, To); continue; } filler = true; if (style == nullptr && other->eType == tt_Normal) style = other; } if (filler) { segment_data path; path.points[segment_data::point::start] = To; path.points[segment_data::point::end] = From; auto *piece{editor_track::create_path(style != nullptr ? *style : Track, path)}; Edit.created.push_back(piece); Edit.changed.push_back(piece); } return true; } std::optional editor_mode::take_switch_branch(TTrack const &Track, editor_track::joint const &Joint, std::size_t const Index) const { auto *first{Joint.other}; if (first == nullptr || first->eType != tt_Normal || first->m_paths.empty()) return std::nullopt; auto const category{first->iCategoryFlag & 15}; auto const regular = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && false == Other->m_editorremoved && editor_track::is_supported(*Other) && (Other->iCategoryFlag & 15) == category; }; // from the switch on: a straight up to the curve maybe, the curve, the straight past it std::vector run; auto *current{first}; auto atstart{Joint.theirs.kind == editor_track::point_kind::start}; int phase{0}; std::size_t straight{0}; double past{0.0}; while (regular(current) && std::find(run.begin(), run.end(), current) == run.end() && past < kBranchStraight) { auto const plain{editor_track::is_straight(*current, m_straights.tolerance)}; if (false == plain && phase == 2) break; if (false == plain) phase = 1; else if (phase == 1) { phase = 2; straight = run.size(); } if (phase == 2) past += current->Length(); run.push_back(current); auto *next{atstart ? current->trNext : current->trPrev}; if (next == nullptr || (next->trPrev != current && next->trNext != current)) break; atstart = next->trPrev == current; current = next; } if (phase != 2) return std::nullopt; switch_branch branch; branch.joint = Index; branch.straight = straight; std::string error; auto &chain{branch.chain}; if (false == editor_track::find_chain(run.front(), run.back(), chain, error) || chain.tracks != run || plan_distance(chain.start, editor_track::point_position(Track, Joint.own)) > geometry::same_point) return std::nullopt; auto &design{branch.design}; design.norms = m_route.design.norms; design.shape = m_route.design.shape; design.transition_pieces = m_route.design.transition_pieces; design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; if (auto const hit{alignment_store::on_track(*run[straight - 1])}) { auto const &stored{alignment_store::records()[hit->record].design}; design.norms = stored.norms; design.shape = stored.shape; design.transition_pieces = stored.transition_pieces; design.arc_piece_angle = stored.arc_piece_angle; design.speed = stored.speed; } design.norms.checked = false; auto const found{editor_track::recognize_line(chain, m_straights.tolerance, design.norms.gauge, design.shape)}; if (found.size() != 1 || found.front().kink) return std::nullopt; design.start = chain.start; design.end = chain.end; design.start_direction = glm::normalize(plan_of(chain.start_direction)); design.end_direction = glm::normalize(plan_of(chain.end_direction)); design.start_grade = grade_of(chain.start_direction); design.end_grade = grade_of(chain.end_direction); auto vertex{route_vertex_of(found.front().shape)}; vertex.position = found.front().position; design.vertices = {vertex}; branch.result = alignment::compute(design); if (false == branch.result.valid) return std::nullopt; return branch; } bool editor_mode::plan_switch_branch(segment_data const &Path, editor_track::joint const &Joint, switch_branch &Branch) const { auto const atend{Joint.own.kind == editor_track::point_kind::end}; auto const start{Path.points[atend ? segment_data::point::end : segment_data::point::start]}; auto const outward{end_outward(Path, atend)}; auto laid{Branch.design}; laid.start = start; laid.start_direction = glm::normalize(plan_of(outward)); laid.start_grade = grade_of(outward); auto const original{Branch.design.vertices.front()}; auto const make = [&](double const Factor) { laid.vertices = {scaled(original, Factor)}; return alignment::compute(laid); }; // compute() makes the curve tighter when it doesn't fit between the ends auto const fits = [&](alignment::result const &Result) { return Result.valid && false == Result.curves.empty() && Result.curves.front().radius >= laid.vertices.front().radius * (1.0 - 1e-6); }; double factor{1.0}; auto result{make(factor)}; if (false == result.valid || result.curves.empty()) return false; if (false == fits(result)) { // the design keeps the curve the way it was made to fit auto &vertex{laid.vertices.front()}; auto const &report{result.curves.front()}; auto const reduced{report.radius / vertex.radius}; if (vertex.compound) vertex = scaled(vertex, reduced); else { vertex.radius = report.radius; vertex.transition_in = report.transition_in; vertex.transition_out = report.transition_out; } factor = reduced; result = alignment::compute(laid); if (false == result.valid) return false; } else if (opening(Branch.result) < 0.1 && opening(result) >= 0.1) { // the curve which adjoined the switch keeps to it: the largest one which fits double low{1.0}; double high{1.5}; while (fits(make(high)) && high < 5.0) high *= 1.5; if (fits(make(high))) return false; for (int i = 0; i < 40; ++i) { auto const middle{0.5 * (low + high)}; (fits(make(middle)) ? low : high) = middle; } factor = low; result = make(factor); if (false == result.valid) return false; } // the paths are laid up to the first joint on the straight past the old and the new curve auto const &chain{Branch.chain}; auto const end{plan_of(chain.end)}; auto const behind = [&](glm::dvec3 const &Point) { return glm::dot(end - plan_of(Point), laid.end_direction); }; auto const curveend{closing(result)}; auto last{chain.tracks.size()}; for (auto k{Branch.straight - 1}; k < chain.tracks.size(); ++k) if (behind(exit_of(chain, k)) <= curveend - 0.5) { last = k; break; } if (last == chain.tracks.size()) return false; auto part{laid}; auto const joint{exit_of(chain, last)}; part.end = joint; part.end_grade = grade_of(end_outward(chain.tracks[last]->m_paths.front(), chain.forward[last])); auto const partresult{alignment::compute(part)}; if (false == partresult.valid) return false; Branch.part = {}; Branch.part.tracks.assign(chain.tracks.begin(), chain.tracks.begin() + last + 1); Branch.part.forward.assign(chain.forward.begin(), chain.forward.begin() + last + 1); Branch.part.start = chain.start; Branch.part.end = joint; Branch.pieces = alignment::pieces(partresult, part, last + 1); if (Branch.pieces.size() < last + 1) return false; editor_track::keep_heights(Branch.part, Branch.pieces); lift(Branch.pieces, start.y - chain.start.y); Branch.pieces.front().points[segment_data::point::start] = start; Branch.pieces.back().points[segment_data::point::end] = joint; Branch.laid = laid; Branch.factor = factor; return true; } void editor_mode::lay_switch_branch(switch_branch const &Branch, editor_track::edit &Edit) { for (auto *track : Branch.part.tracks) Edit.capture(*track); auto const created{editor_track::relay(Branch.part, Branch.pieces)}; Edit.created.insert(Edit.created.end(), created.begin(), created.end()); // the design kept for the line starting or ending at the switch follows it auto const &records{alignment_store::records()}; auto const from{Branch.chain.start}; auto const joint{Branch.part.end}; for (std::size_t i = 0; i < records.size(); ++i) { auto const &record{records[i]}; if (false == record.whole()) continue; auto const atstart{plan_distance(record.design.start, from) < geometry::same_point}; if (false == atstart && plan_distance(record.design.end, from) >= geometry::same_point) continue; auto design{record.design}; auto &vertices{design.vertices}; if (vertices.empty()) return; auto const outward{Branch.laid.start_direction}; auto const k{atstart ? std::size_t{0} : vertices.size() - 1}; if (atstart) { design.start = Branch.laid.start; design.start_direction = outward; design.start_grade = Branch.laid.start_grade; } else { design.end = Branch.laid.start; design.end_direction = -outward; design.end_grade = -Branch.laid.start_grade; } auto const &corners{record.result.vertices}; if (vertices.size() >= 2 && corners.size() == vertices.size()) { // the corner moves along its other leg to the line from the switch auto const corner{corners[k]}; auto const leg{corners[atstart ? 1 : k - 1] - corner}; auto const determinant{cross(outward, leg)}; if (std::abs(determinant) < 1e-9) return; auto const offset{cross(corner - plan_of(Branch.laid.start), leg) / determinant}; if (offset <= 0.0) return; vertices[k].offset = offset; } // the curve next to the switch takes the shape it was laid with auto const &shape{Branch.laid.vertices.front()}; if (shape.compound || vertices[k].compound) vertices[k] = scaled(vertices[k], Branch.factor); else { vertices[k].radius = shape.radius; vertices[k].transition_in = atstart ? shape.transition_in : shape.transition_out; vertices[k].transition_out = atstart ? shape.transition_out : shape.transition_in; } auto const result{alignment::compute(design)}; if (false == result.valid || false == alignment_store::follows(result, Branch.part.tracks)) return; // the km of the joint the paths are laid up to stays as it was auto origin{record.origin}; if (atstart) { auto const before{alignment_store::chainage_on(record.result, joint)}; auto const after{alignment_store::chainage_on(result, joint)}; if (before.has_value() && after.has_value()) origin += *before - *after; } auto const layer{record.layer}; auto const source{record.source}; auto const fit{record.fit}; alignment_store::forget(static_cast(i)); alignment_store::store(design, result, origin, layer, source, fit); return; } } bool editor_mode::move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status) { if (Paths.size() < 2 || Track.m_paths.size() < 2 || false == editor_track::main_axis(Track).has_value()) return false; double low{0.0}; double high{0.0}; std::string reason; bool const online{switch_room(Track, low, high, reason)}; if (false == online && false == reason.empty()) { Status = reason; return false; } auto const ends{switch_end_moves(Track, Paths.front(), online, {low, high}, m_straights.tolerance.offset, Status)}; if (false == ends.has_value()) return false; auto const joints{editor_track::joints(Track)}; std::vector positions; for (auto const &joint : joints) positions.push_back(editor_track::point_position(Track, joint.own)); std::vector handled(joints.size(), false); auto branches{switch_branches(Track, joints)}; editor_track::edit edit; edit.capture(Track); auto const line{online ? editor_track::find_straight(Track, m_straights.tolerance) : editor_track::straight{}}; for (auto const &end : *ends) { for (std::size_t i = 0; i < joints.size(); ++i) if (glm::distance(positions[i], end.from) < geometry::same_point) handled[i] = true; if (false == switch_vacate(Track, end.from, end.to, end.shift, line, edit, Status)) return false; } Track.m_paths.assign(Paths.begin(), Paths.begin() + 2); auto const notes{plan_switch_branches(Track, joints, branches, handled)}; auto const loose{switch_rejoin(Track, joints, handled, edit)}; editor_track::commit(edit.changed); for (auto const &branch : branches) lay_switch_branch(branch, edit); push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed)); if (false == loose.empty()) Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose; if (false == notes.empty()) Status += (Status.empty() ? "" : "\n") + notes; return true; } std::vector editor_mode::switch_branches(TTrack const &Track, std::vector const &Joints, std::vector const &Skip) const { std::vector branches; auto const axis{editor_track::main_axis(Track)}; if (false == axis.has_value()) return branches; for (std::size_t i = 0; i < Joints.size(); ++i) { if (std::find(Skip.begin(), Skip.end(), Joints[i].other) != Skip.end() || axis->lateral(editor_track::point_position(Track, Joints[i].own)) <= m_straights.tolerance.offset) continue; if (auto branch{take_switch_branch(Track, Joints[i], i)}) branches.push_back(std::move(*branch)); } return branches; } std::string editor_mode::plan_switch_branches(TTrack const &Track, std::vector const &Joints, std::vector &Branches, std::vector &Handled) const { std::string notes; for (auto branch{Branches.begin()}; branch != Branches.end();) { auto const &joint{Joints[branch->joint]}; std::string reason; if (false == plan_switch_branch(Track.m_paths[joint.own.path], joint, *branch) || false == tracks_editable(branch->part.tracks, reason)) { notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch can't keep to it, %s follows the switch on its own"), name_of(*joint.other).c_str()); branch = Branches.erase(branch); continue; } Handled[branch->joint] = true; if (std::abs(branch->factor - 1.0) > 1e-4) notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch keeps to it with the radius of %.1f m instead of %.1f m"), branch->laid.vertices.front().radius, branch->design.vertices.front().radius); ++branch; } return notes; } bool editor_mode::double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason) { double low{0.0}; double high{0.0}; double low2{0.0}; double high2{0.0}; auto const axis{editor_track::main_axis(*Set.first)}; if (false == axis.has_value() || false == switch_room(*Set.first, low, high, Reason) || false == switch_room(*Set.second, low2, high2, Reason)) return false; auto const &main{Set.second->m_paths.front()}; Low = low; High = axis->along(main.points[segment_data::point::start]) + high2; Length = axis->along(main.points[segment_data::point::end]); return true; } bool editor_mode::move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status) { auto const axis{editor_track::main_axis(*Set.first)}; if (Paths.size() < 5 || false == axis.has_value()) return false; double low{0.0}; double high{0.0}; double length{0.0}; std::string reason; bool const online{double_room(Set, low, high, length, reason)}; if (false == online && false == reason.empty()) { Status = reason; return false; } struct outer_end { TTrack *track{nullptr}; switch_end_move move; }; std::vector ends; auto const outer = [&](TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, bool const Atstart) { if (false == online || glm::distance(From, To) < 0.005 || axis->lateral(To) > m_straights.tolerance.offset) return true; auto const target{axis->along(To)}; if (target < low - 0.01 || target > high + 0.01) { Status = STR_C("The switch would run into another switch or past the end of the straight"); return false; } ends.push_back({&Track, {From, To, Atstart ? axis->along(From) - target : target - axis->along(From)}}); return true; }; if (false == outer(*Set.first, Set.first->m_paths.front().points[segment_data::point::start], Paths[0].points[segment_data::point::start], true) || false == outer(*Set.second, Set.second->m_paths.front().points[segment_data::point::end], Paths[2].points[segment_data::point::end], false)) return false; std::stable_sort(ends.begin(), ends.end(), [](outer_end const &A, outer_end const &B) { return A.move.shift > B.move.shift; }); std::array const switches{Set.first, Set.second}; std::array, 2> joints; std::array, 2> positions; std::array, 2> handled; for (std::size_t k = 0; k < 2; ++k) { joints[k] = editor_track::joints(*switches[k]); for (auto const &joint : joints[k]) { positions[k].push_back(editor_track::point_position(*switches[k], joint.own)); handled[k].push_back(joint.other == Set.link || joint.other == Set.first || joint.other == Set.second); } } auto const joint{Set.first->m_paths.front().points[segment_data::point::end]}; auto const &link{Set.link->m_paths.front()}; bool const reversed{glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint)}; std::array, 2> branches; for (std::size_t k = 0; k < 2; ++k) branches[k] = switch_branches(*switches[k], joints[k], {Set.first, Set.link, Set.second}); editor_track::edit edit; for (auto *track : {Set.first, Set.link, Set.second}) edit.capture(*track); for (auto const &end : ends) { for (std::size_t k = 0; k < 2; ++k) for (std::size_t i = 0; i < joints[k].size(); ++i) if (glm::distance(positions[k][i], end.move.from) < geometry::same_point) handled[k][i] = true; auto const line{editor_track::find_straight(*end.track, m_straights.tolerance)}; if (false == switch_vacate(*end.track, end.move.from, end.move.to, end.move.shift, line, edit, Status)) return false; } Set.first->m_paths.assign(Paths.begin(), Paths.begin() + 2); Set.second->m_paths.assign(Paths.begin() + 2, Paths.begin() + 4); Set.link->m_paths = {reversed ? geometry::reversed(Paths[4]) : Paths[4]}; std::string notes; for (std::size_t k = 0; k < 2; ++k) { auto const note{plan_switch_branches(*switches[k], joints[k], branches[k], handled[k])}; if (false == note.empty()) notes += (notes.empty() ? "" : "\n") + note; } std::string loose; for (std::size_t k = 0; k < 2; ++k) { auto const left{switch_rejoin(*switches[k], joints[k], handled[k], edit)}; if (false == left.empty()) loose += (loose.empty() ? "" : ", ") + left; } editor_track::commit(edit.changed); for (auto const &set : branches) for (auto const &branch : set) lay_switch_branch(branch, edit); push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed)); if (false == loose.empty()) Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose; if (false == notes.empty()) Status += (Status.empty() ? "" : "\n") + notes; return true; } bool editor_mode::slip_room(slip_set const &Slip, slip_slide &Slide, std::string &Reason) const { Slide = slip_slide{}; Reason.clear(); // one laid out of straights only, along its first one for (auto const *part : Slip.switches) if (has_controls(part->m_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] = straight_along(*other); 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::bent_switch_frame(TTrack const &Track, std::vector &Frame, std::array &Sides, double &Station) const { Frame.clear(); Sides = {nullptr, nullptr}; if (Track.eType != tt_Switch || Track.m_paths.size() != 2 || Track.m_editorremoved || false == has_controls(Track.m_paths.front()) || slip_of(Track).has_value() || double_of(Track).has_value() || crossover_of(Track).has_value()) return false; // the plain tracks the main track goes on into, each run towards the end of the switch it joins: the one before into its start, the // one past it out of its end std::array forward{true, true}; for (auto const &joint : editor_track::joints(Track)) { if (joint.own.path != 0 || joint.other == nullptr || joint.other->eType != tt_Normal || joint.other->m_editorremoved || joint.other->m_paths.size() != 1) continue; auto const past{joint.own.kind == editor_track::point_kind::end ? 1 : 0}; Sides[past] = joint.other; forward[past] = (joint.theirs.kind == editor_track::point_kind::end) == (past == 0); } if (Sides[0] == nullptr || Sides[1] == nullptr || Sides[0] == Sides[1]) return false; editor_track::chain chain; chain.tracks = {Sides[0], const_cast(&Track), Sides[1]}; chain.forward = {forward[0], true, forward[1]}; Frame = sample_chain(chain); Station = nearest_station(Frame, Track.m_paths.front().points[segment_data::point::start]); return Frame.size() >= 2; } std::vector editor_mode::bent_switch_paths(TTrack const &Track, std::vector const &Frame, double const Station, double const Distance) const { // a path as seen from the curve where the switch starts: across it, over it, along it, as frame_path takes a path of a template auto const local_end = [&](glm::dvec3 const &World, glm::dvec3 const &Control, glm::dvec3 &Point, glm::dvec3 &Localcontrol) { auto const at{nearest_station(Frame, World)}; auto const sample{sample_at(Frame, at)}; glm::dvec2 const across{-sample.tangent.y, sample.tangent.x}; auto const lateral{glm::dot(plan_of(World) - plan_of(sample.position), across)}; Point = {lateral, World.y - sample.position.y + lateral * std::tan(glm::radians(static_cast(sample.roll))), at - Station}; auto const planar{plan_of(Control)}; Localcontrol = glm::length(planar) > 1e-9 ? glm::dvec3{glm::dot(planar, across), Control.y, glm::dot(planar, sample.tangent)} : glm::dvec3{0.0}; }; auto const localized = [&](segment_data const &Path) { auto local{Path}; local.pieces.clear(); local_end(Path.points[segment_data::point::start], Path.points[segment_data::point::control1], local.points[segment_data::point::start], local.points[segment_data::point::control1]); local_end(Path.points[segment_data::point::end], Path.points[segment_data::point::control2], local.points[segment_data::point::end], local.points[segment_data::point::control2]); return local; }; std::vector result; for (std::size_t i = 0; i < Track.m_paths.size(); ++i) { auto const &own{Track.m_paths[i]}; auto path{frame_path(localized(own), Frame, Station + Distance, 1, 1)}; for (auto const &piece : own.pieces) path.pieces.push_back(frame_path(localized(piece), Frame, Station + Distance, 1, 1)); // the main track follows the curve it lies in if (i == 0) path.radius = own.radius; result.push_back(path); } return result; } bool editor_mode::slide_bent_switch(TTrack &Track, double const Distance) { std::vector frame; std::array sides; double station; if (false == bent_switch_frame(Track, frame, sides, station)) return false; auto const &main{Track.m_paths.front()}; auto const length{nearest_station(frame, main.points[segment_data::point::end]) - station}; // as far as the far end of a track next to it, which goes then, the switch joining what it joined there: P4 of another switch, say auto const back{station}; auto const ahead{frame.back().station - station - length}; if (Distance < -back - 1e-3 || Distance > ahead + 1e-3) { m_switch.status = format(STR_C("The switch goes %.2f m back and %.2f m on along the curve at most"), std::max(0.0, back), std::max(0.0, ahead)); ui()->set_status(m_switch.status, true); return false; } // a track shorter than this left beside it goes too constexpr double kRemnant{0.5}; auto distance{std::clamp(Distance, -back, ahead)}; if (back + distance < kRemnant) distance = -back; else if (ahead - distance < kRemnant) distance = ahead; auto const paths{bent_switch_paths(Track, frame, station, distance)}; auto const joints{editor_track::joints(Track)}; std::vector handled(joints.size(), false); // the line leaving the end of the diverging track: the curve past it laid again up to the straight beyond it std::vector branches; for (std::size_t i = 0; i < joints.size(); ++i) if (joints[i].own.path == 1) if (auto branch{take_switch_branch(Track, joints[i], i)}) branches.push_back(std::move(*branch)); editor_track::edit edit; edit.capture(Track); // the tracks of the curve on both sides laid along it up to the new ends of the switch std::vector gone; for (int k = 0; k < 2; ++k) { auto &side{*sides[k]}; for (std::size_t i = 0; i < joints.size(); ++i) if (joints[i].own.path == 0 && joints[i].other == &side) handled[i] = true; // taken up whole: the switch takes its place up to its far end if ((k == 0 ? back + distance : ahead - distance) < 1e-3) { gone.push_back(side.name()); editor_track::retire(side); edit.removed.push_back(&side); continue; } auto const joint{main.points[k == 0 ? segment_data::point::start : segment_data::point::end]}; auto const &path{side.m_paths.front()}; auto const atend{glm::distance(path.points[segment_data::point::end], joint) < glm::distance(path.points[segment_data::point::start], joint)}; auto from{nearest_station(frame, path.points[segment_data::point::start])}; auto to{nearest_station(frame, path.points[segment_data::point::end])}; (atend ? to : from) = station + distance + (k == 0 ? 0.0 : length); auto const a{sample_at(frame, from)}; auto const b{sample_at(frame, to)}; auto const way{to >= from ? 1.0 : -1.0}; auto const span{std::abs(to - from)}; auto const turn{std::abs(geometry::signed_angle(a.tangent, b.tangent))}; auto const handle{turn > 1e-6 ? 4.0 / 3.0 * std::tan(turn / 4.0) * span / turn : span / 3.0}; segment_data laid{path}; laid.pieces.clear(); laid.points[segment_data::point::start] = a.position; laid.points[segment_data::point::end] = b.position; // right at the new end of the switch laid.points[atend ? segment_data::point::end : segment_data::point::start] = paths.front().points[k == 0 ? segment_data::point::start : segment_data::point::end]; auto const rise{laid.points[segment_data::point::end].y - laid.points[segment_data::point::start].y}; laid.points[segment_data::point::control1] = {a.tangent.x * way * handle, rise / 3.0, a.tangent.y * way * handle}; laid.points[segment_data::point::control2] = {-b.tangent.x * way * handle, -rise / 3.0, -b.tangent.y * way * handle}; laid.rolls = {static_cast(a.roll * way), static_cast(b.roll * way)}; edit.capture(side); side.m_paths.front() = laid; } Track.m_paths.assign(paths.begin(), paths.begin() + 2); auto const notes{plan_switch_branches(Track, joints, branches, handled)}; auto const loose{switch_rejoin(Track, joints, handled, edit)}; editor_track::commit(edit.changed); for (auto const &branch : branches) lay_switch_branch(branch, edit); push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed)); switch_store::follow(); records_to_history(); straight_refresh(); m_switch.status = format(STR_C("Switch %s moved %.2f m along the curve"), Track.name().c_str(), distance); for (auto const &name : gone) m_switch.status += "\n" + format(STR_C("%s taken up whole, the switch joins what it joined"), name.c_str()); if (false == loose.empty()) m_switch.status += "\n" + std::string{STR_C("Too far from the new ends of the switch, left unjoined: ")} + loose; if (false == notes.empty()) m_switch.status += "\n" + notes; ui()->set_status(m_switch.status, false == loose.empty()); 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); // one bent into a curve slides along the curve if (Track.m_paths.size() == 2 && has_controls(Track.m_paths.front())) return slide_bent_switch(Track, Distance); auto const set{double_of(Track)}; auto &first{set.has_value() ? *set->first : Track}; if (first.m_paths.size() < 2) return false; auto paths{editor_track::slid(first, Distance)}; if (paths.empty()) return false; std::string status; bool moved{false}; if (set.has_value()) { auto const shift{paths.front().points[segment_data::point::start] - first.m_paths.front().points[segment_data::point::start]}; paths = double_paths(*set); for (auto &path : paths) path.translate(shift); moved = move_double_paths(*set, paths, status); } else moved = move_switch_paths(Track, paths, status); if (false == moved) { m_switch.status = status; ui()->set_status(status, true); return false; } switch_store::follow(); records_to_history(); straight_refresh(); m_switch.status = status.empty() ? format(STR_C("Switch %s moved %.2f m along the straight"), first.name().c_str(), Distance) : status; ui()->set_status(m_switch.status, false == status.empty()); return true; } 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)}; return set.has_value() && switch_slides(*set->first) && switch_slides(*set->second); } if (Track.eType != tt_Switch || Track.m_paths.size() != 2 || Track.m_editorremoved) return false; if (false == has_controls(Track.m_paths.front())) return true; // bent into a curve: along the curve, where it goes on into plain tracks on both sides std::vector frame; std::array sides; double station; return bent_switch_frame(Track, frame, sides, station); } 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; } // bent into a curve: along it, the tracks next to it keeping a metre, the curve past its diverging track drawn as it'll be laid if (std::array sides; bent_switch_frame(Track, slide.frame, sides, slide.station)) { auto const &main{Track.m_paths.front()}; auto const &mouse{ImGui::GetIO().MousePos}; slide.active = true; slide.track = &Track; slide.pressed = {mouse.x, mouse.y}; slide.grip = nearest_station(slide.frame, cursor_level(main.points[segment_data::point::start].y)); slide.length = nearest_station(slide.frame, main.points[segment_data::point::end]) - slide.station; slide.low = -slide.station; slide.high = std::max(0.0, slide.frame.back().station - slide.station - slide.length); auto const joints{editor_track::joints(Track)}; for (std::size_t i = 0; i < joints.size(); ++i) if (joints[i].own.path == 1) if (auto branch{take_switch_branch(Track, joints[i], i)}) slide.branches.push_back({joints[i], 1, std::move(*branch)}); return; } auto const set{double_of(Track)}; auto &first{set.has_value() ? *set->first : Track}; auto const axis{editor_track::main_axis(first)}; if (false == axis.has_value()) return; slide.length = axis->length; if (false == (set.has_value() ? double_room(*set, slide.low, slide.high, slide.length, reason) : switch_room(first, slide.low, slide.high, reason))) { if (false == reason.empty()) ui()->set_status(reason, true); return; } auto const &mouse{ImGui::GetIO().MousePos}; slide.active = true; slide.track = &first; slide.pressed = {mouse.x, mouse.y}; slide.grip = axis->along(cursor_level(axis->origin.y)); std::vector members{&first}; if (set.has_value()) members = {set->first, set->link, set->second}; for (auto *member : members) { if (member->eType != tt_Switch) continue; auto const line{editor_track::find_straight(*member, m_straights.tolerance)}; for (std::size_t i = 0; i < line.tracks.size(); ++i) { if (std::find(members.begin(), members.end(), line.tracks[i]) != members.end()) continue; auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; for (auto const index : {segment_data::point::start, segment_data::point::end}) slide.joints.push_back(axis->along(path.points[index])); } } std::vector const skip(members.begin(), members.end()); for (auto *member : members) { if (member->eType != tt_Switch) continue; std::size_t const offset{set.has_value() && member == set->second ? 2u : 0u}; auto const joints{editor_track::joints(*member)}; for (auto &branch : switch_branches(*member, joints, skip)) { auto const &joint{joints[branch.joint]}; slide.branches.push_back({joint, offset + static_cast(joint.own.path), std::move(branch)}); } } } void editor_mode::switch_slide_update() { auto &slide{m_switch.slide}; if (false == slide.active) return; // along the curve it's bent into if (false == slide.frame.empty()) { if (slide.track == nullptr || slide.track->m_editorremoved) { slide = {}; return; } auto const height{sample_at(slide.frame, slide.station).position.y}; auto distance{std::clamp(nearest_station(slide.frame, cursor_level(height)) - slide.grip, slide.low, slide.high)}; // drawn to the far ends of the tracks next to it, where it takes one up whole and joins what it joined for (auto const end : {slide.low, slide.high}) if (std::abs(distance - end) < 1.0) distance = end; slide.distance = distance; return; } auto const axis{slide.track != nullptr && false == slide.track->m_editorremoved && slide.track->m_paths.size() >= 2 ? editor_track::main_axis(*slide.track) : std::nullopt}; if (false == axis.has_value()) { slide = {}; return; } auto const length{slide.length}; auto const distance{std::clamp(axis->along(cursor_level(axis->origin.y)) - slide.grip, slide.low, slide.high - length)}; double best{1.0}; double snapped{distance}; for (auto const joint : slide.joints) for (auto const end : {0.0, length}) if (auto const gap{std::abs(joint - (end + distance))}; gap < best && std::abs(joint - end) > 0.01) { best = gap; snapped = joint - end; } slide.distance = std::clamp(snapped, slide.low, slide.high - length); } void editor_mode::switch_slide_finish() { auto &slide{m_switch.slide}; if (false == slide.active) return; auto *track{slide.track}; auto const distance{slide.distance}; auto const &mouse{ImGui::GetIO().MousePos}; auto const dragged{glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) > kDragPixels}; slide = {}; if (track != nullptr && false == track->m_editorremoved && dragged) slide_switch(*track, distance); } void editor_mode::draw_switch_slide() const { auto const &slide{m_switch.slide}; if (false == slide.active || slide.track == nullptr || slide.track->m_paths.size() < 2) return; auto const &mouse{ImGui::GetIO().MousePos}; if (glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) <= kDragPixels) return; auto paths{slide.frame.empty() ? editor_track::slid(*slide.track, slide.distance) : bent_switch_paths(*slide.track, slide.frame, slide.station, slide.distance)}; if (paths.empty()) return; // 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); for (auto &path : paths) path.translate(shift); } screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; auto const draw = [&](segment_data const &Path, ImU32 const Color, float const Width) { bezier const curve{Path}; auto previous{curve.point(0.0)}; for (int i = 1; i <= 24; ++i) { auto const next{curve.point(i / 24.0)}; projection.line(drawlist, previous, next, Color, Width); previous = next; } }; for (auto const &path : paths) draw(path, overlay_color::marked, 3.0f); // the curves past the switch, laid again up to the straight beyond them for (auto const &line : slide.branches) { if (line.path >= paths.size()) continue; auto branch{line.branch}; if (plan_switch_branch(paths[line.path], line.joint, branch)) for (auto const &piece : branch.pieces) draw(piece, overlay_color::grip, 2.0f); } ImVec2 at; auto const start{paths.front().points[segment_data::point::start]}; if (false == projection.project(start, at)) return; auto const km{alignment_store::chainage_at(start)}; auto const label{km.has_value() ? format("%+.2f m km %.3f", slide.distance, *km / 1000.0) : format("%+.2f m", slide.distance)}; auto const size{ImGui::CalcTextSize(label.c_str())}; drawlist->AddRectFilled(ImVec2(at.x + 10.0f, at.y - 8.0f), ImVec2(at.x + 18.0f + size.x, at.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f); drawlist->AddText(ImVec2(at.x + 14.0f, at.y - 8.0f), overlay_color::highlight, label.c_str()); }