diff --git a/application/editormode.cpp b/application/editormode.cpp index 2b44933f..ff40cc26 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -2758,6 +2758,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in m_straights.handle = -1; m_route.grip = -1; m_straights.tool_placed = false; + m_straights.detour.clear(); } break; @@ -2867,7 +2868,10 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const } if ((Mods & (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT)) != 0 && m_track_tab == track_tab::straights && false == current_straight().tracks.empty()) { - start_straight_gesture((Mods & GLFW_MOD_CONTROL) != 0 ? 1 : 2); + if ((Mods & GLFW_MOD_CONTROL) != 0) + start_straight_gesture(1); + else + add_detour_point(); m_input.mouse.button(Button, Action); return; } diff --git a/application/editormode.h b/application/editormode.h index 6ac172c7..02cbd544 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -354,6 +354,9 @@ class editor_mode : public application_mode double transition{0.0}; int tool{0}; bool tool_mouse{false}; + std::vector detour; + editor_track::straight detour_line; + glm::dvec3 detour_mouse{0.0}; bool tool_placed{false}; bool tool_dragging{false}; double tool_at{0.0}; @@ -378,6 +381,9 @@ class editor_mode : public application_mode double straight_tool_radius(editor_track::straight const &Line) const; void straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius); void straight_refresh(); + void add_detour_point(); + void apply_detour(); + std::vector detour_outline() const; void find_neighbour_straights(); glm::dvec3 snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const; glm::dvec3 snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 5209f164..4f727c40 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -374,13 +374,13 @@ void editor_mode::commit_track_drag(bool const Force) void editor_mode::push_track_snapshot(std::vector> States, std::vector Created) { - if (States.empty()) + if (States.empty() && Created.empty()) return; if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); - auto *track = States.front().first; + auto *track = States.empty() ? Created.front() : States.front().first; EditorSnapshot snap; snap.action = EditorSnapshot::Action::TrackEdit; snap.node_name = track->name(); @@ -547,8 +547,6 @@ void editor_mode::render_turnout_ui() } ImGui::PopItemWidth(); auto const replace = [&](editor_track::switch_template const &Template, int const Side) { - if (false == track->Dynamics.empty()) - return; auto const grade{length > 0.0 ? (main.points[segment_data::point::end].y - origin.y) / length : 0.0}; auto const paths{editor_track::place_switch(Template, origin, axis, Side, grade)}; push_track_snapshot({{track, editor_track::capture(*track)}}); @@ -1846,14 +1844,6 @@ void editor_mode::straights_apply(std::vector const &Lin auto &state{m_straights}; state.status.clear(); auto const tracks{editor_track::straight_affected(Lines)}; - for (auto *track : tracks) - { - if (false == track->Dynamics.empty()) - { - state.status = "There are vehicles placed on path " + track->name(); - return; - } - } std::vector> states; for (auto *track : tracks) states.emplace_back(track, editor_track::capture(*track)); @@ -1917,11 +1907,6 @@ void editor_mode::render_straight_gizmo() lines.push_back(line); } auto const tracks{editor_track::straight_affected(lines)}; - if (std::any_of(tracks.begin(), tracks.end(), [](TTrack const *Track) { return false == Track->Dynamics.empty(); })) - { - state.status = "There are vehicles placed on the straights or the paths attached to them"; - return; - } state.dragging = true; state.drag_line = line; state.drag_lines = lines; @@ -2024,8 +2009,38 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co } } }; + auto const keep_regular = [&]() { + if (first < 0) + return; + auto const centre{(std::max(From, 0.0) + std::min(To, line.length)) * 0.5}; + int middle{first}; + for (int i = first; i <= last; ++i) + if (spans[i].from <= centre && spans[i].to >= centre) + middle = i; + if (spans[middle].track->eType != tt_Normal) + { + int best{-1}; + for (int i = first; i <= last; ++i) + if (spans[i].track->eType == tt_Normal && (best < 0 || std::abs(i - middle) < std::abs(best - middle))) + best = i; + if (best < 0) + { + first = last = -1; + return; + } + middle = best; + } + int low{middle}, high{middle}; + while (low > first && spans[low - 1].track->eType == tt_Normal) + --low; + while (high < last && spans[high + 1].track->eType == tt_Normal) + ++high; + first = low; + last = high; + }; collect(); select(To); + keep_regular(); if (first < 0) return; @@ -2035,11 +2050,8 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co if (last + 1 >= static_cast(spans.size())) { auto const final{spans.back()}; - if (final.track->eType != tt_Normal || false == final.track->Dynamics.empty()) - { - state.status = "The end of the straight can't be reshaped here"; + if (final.track->eType != tt_Normal) return; - } auto const length{final.to - final.from}; auto const cut{To < final.to - 1.0 ? std::max(To, final.from + std::min(1.0, length * 0.5)) : final.from + length * 0.7}; splitstates.emplace_back(final.track, editor_track::capture(*final.track)); @@ -2056,6 +2068,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co collect(); end = std::min(To, cut); select(end); + keep_regular(); if (first < 0 || last + 1 >= static_cast(spans.size())) { for (auto *track : splitcreated) @@ -2074,15 +2087,6 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co editor_track::commit({entry.first}); } }; - for (int i = first; i <= last; ++i) - { - if (spans[i].track->eType != tt_Normal) - { - undo_split(); - state.status = "There's a switch in the reshaped part of the straight"; - return; - } - } editor_track::straight tail; tail.direction = line.direction; @@ -2103,15 +2107,6 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co for (auto *track : chainpaths) if (std::find(tracks.begin(), tracks.end(), track) == tracks.end()) tracks.push_back(track); - for (auto *track : tracks) - { - if (false == track->Dynamics.empty()) - { - undo_split(); - state.status = "There are vehicles placed on path " + track->name(); - return; - } - } std::vector> states; for (auto *track : tracks) states.emplace_back(track, editor_track::capture(*track)); @@ -2187,6 +2182,11 @@ bool editor_mode::place_straight_tool() auto &state{m_straights}; if (ui()->mode() != nodebank_panel::TRACK || m_track_tab != track_tab::straights || state.tool == 0) return false; + if (state.tool == 2) + { + add_detour_point(); + return true; + } auto const &line{current_straight()}; if (line.tracks.empty()) return false; @@ -2339,14 +2339,16 @@ void editor_mode::delete_selected_track() void editor_mode::draw_track_hints() { std::string hint; - if (m_extend.active) + if (false == m_straights.detour.empty()) + hint = "Shift+click: point " + std::to_string(m_straights.detour.size() + 1) + " of 4 (1: leave the axis, 2: shifted, 3: start back, 4: back on the axis) Esc: cancel"; + else if (m_extend.active) hint = "Drag: curve through the cursor, along the end: straight Ctrl: straight"; else if (m_switch.placing) hint = "Drag along the straight to set the direction of the switch, sideways to set its side"; else if (m_switch.armed >= 0 && m_track_tab == track_tab::straights) hint = "Press on the selected straight where the switch starts and drag"; else if (m_track_tab == track_tab::straights) - hint = current_straight().tracks.empty() ? "LMB: select a straight or a curve" : "Drag the ends or the middle Ctrl+drag: break Shift+drag: S-curve Alt+click: add to the set Drag from a red ring: new path Del: delete"; + hint = current_straight().tracks.empty() ? "LMB: select a straight or a curve" : "Drag the ends or the middle Ctrl+drag: break Shift+click x4: shift around Alt+click: add to the set Drag from a red ring: new path Del: delete"; else if (m_track_tab == track_tab::turnout) hint = "Gizmo: move the switch (W: turn) Drag from a red ring: new path Del: delete"; else if (m_track_tab == track_tab::route) @@ -2365,6 +2367,7 @@ void editor_mode::draw_track_hints() void editor_mode::update_build_tools() { glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + m_straights.detour_mouse = ground; if (m_extend.active) m_extend.mouse = ground; if (m_switch.placing) @@ -2374,7 +2377,7 @@ void editor_mode::update_build_tools() bool editor_mode::start_extend() { auto *track{selected_track()}; - if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track) || false == track->Dynamics.empty()) + if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) return false; screen_projection const projection; ImVec2 const mouse = ImGui::GetIO().MousePos; @@ -2442,7 +2445,7 @@ std::vector editor_mode::extend_pieces() const auto const radius{chord / (2.0 * std::sin(angle * 0.5))}; double const side{lateral > 0.0 ? 1.0 : -1.0}; glm::dvec2 const centre{glm::dvec2{tool.point.x, tool.point.z} + glm::dvec2{-direction.y, direction.x} * (side * radius)}; - auto const count{std::max(1, static_cast(std::ceil(angle / glm::radians(10.0) - 1e-9)))}; + auto const count{std::max(1, static_cast(std::ceil(angle / glm::radians(90.0) - 1e-9)))}; auto const step{angle / count}; auto const handle{4.0 / 3.0 * std::tan(step / 4.0) * radius}; auto const turn = [&](glm::dvec2 const &Vector, double const Angle) { @@ -2537,13 +2540,11 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const auto &state{m_straights}; state.status.clear(); auto const &shape{m_switch.templates[m_switch.armed]}; - auto const from{Direction > 0 ? Along : Along - shape.length}; - auto const to{from + shape.length}; - if (from < -0.01 || to > Line.length + 0.01) - { - state.status = "The switch doesn't fit on the straight here"; + if (shape.length > Line.length - 0.02) return; - } + auto const from{std::clamp(Direction > 0 ? Along : Along - shape.length, 0.01, Line.length - shape.length - 0.01)}; + auto const to{from + shape.length}; + auto const origin{Direction > 0 ? from : to}; struct span { TTrack *track; @@ -2576,11 +2577,8 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const editor_track::chain chain; for (int i = first; i <= last; ++i) { - if (spans[i].track->eType != tt_Normal || false == spans[i].track->Dynamics.empty()) - { - state.status = "The switch would overlap another switch or a path with vehicles"; + if (spans[i].track->eType != tt_Normal) return; - } chain.tracks.push_back(spans[i].track); chain.forward.push_back(spans[i].forward); } @@ -2588,9 +2586,23 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const auto const after{std::max(0.0, spans[last].to - to)}; bool const hasbefore{before > 0.01}; bool const hasafter{after > 0.01}; + auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; + glm::dvec2 const direction{Line.direction * static_cast(Direction)}; if (false == hasbefore && false == hasafter) { - state.status = "The switch would replace the paths completely, place it so that a piece of the straight remains"; + for (auto *track : chain.tracks) + editor_track::retire(*track); + auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; + auto *track{editor_track::create_switch(shape, paths, *spans[first].track)}; + std::vector created; + if (track != nullptr) + { + editor_track::commit({track}); + created.push_back(track); + } + push_track_snapshot({}, std::move(created)); + m_history.back().removed = chain.tracks; + straight_refresh(); return; } auto const count{std::max(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))}; @@ -2599,7 +2611,6 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const countbefore = std::clamp(static_cast(std::round(count * before / (before + after))), 1, count - 1); else if (hasbefore) countbefore = count; - auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; std::vector pieces; auto const straight = [&](double const From, double const To, std::size_t const Count) { for (std::size_t i = 0; i < Count; ++i) @@ -2619,8 +2630,7 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const for (auto *track : chain.tracks) states.emplace_back(track, editor_track::capture(*track)); auto created{editor_track::relay(chain, pieces)}; - glm::dvec2 const direction{Line.direction * static_cast(Direction)}; - auto const paths{editor_track::place_switch(shape, point(Along), direction, Side, Line.grade * Direction)}; + auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; auto *track{editor_track::create_switch(shape, paths, *spans[first].track)}; if (track != nullptr) { @@ -2651,6 +2661,18 @@ void editor_mode::draw_build_overlay() const if (m_switch.placing) for (auto const &piece : switch_preview()) drawpath(piece, IM_COL32(255, 210, 60, 255)); + if (false == m_straights.detour.empty()) + { + auto const outline{detour_outline()}; + for (std::size_t i = 0; i + 1 < outline.size(); ++i) + projection.line(drawlist, outline[i], outline[i + 1], IM_COL32(255, 210, 60, 255), 3.0f); + for (std::size_t i = 0; i < outline.size() && i < m_straights.detour.size(); ++i) + { + ImVec2 screen; + if (projection.project(outline[i], screen)) + drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(255, 60, 60, 255)); + } + } auto const *track{selected_track()}; if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || m_extend.active) return; @@ -2697,3 +2719,154 @@ void editor_mode::render_switch_ui() tool.armed = -1; } } + +std::vector editor_mode::detour_outline() const +{ + auto const &state{m_straights}; + std::vector result; + if (state.detour.empty()) + return result; + auto const &line{state.detour_line}; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto points{state.detour}; + if (points.size() < 4) + points.push_back(state.detour_mouse); + auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; + auto const offset{points.size() >= 2 ? glm::dot(glm::dvec2{points[1].x - line.start.x, points[1].z - line.start.z}, normal) : 0.0}; + auto const place = [&](double const Along, double const Lateral) { + auto const planar{glm::dvec2{line.start.x, line.start.z} + line.direction * Along + normal * Lateral}; + return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; + }; + for (std::size_t i = 0; i < points.size(); ++i) + result.push_back(place(along(points[i]), (i == 1 || i == 2) ? offset : 0.0)); + return result; +} + +void editor_mode::add_detour_point() +{ + auto &state{m_straights}; + if (state.detour.empty()) + { + auto const &line{current_straight()}; + if (line.tracks.empty()) + return; + state.detour_line = line; + } + state.detour.push_back(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); + if (state.detour.size() >= 4) + { + apply_detour(); + state.detour.clear(); + } +} + +void editor_mode::apply_detour() +{ + auto &state{m_straights}; + auto line{editor_track::find_straight(*state.detour_line.tracks.front(), state.tolerance)}; + if (line.tracks.empty()) + return; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; + auto const offset{glm::dot(glm::dvec2{state.detour[1].x - line.start.x, state.detour[1].z - line.start.z}, normal)}; + std::array stations{along(state.detour[0]), along(state.detour[1]), along(state.detour[2]), along(state.detour[3])}; + if (stations[0] > stations[3]) + { + std::swap(stations[0], stations[3]); + std::swap(stations[1], stations[2]); + } + for (auto &station : stations) + station = std::clamp(station, 0.0, line.length); + stations[1] = std::clamp(stations[1], stations[0] + 1.0, stations[3] - 1.0); + stations[2] = std::clamp(stations[2], stations[1], stations[3] - 1.0); + if (std::abs(offset) < 0.01 || stations[3] - stations[0] < 3.0) + return; + + auto const radius{straight_tool_radius(line)}; + auto const slope{std::atan(std::abs(offset) / std::max(1.0, std::min(stations[1] - stations[0], stations[3] - stations[2])))}; + double speed{-1.0}; + for (auto const *track : line.tracks) + speed = std::max(speed, editor_track::velocity(*track)); + auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; + auto const reach{radius * std::tan(slope * 0.5) + transition * 0.5 + 5.0}; + auto const from{std::max(0.0, stations[0] - reach)}; + auto const to{std::min(line.length, stations[3] + reach)}; + + struct span + { + TTrack *track; + double from, to; + }; + std::vector spans; + for (std::size_t i = 0; i < line.tracks.size(); ++i) + { + auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; + auto const a{along(path.points[segment_data::point::start])}; + auto const b{along(path.points[segment_data::point::end])}; + spans.push_back({line.tracks[i], std::min(a, b), std::max(a, b)}); + } + std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); + int first{-1}, last{-1}; + for (int i = 0; i < static_cast(spans.size()); ++i) + { + if (spans[i].to > from && spans[i].from < to && spans[i].track->eType == tt_Normal) + { + if (first < 0) + first = i; + last = i; + } + else if (first >= 0 && spans[i].from < to) + { + if (spans[i].from > stations[3]) + break; + first = last = -1; + } + } + if (first < 0) + return; + + m_route.from = spans[first].track; + m_route.to = spans[last].track; + route_reset(); + if (m_route.chain.tracks.empty()) + return; + auto &design{m_route.design}; + auto const startalong{along(design.start)}; + auto const endalong{along(design.end)}; + auto const place = [&](double const Along, double const Lateral) { + auto const planar{glm::dvec2{line.start.x, line.start.z} + line.direction * Along + normal * Lateral}; + return planar; + }; + alignment::vertex vertex; + vertex.radius = radius; + route_recommend(vertex); + if (false == state.auto_transitions) + vertex.transition_in = vertex.transition_out = state.transition; + design.vertices.clear(); + auto first_vertex{vertex}; + first_vertex.offset = std::max(0.1, stations[0] - startalong); + design.vertices.push_back(first_vertex); + if (stations[2] - stations[1] < 1.0) + { + auto middle{vertex}; + middle.position = place((stations[1] + stations[2]) * 0.5, offset); + design.vertices.push_back(middle); + } + else + { + auto second{vertex}; + second.position = place(stations[1], offset); + auto third{vertex}; + third.position = place(stations[2], offset); + design.vertices.push_back(second); + design.vertices.push_back(third); + } + auto last_vertex{vertex}; + last_vertex.offset = std::max(0.1, endalong - stations[3]); + design.vertices.push_back(last_vertex); + route_update(); + if (false == m_route.result.valid) + return; + route_apply(); + straight_refresh(); +} diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 63134cbe..bc6fddc3 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -11,6 +11,7 @@ http: #include "editor/editorTrack.hpp" #include "world/Track.h" +#include "vehicle/DynObj.h" #include "scene/scene.h" #include "simulation/simulation.h" #include "rendering/renderer.h" @@ -191,11 +192,6 @@ bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason) Reason = "Path geometry can't be rebuilt with the experimental renderer"; return false; } - if (false == Track.Dynamics.empty()) - { - Reason = "There are vehicles placed on this path"; - return false; - } Reason.clear(); return true; } @@ -235,6 +231,9 @@ void editor_track::commit(std::vector const &Tracks) rebuild_geometry(*track); for (auto *track : Tracks) track->mark_dirty(); + for (auto *track : affected) + for (auto *vehicle : track->Dynamics) + vehicle->Move(0.000001); } void editor_track::commit_parameters(TTrack &Track) @@ -745,12 +744,6 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin Chain = {}; return false; } - if (false == track->Dynamics.empty()) - { - Error = "There are vehicles placed on path " + track->name(); - Chain = {}; - return false; - } Chain.length += track->Length(); Chain.velocity = std::max(Chain.velocity, velocity(*track)); auto const radius{path_radius(track->m_paths.front(), 0.5)};