diff --git a/application/editormode.cpp b/application/editormode.cpp index e4479803..77afe2e7 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1016,7 +1016,11 @@ bool editor_mode::update() if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights) draw_straights_overlay(); if (ui()->mode() == nodebank_panel::TRACK) + { + update_build_tools(); + draw_build_overlay(); draw_track_hints(); + } // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) @@ -2846,6 +2850,11 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (mode == nodebank_panel::TRACK) { + if (start_extend() || start_switch_placement()) + { + m_input.mouse.button(Button, Action); + return; + } if ((Mods & GLFW_MOD_ALT) != 0) { GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { @@ -2968,6 +2977,10 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const mouseHold = false; if (m_straights.tool_mouse) finish_straight_gesture(); + if (m_extend.active) + finish_extend(); + if (m_switch.placing) + finish_switch_placement(); } m_dragging = false; diff --git a/application/editormode.h b/application/editormode.h index 2545c5e8..1ff98fd3 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -382,6 +382,39 @@ class editor_mode : public application_mode 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; straights_state m_straights; + + struct switch_tool + { + std::vector templates; + bool collected{false}; + int armed{-1}; + bool placing{false}; + double along{0.0}; + glm::dvec3 point{0.0}; + glm::dvec3 mouse{0.0}; + editor_track::straight line; + }; + switch_tool m_switch; + struct extend_tool + { + bool active{false}; + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + glm::dvec3 mouse{0.0}; + }; + extend_tool m_extend; + void render_switch_ui(); + bool start_switch_placement(); + void finish_switch_placement(); + std::vector switch_preview() const; + void insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side); + bool start_extend(); + void finish_extend(); + std::vector extend_pieces() const; + void update_build_tools(); + void draw_build_overlay() const; enum class track_tab { straights, route, path }; track_tab m_track_tab{track_tab::straights}; int m_track_tab_request{-1}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index fc4796b9..57cd984b 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1583,6 +1583,7 @@ void editor_mode::render_straights_ui() } if (false == state.status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); + render_switch_ui(); if (false == state.set.empty()) { @@ -2091,8 +2092,14 @@ void editor_mode::delete_selected_track() void editor_mode::draw_track_hints() { std::string hint; - 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 of the straight Ctrl+drag: break Shift+drag: S-curve Alt+click: add to the set Del: delete path"; + 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"; else if (m_track_tab == track_tab::route) hint = m_route.chain.tracks.empty() ? "LMB: select a curve" : "Drag the vertex (yellow) or the radius grip (green), the change is applied on release Ctrl+Z: undo"; else @@ -2105,3 +2112,333 @@ void editor_mode::draw_track_hints() drawlist->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 150), 4.0f); drawlist->AddText(position, IM_COL32(255, 255, 255, 230), hint.c_str()); } + +void editor_mode::update_build_tools() +{ + glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + if (m_extend.active) + m_extend.mouse = ground; + if (m_switch.placing) + m_switch.mouse = ground; +} + +bool editor_mode::start_extend() +{ + auto *track{selected_track()}; + if (track == nullptr || track->eType != tt_Normal || false == editor_track::is_supported(*track) || false == track->Dynamics.empty()) + return false; + screen_projection const projection; + ImVec2 const mouse = ImGui::GetIO().MousePos; + auto const &path{track->m_paths.front()}; + for (auto const atend : {false, true}) + { + editor_track::point_ref const point{0, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + if (editor_track::is_connected(*track, point)) + continue; + auto const position{editor_track::point_position(*track, point)}; + ImVec2 screen; + if (false == projection.project(position, screen)) + continue; + if ((screen.x - mouse.x) * (screen.x - mouse.x) + (screen.y - mouse.y) * (screen.y - mouse.y) > kHandleRadius * kHandleRadius * 1.5f) + continue; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto const &control1{path.points[segment_data::point::control1]}; + auto const &control2{path.points[segment_data::point::control2]}; + glm::dvec3 tangent; + if (atend) + tangent = control2 != glm::dvec3{} ? -control2 : end - (start + control1); + else + tangent = -(control1 != glm::dvec3{} ? control1 : (end + control2) - start); + glm::dvec2 const planar{tangent.x, tangent.z}; + if (glm::length(planar) < 1e-6) + return false; + m_extend.active = true; + m_extend.track = track; + m_extend.point = position; + m_extend.direction = glm::normalize(planar); + m_extend.grade = tangent.y / glm::length(planar); + m_extend.mouse = position; + return true; + } + return false; +} + +std::vector editor_mode::extend_pieces() const +{ + std::vector result; + auto const &tool{m_extend}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const chord{glm::length(offset)}; + if (chord < 0.5) + return result; + auto const &direction{tool.direction}; + auto const along{glm::dot(offset, direction)}; + auto const lateral{direction.x * offset.y - direction.y * offset.x}; + auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; }; + if (ImGui::GetIO().KeyCtrl || std::abs(lateral) < 0.005 * chord + 0.05) + { + if (along < 0.5) + return result; + segment_data path; + path.points[segment_data::point::start] = tool.point; + path.points[segment_data::point::end] = {tool.point.x + direction.x * along, height(along), tool.point.z + direction.y * along}; + result.push_back(path); + return result; + } + auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; + 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 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) { + auto const c{std::cos(Angle)}; + auto const s{std::sin(Angle)}; + return glm::dvec2{Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; + }; + glm::dvec2 const radial{glm::dvec2{tool.point.x, tool.point.z} - centre}; + for (int i = 0; i < count; ++i) + { + auto const a0{side * step * i}; + auto const a1{side * step * (i + 1)}; + auto const p0{centre + turn(radial, a0)}; + auto const p3{centre + turn(radial, a1)}; + auto const d0{turn(direction, a0)}; + auto const d3{turn(direction, a1)}; + auto const y0{height(radius * step * i)}; + auto const y3{height(radius * step * (i + 1))}; + segment_data path; + path.points[segment_data::point::start] = {p0.x, y0, p0.y}; + path.points[segment_data::point::end] = {p3.x, y3, p3.y}; + path.points[segment_data::point::control1] = {d0.x * handle, tool.grade * handle, d0.y * handle}; + path.points[segment_data::point::control2] = {-d3.x * handle, -tool.grade * handle, -d3.y * handle}; + path.radius = static_cast(radius); + result.push_back(path); + } + return result; +} + +void editor_mode::finish_extend() +{ + auto const pieces{extend_pieces()}; + auto *style{m_extend.track}; + m_extend.active = false; + if (pieces.empty() || style == nullptr) + return; + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*style, piece)); + editor_track::commit(created); + push_track_snapshot({}, created); + m_node = created.back(); + ui()->set_node(m_node); + straight_refresh(); +} + +std::vector editor_mode::switch_preview() const +{ + auto const &tool{m_switch}; + if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return {}; + auto const &line{tool.line}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{line.direction * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + return editor_track::place_switch(tool.templates[tool.armed], tool.point, direction, side, line.grade * forward); +} + +bool editor_mode::start_switch_placement() +{ + auto &tool{m_switch}; + if (tool.armed < 0 || m_track_tab != track_tab::straights) + return false; + auto const &line{current_straight()}; + if (line.tracks.empty()) + return false; + glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + tool.along = std::clamp(glm::dot(glm::dvec2{ground.x - line.start.x, ground.z - line.start.z}, line.direction), 0.0, line.length); + tool.point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * tool.along; + tool.mouse = ground; + tool.line = line; + tool.placing = true; + return true; +} + +void editor_mode::finish_switch_placement() +{ + auto &tool{m_switch}; + tool.placing = false; + if (tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, tool.line.direction) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{tool.line.direction * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + insert_switch(tool.line, tool.along, forward, side); +} + +void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side) +{ + 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"; + return; + } + struct span + { + TTrack *track; + int path; + double from, to; + bool forward; + }; + auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - Line.start.x, Point.z - Line.start.z}, Line.direction); }; + 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], Line.paths[i], std::min(a, b), std::max(a, b), 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 + 1e-3 && spans[i].from < to - 1e-3) + { + if (first < 0) + first = i; + last = i; + } + } + if (first < 0) + return; + 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"; + return; + } + chain.tracks.push_back(spans[i].track); + chain.forward.push_back(spans[i].forward); + } + auto const before{std::max(0.0, from - spans[first].from)}; + auto const after{std::max(0.0, spans[last].to - to)}; + bool const hasbefore{before > 0.01}; + bool const hasafter{after > 0.01}; + if (false == hasbefore && false == hasafter) + { + state.status = "The switch would replace the paths completely, place it so that a piece of the straight remains"; + return; + } + auto const count{std::max(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))}; + std::size_t countbefore{0}; + if (hasbefore && hasafter) + 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) + { + segment_data path; + path.points[segment_data::point::start] = point(From + (To - From) * i / Count); + path.points[segment_data::point::end] = point(From + (To - From) * (i + 1) / Count); + pieces.push_back(path); + } + }; + if (hasbefore) + straight(spans[first].from, from, countbefore); + if (hasafter) + straight(to, spans[last].to, count - countbefore); + + std::vector> states; + 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 *track{editor_track::create_switch(shape, paths, *spans[first].track)}; + if (track != nullptr) + { + editor_track::commit({track}); + created.push_back(track); + } + push_track_snapshot(std::move(states), std::move(created)); + straight_refresh(); + state.status = "Switch inserted"; +} + +void editor_mode::draw_build_overlay() const +{ + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + auto const drawpath = [&](segment_data const &Path, ImU32 const Color) { + auto previous{path_point(Path, 0.0)}; + for (int i = 1; i <= 24; ++i) + { + auto const next{path_point(Path, i / 24.0)}; + projection.line(drawlist, previous, next, Color, 3.0f); + previous = next; + } + }; + if (m_extend.active) + for (auto const &piece : extend_pieces()) + drawpath(piece, IM_COL32(255, 210, 60, 255)); + if (m_switch.placing) + for (auto const &piece : switch_preview()) + drawpath(piece, IM_COL32(255, 210, 60, 255)); + auto const *track{selected_track()}; + if (track == nullptr || track->eType != tt_Normal || m_extend.active) + return; + for (auto const atend : {false, true}) + { + editor_track::point_ref const point{0, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + if (editor_track::is_connected(*track, point)) + continue; + ImVec2 screen; + if (projection.project(editor_track::point_position(*track, point), screen)) + drawlist->AddCircle(screen, 10.0f, IM_COL32(240, 60, 60, 255), 16, 3.0f); + } +} + +void editor_mode::render_switch_ui() +{ + auto &tool{m_switch}; + if (false == ImGui::CollapsingHeader("Switches", ImGuiTreeNodeFlags_DefaultOpen)) + return; + if (false == tool.collected) + { + tool.templates = editor_track::standard_switch_templates(); + auto const found{editor_track::find_switch_templates()}; + tool.templates.insert(tool.templates.end(), found.begin(), found.end()); + tool.collected = true; + } + ImGui::TextDisabled(tool.armed >= 0 ? "Press on the straight where the switch starts and drag: along sets its direction, sideways its side" : "Click a template, then drag on the selected straight"); + ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, 150.0f), true); + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &entry{tool.templates[i]}; + auto label{entry.source != nullptr ? "Scenery: " + entry.label + " (" + std::to_string(entry.count) + "x)" : "PLK: " + entry.label}; + label += "##switch" + std::to_string(i); + if (ImGui::Selectable(label.c_str(), tool.armed == i)) + tool.armed = (tool.armed == i ? -1 : i); + } + ImGui::EndChild(); + if (ImGui::SmallButton("Collect again from the scenery")) + { + tool.collected = false; + tool.armed = -1; + } +} diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index f7a4f778..261afe2e 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1432,6 +1432,34 @@ TTrack *editor_track::clone(TTrack const &Template) return track; } +TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) +{ + char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"}; + auto const category{Style.iCategoryFlag & 15}; + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + text << (category == 2 ? "road " : category == 4 ? "river " : "normal ") << glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end]) << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << Style.fSoundDistance << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' '; + if (Style.m_visible) + text << "vis " << material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << material_name(Style.m_material2) << ' ' << texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' '; + else + text << "unvis "; + auto const &start{Path.points[segment_data::point::start]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + auto const &end{Path.points[segment_data::point::end]}; + text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; + auto const speed{velocity(Style)}; + if (speed > 0.0) + text << "velocity " << speed << ' '; + if (category == 1 && false == Style.m_profile1.first.empty()) + text << "railprofile " << Style.m_profile1.first << ' '; + text << "endtrack\n"; + auto *track{load_path(text.str(), Style)}; + track->m_paths = {Path}; + return track; +} + TTrack *editor_track::load_path(std::string const &Text, TTrack const &Template) { cParser parser(Text, cParser::buffer_TEXT); diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index ad828284..bab1d75c 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -144,6 +144,8 @@ class editor_track }; static std::vector standard_switch_templates(); static std::vector find_switch_templates(); + static TTrack *load_path(std::string const &Text, TTrack const &Template); + static TTrack *create_path(TTrack const &Style, segment_data const &Path); static std::vector place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade); static TTrack *create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style); static void move_straights(std::vector const &Lines, std::vector> const &Ends); @@ -169,6 +171,5 @@ class editor_track static void store_switch_path(TTrack &Switch, int const Path); static void rebuild_geometry(TTrack &Track); static TTrack *clone(TTrack const &Template); - static TTrack *load_path(std::string const &Text, TTrack const &Template); static void move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function const &Member); };