diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f13c2b8..dabcc167 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,9 +170,11 @@ set(SOURCES "editor/editorModelSets.cpp" "editor/editorIncludeInfo.cpp" "editor/editorTerrain.cpp" +"editor/editorTrack.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" +"application/editormodetrack.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 6edf40e4..0e044433 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -339,6 +339,7 @@ editor_mode::editor_mode() { ui()->set_fill_options([this]() { render_area_fill(); }); ui()->set_gizmo_options([this]() { render_gizmo_options(); }); ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); + ui()->set_track_options([this]() { render_track_ui(); }); // the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) { gather_ground_triangles(Min, Max, true, 50.0f, Out); @@ -680,6 +681,12 @@ void editor_mode::undo_last() return; } + if (snap.action == EditorSnapshot::Action::TrackEdit) + { + restore_track_snapshot(snap, g_redo); + return; + } + if (snap.action == EditorSnapshot::Action::Delete) { // undo delete -> recreate model @@ -769,6 +776,12 @@ void editor_mode::redo_last() return; } + if (snap.action == EditorSnapshot::Action::TrackEdit) + { + restore_track_snapshot(snap, m_history); + return; + } + // handle delete redo (re-delete) separately if (snap.action == EditorSnapshot::Action::Delete) { @@ -996,6 +1009,10 @@ bool editor_mode::update() // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); + // --- path editing: course and point handles of the selected path --- + if (selected_track()) + draw_track_overlay(); + // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) draw_area_fill_outline(); @@ -2232,6 +2249,13 @@ void editor_mode::render_gizmo() return; } + // paths aren't transformed as a whole node, they're edited through their source data + if (selected_track()) + { + render_track_gizmo(); + return; + } + ImGuizmo::BeginFrame(); ImGuizmo::SetOrthographic(false); @@ -2504,6 +2528,13 @@ void editor_mode::exit() FreeFlyModeFlag = m_statebackup.freefly; Global.pCamera = m_statebackup.camera; + commit_track_drag(true); + m_track_gizmo_using = false; + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_snap = {}; + m_track_point = {}; + g_redo.clear(); m_history.clear(); m_fill_last.clear(); @@ -2647,6 +2678,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in } break; + case GLFW_KEY_ESCAPE: + // track mode: release the selected point handle, so the gizmo moves the whole path again + if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) + m_track_point = {}; + break; + case GLFW_KEY_BACKSPACE: // area fill: remove the last outline point if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) @@ -2735,6 +2772,20 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } + // in track mode the left button selects a point handle of the selected path, or another path + if (mode == nodebank_panel::TRACK) + { + if (false == ImGuizmo::IsOver() && false == pick_track_handle()) + { + GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { + if (viewport_click()) + select_track(node); + }); + } + m_input.mouse.button(Button, Action); + return; + } + // delegate node picking behaviour depending on current panel mode. // NOTE: the pick is resolved a few frames later, often after the button was already released, // so the callback mustn't depend on the button still being held @@ -2879,7 +2930,8 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::Delete ? "DEL" : s.action == EditorSnapshot::Action::Move ? "MOV" : s.action == EditorSnapshot::Action::Rotate ? "ROT" : - s.action == EditorSnapshot::Action::Scale ? "SCA" : "OTH", + s.action == EditorSnapshot::Action::Scale ? "SCA" : + s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : "OTH", s.node_name.empty() ? "(noname)" : s.node_name.c_str(), s.position.x, s.position.y, s.position.z); diff --git a/application/editormode.h b/application/editormode.h index edd35ea7..6dc9320d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -19,12 +19,16 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTerrain.hpp" #include "editor/editorTerrainStreamer.hpp" #include "editor/editorOrthophoto.hpp" +#include "editor/editorTrack.hpp" +#include +#include #include #include #include class TAnimModel; +class TTrack; class editor_mode : public application_mode { @@ -85,7 +89,7 @@ class editor_mode : public application_mode struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, Other }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other }; Action action{Action::Other}; std::string node_name; // node identifier (basic_node::name()) @@ -98,6 +102,8 @@ class editor_mode : public application_mode UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it scene::instance_handle instance{0}; // include of a scenery template which was placed or removed, instead of a node + // TrackEdit: state of all paths modified by the change (the edited path and neighbours dragged along) + std::vector> tracks; }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -268,4 +274,37 @@ class editor_mode : public application_mode bool m_gizmo_local{false}; // manipulate in the object's local space instead of world space gizmo_operation m_gizmo_op{gizmo_operation::translate}; // current transform mode (translate/rotate/scale) float m_gizmo_snap{1.0f}; // translation snap step (metres) applied while Ctrl is held + + // path (track) editing, implemented in editormodetrack.cpp + TTrack *selected_track() const; + // settings and parameters of the selected path, drawn in the node bank tab of the track mode + void render_track_ui(); + // curve and point handles of the selected path + void draw_track_overlay() const; + // selects the point handle of the selected path located under the cursor. returns: true if a handle was hit + bool pick_track_handle(); + // selects the path picked in the viewport + void select_track(scene::basic_node *Node); + // gizmo for the selected path: moves the selected point handle, or the whole path + void render_track_gizmo(); + // rebuilds paths modified by the ongoing drag; unless forced, at most a few times per second + void commit_track_drag(bool const Force); + // records provided state of paths in the undo history + void push_track_snapshot(std::vector> States); + // restores state of paths stored in the history, moving their current state to the opposite stack + void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite); + editor_track::point_ref m_track_point; // selected point handle of the selected path + std::vector m_track_drag; // paths modified by the ongoing gizmo drag + std::vector> m_track_drag_points; // end points of neighbours dragged along + editor_track::snap_target m_track_snap; // connection made at the end of the ongoing drag + glm::dvec3 m_track_pivot{0.0}; // rotation pivot of the ongoing drag + glm::mat4 m_track_gizmo{1.0f}; // gizmo matrix, kept for the duration of a drag + bool m_track_gizmo_using{false}; + bool m_track_dirty{false}; // drag changes waiting for rebuild + std::chrono::steady_clock::time_point m_track_last_commit; + editor_track::state m_track_field_before; // state of the path before the ongoing field edit + std::array, 3> m_track_materials{}; // material names being edited + float m_track_snap_radius{1.0f}; + bool m_track_align_tangent{true}; + bool m_track_drag_connected{true}; }; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp new file mode 100644 index 00000000..ab283129 --- /dev/null +++ b/application/editormodetrack.cpp @@ -0,0 +1,613 @@ +/* +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/. +*/ + +// path (track) editing part of the editor mode + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include + +namespace +{ + +// projects world points onto the screen, with the same camera-relative view and clean perspective the gizmo uses +class screen_projection +{ + public: + screen_projection() + { + ImGuiIO const &io = ImGui::GetIO(); + m_size = io.DisplaySize; + m_camera = GfxRenderer->Camera_Position(); + float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor); + float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; + m_viewprojection = glm::perspective(fovy, aspect, 0.1f, 10000.0f) * GfxRenderer->Camera_View_Matrix(); + } + + glm::vec4 clip(glm::dvec3 const &Point) const + { + return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); + } + ImVec2 screen(glm::vec4 const &Clip) const + { + return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); + } + // returns: true if the point is in front of the camera + bool project(glm::dvec3 const &Point, ImVec2 &Screen) const + { + auto const c = clip(Point); + if (c.w < kNear) + return false; + Screen = screen(c); + return true; + } + // draws a line, clipped against the near plane so points behind the camera don't flip across the screen + void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const + { + glm::vec4 a = clip(A); + glm::vec4 b = clip(B); + if (a.w < kNear && b.w < kNear) + return; + if (a.w < kNear) + a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); + else if (b.w < kNear) + b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); + Drawlist->AddLine(screen(a), screen(b), Color, Thickness); + } + + private: + static constexpr float kNear{0.1f}; + ImVec2 m_size; + glm::dvec3 m_camera; + glm::mat4 m_viewprojection; +}; + +// point on the course of a path, matching the curve TSegment builds from the same data +glm::dvec3 path_point(segment_data const &Path, double const T) +{ + auto const &p0 = Path.points[segment_data::point::start]; + auto const &p3 = Path.points[segment_data::point::end]; + auto const &cp1 = Path.points[segment_data::point::control1]; + auto const &cp2 = Path.points[segment_data::point::control2]; + if (cp1 == glm::dvec3{} && cp2 == glm::dvec3{}) + return glm::mix(p0, p3, T); + auto const p1 = p0 + cp1; + auto const p2 = p3 + cp2; + double const u = 1.0 - T; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; +} + +char const *point_label(editor_track::point_kind const Kind) +{ + switch (Kind) + { + case editor_track::point_kind::start: return "Start"; + case editor_track::point_kind::control1: return "Control 1"; + case editor_track::point_kind::control2: return "Control 2"; + default: return "End"; + } +} + +bool is_end(editor_track::point_kind const Kind) +{ + return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::end; +} + +editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, editor_track::point_kind::control1, editor_track::point_kind::control2, editor_track::point_kind::end}; + +// screen distance within which a click grabs a point handle +float const kHandleRadius{10.0f}; + +} // namespace + +TTrack *editor_mode::selected_track() const +{ + return dynamic_cast(m_node); +} + +void editor_mode::select_track(scene::basic_node *Node) +{ + auto *track = dynamic_cast(Node); + if (track == nullptr) + { + // a click beside the selected path releases its point handle + m_track_point = {}; + return; + } + // paths are long, so the distance is measured to the nearest point of the path rather than its centre + glm::dvec3 const camera{Global.pCamera.Pos}; + if (glm::distance(glm::dvec3(track->get_nearest_point(camera)), camera) > static_cast(kMaxPlacementDistance)) + return; + + if (track != m_node) + m_track_point = {}; + m_node = track; + ui()->set_node(m_node); +} + +bool editor_mode::pick_track_handle() +{ + auto *track = selected_track(); + if (track == nullptr || false == editor_track::is_supported(*track)) + return false; + + screen_projection const projection; + ImVec2 const mouse = ImGui::GetIO().MousePos; + float best = kHandleRadius * kHandleRadius; + editor_track::point_ref hit; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + for (auto const kind : kPointKinds) + { + editor_track::point_ref const point{i, kind}; + ImVec2 screen; + if (false == projection.project(editor_track::point_position(*track, point), screen)) + continue; + float const dx = screen.x - mouse.x; + float const dy = screen.y - mouse.y; + // end points take precedence over control points placed at the same spot + float const distance = (dx * dx + dy * dy) * (is_end(kind) ? 0.5f : 1.0f); + if (distance < best) + { + best = distance; + hit = point; + } + } + } + if (false == hit.valid()) + return false; + m_track_point = hit; + return true; +} + +void editor_mode::draw_track_overlay() const +{ + auto const *track = selected_track(); + if (track == nullptr) + return; + + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + bool const trackmode = ui()->mode() == nodebank_panel::TRACK; + + // course of the path, from its source data so it follows the edits before the path is rebuilt + ImU32 const coursecolor = IM_COL32(40, 220, 255, 220); + int constexpr samples{32}; + for (auto const &path : track->m_paths) + { + auto previous = path_point(path, 0.0); + for (int i = 1; i <= samples; ++i) + { + auto const next = path_point(path, static_cast(i) / samples); + projection.line(drawlist, previous, next, coursecolor, 2.5f); + previous = next; + } + } + if (false == trackmode || false == editor_track::is_supported(*track)) + return; + + // control vectors + ImU32 const vectorcolor = IM_COL32(200, 200, 200, 160); + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::start}), editor_track::point_position(*track, {i, editor_track::point_kind::control1}), vectorcolor, 1.0f); + projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::end}), editor_track::point_position(*track, {i, editor_track::point_kind::control2}), vectorcolor, 1.0f); + } + // handles: end points coloured by connection state (green: connected, red: free), control points as squares + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + for (auto const kind : kPointKinds) + { + editor_track::point_ref const point{i, kind}; + ImVec2 screen; + if (false == projection.project(editor_track::point_position(*track, point), screen)) + continue; + bool const selected = (point == m_track_point); + if (is_end(kind)) + { + ImU32 const color = editor_track::is_connected(*track, point) ? IM_COL32(60, 220, 60, 255) : IM_COL32(240, 60, 60, 255); + drawlist->AddCircleFilled(screen, 6.0f, color); + } + else + { + drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 4.0f), ImVec2(screen.x + 4.0f, screen.y + 4.0f), IM_COL32(230, 230, 230, 255)); + } + if (selected) + drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 220, 40, 255), 16, 2.5f); + } + } + // connection to be made when the ongoing drag ends + if (m_track_snap.track != nullptr) + { + ImVec2 screen; + if (projection.project(m_track_snap.position, screen)) + drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); + projection.line(drawlist, m_track_snap.position, m_track_snap.position + m_track_snap.direction * 5.0, IM_COL32(255, 60, 255, 200), 2.0f); + } +} + +void editor_mode::render_track_gizmo() +{ + auto *track = selected_track(); + std::string reason; + if (track == nullptr || false == editor_track::can_edit_geometry(*track, reason)) + { + m_track_gizmo_using = false; + return; + } + bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size()); + + ImGuizmo::BeginFrame(); + ImGuizmo::SetOrthographic(false); + ImGuiIO const &io = ImGui::GetIO(); + ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); + + // same camera-relative view and clean perspective as the transform gizmo of the other nodes + glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); + glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); + float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor); + float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; + glm::mat4 const projection = glm::perspective(fovy, aspect, 0.1f, 10000.0f); + + // point handles only move; the whole path moves or turns around the vertical axis + ImGuizmo::OPERATION const operation = (false == pointmode && m_gizmo_op == gizmo_operation::rotate) ? ImGuizmo::ROTATE_Y : ImGuizmo::TRANSLATE; + + if (false == m_track_gizmo_using) + { + auto const anchor = pointmode ? editor_track::point_position(*track, m_track_point) : editor_track::pivot(*track); + m_track_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - camerapos)); + } + + glm::vec3 snapvalue(operation == ImGuizmo::ROTATE_Y ? 5.0f : m_gizmo_snap); + float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; + glm::mat4 delta(1.0f); + ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), operation, ImGuizmo::WORLD, glm::value_ptr(m_track_gizmo), glm::value_ptr(delta), snap); + + if (ImGuizmo::IsUsing()) + { + if (false == m_track_gizmo_using) + { + // drag start: gather the paths it modifies and record their state for undo + m_track_gizmo_using = true; + m_track_pivot = editor_track::pivot(*track); + m_track_drag = {track}; + m_track_drag_points.clear(); + m_track_snap = {}; + if (pointmode && is_end(m_track_point.kind) && m_track_drag_connected) + { + for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) + { + std::string neighbourreason; + if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason)) + continue; + m_track_drag_points.emplace_back(connection); + if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) + m_track_drag.emplace_back(connection.first); + } + } + std::vector> states; + for (auto *dragged : m_track_drag) + states.emplace_back(dragged, editor_track::capture(*dragged)); + push_track_snapshot(std::move(states)); + } + + if (operation == ImGuizmo::ROTATE_Y) + { + double const angle = std::atan2(delta[2][0], delta[0][0]); + if (std::abs(angle) > 1e-9) + { + editor_track::rotate_y(*track, m_track_pivot, angle); + m_track_dirty = true; + } + } + else if (pointmode) + { + glm::dvec3 const position = camerapos + glm::dvec3(m_track_gizmo[3]); + editor_track::move_point(*track, m_track_point, position); + for (auto const &connection : m_track_drag_points) + editor_track::move_point(*connection.first, connection.second, position); + m_track_dirty = true; + // a free end point can be connected with another path; a joint moved as a whole stays as it is + if (is_end(m_track_point.kind) && m_track_drag_points.empty()) + { + std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); + m_track_snap = editor_track::find_snap_target(*track, position, m_track_snap_radius, exclude); + } + } + else + { + glm::dvec3 const offset{delta[3]}; + if (glm::length(offset) > 1e-9) + { + editor_track::translate(*track, offset); + m_track_dirty = true; + } + } + commit_track_drag(false); + } + else if (m_track_gizmo_using) + { + // drag end: make the offered connection and rebuild everything for good + if (m_track_snap.track != nullptr && pointmode) + { + editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent); + m_track_dirty = true; + } + m_track_snap = {}; + commit_track_drag(true); + m_track_gizmo_using = false; + m_track_drag.clear(); + m_track_drag_points.clear(); + } +} + +void editor_mode::commit_track_drag(bool const Force) +{ + if (false == m_track_dirty || m_track_drag.empty()) + return; + // each rebuild re-uploads geometry of the affected sections, so it isn't done on every frame of a drag + auto const now = std::chrono::steady_clock::now(); + if (false == Force && now - m_track_last_commit < std::chrono::milliseconds(100)) + return; + editor_track::commit(m_track_drag); + m_track_dirty = false; + m_track_last_commit = now; +} + +void editor_mode::push_track_snapshot(std::vector> States) +{ + if (States.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; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::TrackEdit; + snap.node_name = track->name(); + snap.node_ptr = track; + snap.position = track->location(); + snap.uuid = track->uuid; + snap.tracks = std::move(States); + m_history.push_back(std::move(snap)); + g_redo.clear(); +} + +void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite) +{ + if (Snapshot.tracks.empty()) + return; + + EditorSnapshot current{Snapshot}; + std::vector tracks; + for (auto &entry : current.tracks) + { + tracks.emplace_back(entry.first); + entry.second = editor_track::capture(*entry.first); + } + Opposite.push_back(std::move(current)); + + for (auto const &entry : Snapshot.tracks) + editor_track::apply(*entry.first, entry.second); + editor_track::commit(tracks); + + m_track_snap = {}; + m_track_dirty = false; + if (m_node != tracks.front()) + m_track_point = {}; + m_node = tracks.front(); + ui()->set_node(m_node); +} + +void editor_mode::render_track_ui() +{ + ImGui::TextDisabled("LMB: select path or point handle Esc: release point\nGizmo moves the selected point, otherwise the whole path (W: turn)"); + auto *track = selected_track(); + if (track == nullptr) + { + ImGui::TextDisabled("No path selected"); + return; + } + + char const *type = track->eType == tt_Normal ? "normal" : track->eType == tt_Switch ? "switch" : track->eType == tt_Cross ? "cross" : track->eType == tt_Table ? "turntable" : track->eType == tt_Tributary ? "tributary" : "unknown"; + char const *category = (track->iCategoryFlag & 1) ? "rail" : (track->iCategoryFlag & 2) ? "road" : (track->iCategoryFlag & 4) ? "river" : "other"; + ImGui::Text("%s (%s %s, %.2f m)", track->name().empty() ? "(noname)" : track->name().c_str(), category, type, track->Length()); + + if (false == editor_track::is_supported(*track)) + { + ImGui::TextDisabled("Editing of this path type isn't supported"); + return; + } + std::string reason; + bool const geometry = editor_track::can_edit_geometry(*track, reason); + if (false == geometry) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "Geometry locked: %s", reason.c_str()); + + auto const before = editor_track::capture(*track); + // records undo state when a field edit starts, and rebuilds the path once it's finished + enum class rebuild { none, parameters, geometry }; + auto const finish_edit = [&](rebuild const Rebuild) { + if (ImGui::IsItemActivated()) + m_track_field_before = before; + if (ImGui::IsItemDeactivatedAfterEdit()) + { + push_track_snapshot({{track, m_track_field_before}}); + switch (Rebuild) + { + case rebuild::geometry: editor_track::commit({track}); break; + case rebuild::parameters: editor_track::commit_parameters(*track); break; + default: track->mark_dirty(); break; + } + } + }; + // applies a change made with a widget which reports it at once, like a combo box + auto const apply_now = [&](rebuild const Rebuild) { + push_track_snapshot({{track, before}}); + if (Rebuild == rebuild::parameters) + editor_track::commit_parameters(*track); + else + track->mark_dirty(); + }; + + if (ImGui::CollapsingHeader("Path parameters", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PushItemWidth(120.0f); + double velocity = editor_track::velocity(*track); + if (ImGui::InputDouble("Velocity (km/h, -1: none)", &velocity, 0.0, 0.0, "%.1f")) + editor_track::velocity(*track, velocity); + finish_edit(rebuild::none); + if (ImGui::InputFloat("Friction", &track->fFriction, 0.0f, 0.0f, "%.3f")) + track->fFriction = std::max(0.0f, track->fFriction); + finish_edit(rebuild::none); + ImGui::InputFloat("Sound distance", &track->fSoundDistance, 0.0f, 0.0f, "%.1f"); + finish_edit(rebuild::none); + ImGui::InputInt("Quality", &track->iQualityFlag, 0); + finish_edit(rebuild::none); + int damage = track->iDamageFlag; + if (ImGui::InputInt("Damage flags", &damage, 0)) + editor_track::damage(*track, damage); + finish_edit(rebuild::none); + + char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + int environment = std::clamp(static_cast(track->eEnvironment), 0, 5); + if (ImGui::Combo("Environment", &environment, environments, IM_ARRAYSIZE(environments))) + { + track->eEnvironment = static_cast(environment); + apply_now(rebuild::none); + } + + if (ImGui::InputFloat("Width", &track->fTrackWidth, 0.0f, 0.0f, "%.3f")) + track->fTrackWidth = std::max(0.01f, track->fTrackWidth); + finish_edit(rebuild::parameters); + ImGui::PopItemWidth(); + + if (track->m_visible) + { + // material names are edited in local buffers, refreshed from the path whenever no field is being edited + material_handle *materials[] = {&track->m_material1, &track->m_material2, track->SwitchExtension ? &track->SwitchExtension->m_material3 : nullptr}; + char const *labels[] = {"Texture 1 (rails/surface)", "Texture 2 (trackbed/side)", "Trackbed (switch)"}; + if (false == ImGui::IsAnyItemActive()) + { + for (int i = 0; i < 3; ++i) + { + auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{}; + std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1); + m_track_materials[i].back() = '\0'; + } + } + for (int i = 0; i < 3; ++i) + { + if (materials[i] == nullptr || (i == 2 && track->eType != tt_Switch)) + continue; + ImGui::PushItemWidth(-160.0f); + if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size(), ImGuiInputTextFlags_EnterReturnsTrue)) + { + *materials[i] = editor_track::fetch_material(m_track_materials[i].data()); + apply_now(rebuild::parameters); + } + ImGui::PopItemWidth(); + } + ImGui::TextDisabled("Enter applies a texture name, \"none\" removes the texture"); + + ImGui::PushItemWidth(120.0f); + if (ImGui::InputFloat("Texture length", &track->fTexLength, 0.0f, 0.0f, "%.2f")) + track->fTexLength = std::max(0.01f, track->fTexLength); + finish_edit(rebuild::parameters); + float height = editor_track::texture_height(*track); + if (ImGui::InputFloat("Trackbed height", &height, 0.0f, 0.0f, "%.3f")) + editor_track::texture_height(*track, height); + finish_edit(rebuild::parameters); + ImGui::InputFloat("Trackbed width", &track->fTexWidth, 0.0f, 0.0f, "%.3f"); + finish_edit(rebuild::parameters); + ImGui::InputFloat("Trackbed slope", &track->fTexSlope, 0.0f, 0.0f, "%.3f"); + finish_edit(rebuild::parameters); + ImGui::PopItemWidth(); + } + } + + if (ImGui::CollapsingHeader("Path geometry", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::PushItemWidth(120.0f); + ImGui::InputFloat("Snap radius (m)", &m_track_snap_radius, 0.0f, 0.0f, "%.2f"); + m_track_snap_radius = std::max(0.0f, m_track_snap_radius); + ImGui::PopItemWidth(); + ImGui::Checkbox("Align with snapped path", &m_track_align_tangent); + ImGui::SameLine(); + ImGui::Checkbox("Drag connected ends", &m_track_drag_connected); + + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + ImGui::PushID(i); + if (track->m_paths.size() > 1) + ImGui::Text("Path %d", i + 1); + // point selection, same as clicking the handle in the viewport + for (auto const kind : kPointKinds) + { + editor_track::point_ref const point{i, kind}; + if (kind != editor_track::point_kind::start) + ImGui::SameLine(); + std::string label = point_label(kind); + if (is_end(kind)) + label += editor_track::is_connected(*track, point) ? " (+)" : " (-)"; + if (ImGui::Selectable(label.c_str(), m_track_point == point, 0, ImVec2(90.0f, 0.0f))) + m_track_point = (m_track_point == point ? editor_track::point_ref{} : point); + } + + auto &path = track->m_paths[i]; + if (geometry) + { + ImGui::PushItemWidth(-160.0f); + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + auto position = path.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end]; + if (ImGui::InputScalarN(point_label(kind), ImGuiDataType_Double, glm::value_ptr(position), 3, nullptr, nullptr, "%.3f")) + editor_track::move_point(*track, {i, kind}, position); // keeps the common start of a switch together + finish_edit(rebuild::geometry); + } + ImGui::InputScalarN("Control 1 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control1]), 3, nullptr, nullptr, "%.3f"); + finish_edit(rebuild::geometry); + ImGui::InputScalarN("Control 2 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control2]), 3, nullptr, nullptr, "%.3f"); + finish_edit(rebuild::geometry); + ImGui::InputFloat2("Roll (deg)", path.rolls.data(), "%.2f"); + finish_edit(rebuild::geometry); + ImGui::InputFloat("Radius", &path.radius, 0.0f, 0.0f, "%.1f"); + finish_edit(rebuild::geometry); + ImGui::PopItemWidth(); + if (ImGui::Button("Straighten")) + { + push_track_snapshot({{track, before}}); + path.points[segment_data::point::control1] = glm::dvec3{}; + path.points[segment_data::point::control2] = glm::dvec3{}; + path.radius = 0.0f; + editor_track::commit({track}); + } + } + else + { + auto const &start = path.points[segment_data::point::start]; + auto const &end = path.points[segment_data::point::end]; + ImGui::TextDisabled("Start (%.2f, %.2f, %.2f) End (%.2f, %.2f, %.2f)", start.x, start.y, start.z, end.x, end.y, end.z); + } + ImGui::PopID(); + } + } +} diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 42239690..aaef5783 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -85,6 +85,10 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) if (m_filloptions) m_filloptions(); break; + case nodebank_panel::TRACK: + if (m_trackoptions) + m_trackoptions(); + break; default: break; } diff --git a/application/editoruilayer.h b/application/editoruilayer.h index bc9edbe1..28773754 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -50,6 +50,11 @@ class editor_ui : public ui_layer { m_filloptions = std::move(Renderer); } + // draws the path editing settings in its node bank tab (the editing state lives in the editor mode) + void set_track_options(std::function Renderer) + { + m_trackoptions = std::move(Renderer); + } // draws the gizmo settings in the toolset window (the gizmo state lives in the editor mode) void set_gizmo_options(std::function Renderer) { @@ -77,6 +82,7 @@ class editor_ui : public ui_layer bool m_insertrandom{false}; // insert mode picks a random template from m_insertset model_set_ref m_insertset; std::function m_filloptions; + std::function m_trackoptions; std::function m_gizmooptions; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any }; diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 4e9d423e..09bfa683 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -542,7 +542,7 @@ void nodebank_panel::render() } // edit modes as tabs, each with its own settings - std::pair const modes[] = {{"Select", MODIFY}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}}; + std::pair const modes[] = {{"Select", MODIFY}, {"Track", TRACK}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}}; if (ImGui::BeginTabBar("##editmodes", ImGuiTabBarFlags_FittingPolicyResizeDown)) { for (auto const &tab : modes) diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 9d48ccce..d86ec06e 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -122,7 +122,8 @@ class nodebank_panel : public ui_panel COPY, ADD, BRUSH, - FILL + FILL, + TRACK }; edit_mode mode = MODIFY; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp new file mode 100644 index 00000000..5a127833 --- /dev/null +++ b/editor/editorTrack.cpp @@ -0,0 +1,645 @@ +/* +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 "editor/editorTrack.hpp" + +#include "world/Track.h" +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +#include +#include +#include + +namespace +{ + +// end points closer than this are treated as the same point +double const kSamePoint{0.05}; + +segment_data::point point_index(editor_track::point_kind const Kind) +{ + switch (Kind) + { + case editor_track::point_kind::start: return segment_data::point::start; + case editor_track::point_kind::control1: return segment_data::point::control1; + case editor_track::point_kind::control2: return segment_data::point::control2; + default: return segment_data::point::end; + } +} + +bool is_end(editor_track::point_kind const Kind) +{ + return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::end; +} + +bool has_switch_paths(TTrack const &Track) +{ + return Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross); +} + +void add_unique(std::vector &Tracks, TTrack *Track) +{ + if (Track != nullptr && std::find(Tracks.begin(), Tracks.end(), Track) == Tracks.end()) + Tracks.emplace_back(Track); +} + +} // namespace + +editor_track::state editor_track::capture(TTrack const &Track) +{ + state result; + result.paths = Track.m_paths; + result.velocity = Track.fVelocity; + result.switchvelocity = Track.SwitchExtension ? Track.SwitchExtension->fVelocity : -1.f; + result.width = Track.fTrackWidth; + result.friction = Track.fFriction; + result.sounddistance = Track.fSoundDistance; + result.quality = Track.iQualityFlag; + result.damage = Track.iDamageFlag; + result.environment = static_cast(Track.eEnvironment); + result.material1 = Track.m_material1; + result.material2 = Track.m_material2; + result.material3 = Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle; + result.texlength = Track.fTexLength; + result.texheight = texture_height(Track); + result.texwidth = Track.fTexWidth; + result.texslope = Track.fTexSlope; + return result; +} + +void editor_track::apply(TTrack &Track, state const &State) +{ + Track.m_paths = State.paths; + Track.fVelocity = State.velocity; + if (Track.SwitchExtension) + { + Track.SwitchExtension->fVelocity = State.switchvelocity; + Track.SwitchExtension->m_material3 = State.material3; + } + Track.fTrackWidth = State.width; + Track.fTrackWidth2 = State.width; + Track.fFriction = State.friction; + Track.fSoundDistance = State.sounddistance; + Track.iQualityFlag = State.quality; + damage(Track, State.damage); + Track.eEnvironment = static_cast(State.environment); + Track.m_material1 = State.material1; + Track.m_material2 = State.material2; + Track.fTexLength = State.texlength; + texture_height(Track, State.texheight); + Track.fTexWidth = State.texwidth; + Track.fTexSlope = State.texslope; +} + +bool editor_track::is_supported(TTrack const &Track) +{ + if ((Track.iCategoryFlag & 0x80) != 0) + return false; // auto-generated helper path + if (Track.eType == tt_Normal) + return Track.Segment != nullptr; + if (Track.eType == tt_Switch) + return Track.SwitchExtension != nullptr && Track.m_paths.size() >= 2; + return false; +} + +bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason) +{ + if (false == is_supported(Track)) + { + Reason = "Editing of this path type isn't supported"; + return false; + } + if (Global.NvRenderer) + { + 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; +} + +void editor_track::commit(std::vector const &Tracks) +{ + if (Tracks.empty()) + return; + + // paths whose render geometry depends on the modified ones + std::vector affected; + for (auto *track : Tracks) + { + add_unique(affected, track); + for (auto *neighbour : neighbours(*track)) + add_unique(affected, neighbour); + } + // take the paths out of the scene while their ends still match the old segments... + for (auto *track : Tracks) + { + disconnect(*track); + simulation::Region->erase_and_unregister(track); + } + // ...rebuild the segments and put the paths back in their new place... + for (auto *track : Tracks) + { + track->init_segments(false); + track->update_location(); + track->m_area.radius = -1.f; // forces radius recalculation + simulation::Region->insert_and_register(track); + } + // ...connect them with whatever they touch now... + for (auto *track : Tracks) + join(*track); + for (auto *track : Tracks) + for (auto *neighbour : neighbours(*track)) + add_unique(affected, neighbour); + // ...and update the visuals + for (auto *track : affected) + update_transition(*track); + for (auto *track : affected) + rebuild_geometry(*track); + for (auto *track : Tracks) + track->mark_dirty(); +} + +void editor_track::commit_parameters(TTrack &Track) +{ + Track.fTrackWidth2 = Track.fTrackWidth; + // segments are re-created from unchanged data, but road texture proportions and roll fix depend on parameters + Track.init_segments(false); + + std::vector affected{&Track}; + for (auto *neighbour : neighbours(Track)) + add_unique(affected, neighbour); + for (auto *track : affected) + update_transition(*track); + for (auto *track : affected) + rebuild_geometry(*track); + Track.mark_dirty(); +} + +glm::dvec3 editor_track::point_position(TTrack const &Track, point_ref const &Point) +{ + if (Point.path < 0 || Point.path >= static_cast(Track.m_paths.size())) + return Track.location(); + + auto const &path{Track.m_paths[Point.path]}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + switch (Point.kind) + { + case point_kind::start: return start; + case point_kind::end: return end; + case point_kind::control1: + { + auto const &control{path.points[segment_data::point::control1]}; + return control == glm::dvec3{} ? start + (end - start) / 3.0 : start + control; + } + case point_kind::control2: + { + auto const &control{path.points[segment_data::point::control2]}; + return control == glm::dvec3{} ? end + (start - end) / 3.0 : end + control; + } + } + return start; +} + +void editor_track::move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position) +{ + if (Point.path < 0 || Point.path >= static_cast(Track.m_paths.size())) + return; + + auto &path{Track.m_paths[Point.path]}; + switch (Point.kind) + { + case point_kind::start: + case point_kind::end: + { + auto &point{path.points[point_index(Point.kind)]}; + auto const oldposition{point}; + point = Position; + // control vectors are relative to their end points, so they follow them automatically. + // other paths of the same piece sharing this point (the common start of a switch) follow as well + for (auto &otherpath : Track.m_paths) + { + if (&otherpath == &path) + continue; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + if (glm::distance(otherpath.points[index], oldposition) < kSamePoint) + otherpath.points[index] = Position; + } + break; + } + case point_kind::control1: + { + auto control{Position - path.points[segment_data::point::start]}; + path.points[segment_data::point::control1] = (glm::length(control) < 0.01 ? glm::dvec3{} : control); + break; + } + case point_kind::control2: + { + auto control{Position - path.points[segment_data::point::end]}; + path.points[segment_data::point::control2] = (glm::length(control) < 0.01 ? glm::dvec3{} : control); + break; + } + } +} + +void editor_track::translate(TTrack &Track, glm::dvec3 const &Offset) +{ + for (auto &path : Track.m_paths) + { + path.points[segment_data::point::start] += Offset; + path.points[segment_data::point::end] += Offset; + } +} + +void editor_track::rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle) +{ + auto const c{std::cos(Angle)}; + auto const s{std::sin(Angle)}; + auto const rotate = [&](glm::dvec3 const &Vector) { return glm::dvec3{Vector.x * c + Vector.z * s, Vector.y, -Vector.x * s + Vector.z * c}; }; + + for (auto &path : Track.m_paths) + { + for (auto const index : {segment_data::point::start, segment_data::point::end}) + path.points[index] = Pivot + rotate(path.points[index] - Pivot); + for (auto const index : {segment_data::point::control1, segment_data::point::control2}) + path.points[index] = rotate(path.points[index]); + } +} + +glm::dvec3 editor_track::pivot(TTrack const &Track) +{ + return Track.location(); +} + +std::vector editor_track::neighbours(TTrack const &Track) +{ + std::vector result; + add_unique(result, Track.trPrev); + add_unique(result, Track.trNext); + if (has_switch_paths(Track)) + { + for (int i = 0; i < 2; ++i) + { + add_unique(result, Track.SwitchExtension->pPrevs[i]); + add_unique(result, Track.SwitchExtension->pNexts[i]); + } + } + result.erase(std::remove(result.begin(), result.end(), &Track), result.end()); + return result; +} + +std::vector> editor_track::connected_points(TTrack const &Track, glm::dvec3 const &Position) +{ + std::vector> result; + for (auto *neighbour : neighbours(Track)) + { + for (int i = 0; i < static_cast(neighbour->m_paths.size()); ++i) + { + for (auto const kind : {point_kind::start, point_kind::end}) + { + if (glm::distance(neighbour->m_paths[i].points[point_index(kind)], Position) < kSamePoint) + result.emplace_back(neighbour, point_ref{i, kind}); + } + } + } + return result; +} + +bool editor_track::is_connected(TTrack const &Track, point_ref const &Point) +{ + if (false == is_end(Point.kind)) + return false; + return false == connected_points(Track, point_position(Track, Point)).empty(); +} + +editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude) +{ + snap_target result; + result.distance = std::numeric_limits::max(); + + auto const excluded = [&](TTrack const *Other) { return std::find(Exclude.begin(), Exclude.end(), Other) != Exclude.end(); }; + auto const category{Track.iCategoryFlag & 15}; + // NOTE: copy, the region returns its scratchpad + auto const sections{simulation::Region->sections(Position, static_cast(Radius))}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *other : cell.m_directories.paths) + { + if (other == &Track || excluded(other) || (other->iCategoryFlag & 15) != category || false == is_supported(*other)) + continue; + for (int i = 0; i < static_cast(other->m_paths.size()); ++i) + { + auto const &path{other->m_paths[i]}; + for (auto const kind : {point_kind::start, point_kind::end}) + { + auto const &point{path.points[point_index(kind)]}; + auto const distance{glm::distance(point, Position)}; + if (distance > Radius || distance >= result.distance) + continue; + // skip ends already connected with something which stays in place + auto const connections{connected_points(*other, point)}; + if (std::any_of(connections.begin(), connections.end(), [&](auto const &Connection) { return false == excluded(Connection.first); })) + continue; + + glm::dvec3 direction; + if (kind == point_kind::start) + { + auto const &control{path.points[segment_data::point::control1]}; + direction = control != glm::dvec3{} ? control : path.points[segment_data::point::end] - point; + } + else + { + auto const &control{path.points[segment_data::point::control2]}; + direction = control != glm::dvec3{} ? control : path.points[segment_data::point::start] - point; + } + if (glm::length(direction) < 1e-6) + continue; + + result.track = other; + result.point = point_ref{i, kind}; + result.position = point; + result.direction = glm::normalize(direction); + result.distance = distance; + } + } + } + } + } + return result; +} + +void editor_track::snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent) +{ + if (Target.track == nullptr || false == is_end(Point.kind)) + return; + + move_point(Track, Point, Target.position); + if (false == Aligntangent) + return; + + // the path continues the target, so it leaves the shared point in the opposite direction + auto &path{Track.m_paths[Point.path]}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto &control{path.points[Point.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2]}; + auto const length{control != glm::dvec3{} ? glm::length(control) : glm::distance(start, end) / 3.0}; + control = -Target.direction * length; +} + +std::string editor_track::material_name(material_handle const Material) +{ + if (Material == null_handle) + return "none"; + auto name{GfxRenderer->Material(Material)->GetName()}; + if (name.find(paths::textures) == 0) + name.erase(0, std::string{paths::textures}.size()); + return name; +} + +material_handle editor_track::fetch_material(std::string const &Name) +{ + if (Name.empty() || Name == "none") + return null_handle; + auto name{Name}; + replace_slashes(name); + return GfxRenderer->Fetch_Material(name); +} + +float editor_track::texture_height(TTrack const &Track) +{ + return (Track.fTexHeight1 - Track.fTexHeightOffset) * (Track.iCategoryFlag & 4 ? -1.f : 1.f); +} + +void editor_track::texture_height(TTrack &Track, float const Height) +{ + Track.fTexHeight1 = Height * (Track.iCategoryFlag & 4 ? -1.f : 1.f) + Track.fTexHeightOffset; +} + +double editor_track::velocity(TTrack const &Track) +{ + if (Track.SwitchExtension && Track.eType == tt_Switch) + return Track.SwitchExtension->fVelocity; + return Track.fVelocity; +} + +void editor_track::velocity(TTrack &Track, double const Velocity) +{ + if (Track.SwitchExtension && Track.eType == tt_Switch) + { + // same rules as TTrack::Switch() + Track.SwitchExtension->fVelocity = static_cast(Velocity); + if (Velocity <= -2.0) + Track.fVelocity = Track.SwitchExtension->CurrentIndex ? -Velocity : -1.0; + else + Track.fVelocity = Velocity; + return; + } + Track.fVelocity = Velocity; +} + +void editor_track::damage(TTrack &Track, int const Damage) +{ + Track.iDamageFlag = Damage; + if (Damage & 128) + Track.iAction |= 0x80; + else + Track.iAction &= ~0x80; +} + +void editor_track::disconnect(TTrack &Track) +{ + for (auto *neighbour : neighbours(Track)) + { + if (has_switch_paths(*neighbour)) + { + auto &extension{*neighbour->SwitchExtension}; + for (int i = 0; i < 2; ++i) + { + if (extension.pPrevs[i] == &Track) + extension.pPrevs[i] = nullptr; + if (extension.pNexts[i] == &Track) + extension.pNexts[i] = nullptr; + } + bind_switch_path(*neighbour, extension.CurrentIndex); + } + else + { + if (neighbour->trPrev == &Track) + neighbour->trPrev = nullptr; + if (neighbour->trNext == &Track) + neighbour->trNext = nullptr; + } + } + Track.trPrev = nullptr; + Track.trNext = nullptr; + if (has_switch_paths(Track)) + { + auto &extension{*Track.SwitchExtension}; + for (int i = 0; i < 2; ++i) + { + extension.pPrevs[i] = nullptr; + extension.pNexts[i] = nullptr; + } + } +} + +void editor_track::join(TTrack &Track) +{ + TTrack *other; + int endpointid; + switch (Track.eType) + { + case tt_Normal: + { + if (Track.trPrev == nullptr) + { + std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_0(), &Track); + connect(Track, true, other, endpointid); + } + if (Track.trNext == nullptr) + { + std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_1(), &Track); + connect(Track, false, other, endpointid); + } + break; + } + case tt_Switch: + { + // like during scenery load, switches are connected from the side of the regular paths + for (int i = 0; i < 2; ++i) + { + auto const &segment{Track.SwitchExtension->Segments[i]}; + for (auto const &point : {segment->FastGetPoint_0(), segment->FastGetPoint_1()}) + { + std::tie(other, endpointid) = simulation::Region->find_path(point, &Track); + if (other != nullptr && other->eType == tt_Normal) + join(*other); + } + } + break; + } + default: + break; + } +} + +void editor_track::connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid) +{ + if (Other == nullptr) + return; + + switch (Endpointid) + { + case 0: + if (Prevside) + Track.ConnectPrevPrev(Other, 0); + else + Track.ConnectNextPrev(Other, 0); + break; + case 1: + if (Prevside) + Track.ConnectPrevNext(Other, 1); + else + Track.ConnectNextNext(Other, 1); + break; + case 2: + case 3: + case 4: + case 5: + { + if (false == has_switch_paths(*Other)) + break; + // same connections as in path_table::InitTracks(), but made without moving the switch blades + auto const path{(Endpointid - 2) / 2}; + auto const atend{((Endpointid - 2) % 2) != 0}; + bind_switch_path(*Other, path); + if (Prevside) + { + if (atend) + Track.ConnectPrevNext(Other, path ? 3 : 1); + else + Track.ConnectPrevPrev(Other, path ? 2 : 0); + } + else + { + if (atend) + Track.ConnectNextNext(Other, path ? 3 : 1); + else + Track.ConnectNextPrev(Other, path ? 2 : 0); + } + store_switch_path(*Other, path); + bind_switch_path(*Other, Other->SwitchExtension->CurrentIndex); + break; + } + default: + break; + } +} + +void editor_track::update_transition(TTrack &Track) +{ + // same rule as TTrack::ConnectNextPrev(): transition is drawn by the path whose end 2 meets start of a different path + Track.iTrapezoid &= ~2; + auto const *next{Track.trNext}; + if (next == nullptr || Track.iNextDirection != 0) + return; + if (Track.m_visible && next->m_visible && Track.eType == tt_Normal && next->eType == tt_Normal) + { + if (Track.fTrackWidth != next->fTrackWidth || Track.fTexHeight1 != next->fTexHeight1 || Track.fTexWidth != next->fTexWidth || Track.fTexSlope != next->fTexSlope) + Track.iTrapezoid |= 2; + } +} + +void editor_track::bind_switch_path(TTrack &Switch, int const Path) +{ + auto &extension{*Switch.SwitchExtension}; + auto const path{Path & 1}; + Switch.Segment = extension.Segments[path]; + Switch.trNext = extension.pNexts[path]; + Switch.trPrev = extension.pPrevs[path]; + Switch.iNextDirection = extension.iNextDirection[path]; + Switch.iPrevDirection = extension.iPrevDirection[path]; +} + +void editor_track::store_switch_path(TTrack &Switch, int const Path) +{ + // same as TTrack::SetConnections(), without switching back to the base position + auto &extension{*Switch.SwitchExtension}; + auto const path{Path & 1}; + extension.pNexts[path] = Switch.trNext; + extension.pPrevs[path] = Switch.trPrev; + extension.iNextDirection[path] = Switch.iNextDirection; + extension.iPrevDirection[path] = Switch.iPrevDirection; + if (Switch.eType == tt_Switch) + { + // switch paths share their start point, so they share the path connected there as well + extension.pPrevs[path ^ 1] = Switch.trPrev; + extension.iPrevDirection[path ^ 1] = Switch.iPrevDirection; + } +} + +void editor_track::rebuild_geometry(TTrack &Track) +{ + auto §ion{simulation::Region->section(Track.location())}; + // sections which weren't displayed yet create geometry of their content on their own + Track.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{}); +} diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp new file mode 100644 index 00000000..bd39766c --- /dev/null +++ b/editor/editorTrack.hpp @@ -0,0 +1,136 @@ +/* +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/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "world/Segment.h" +#include "utilities/Classes.h" + +class TTrack; + +// Editing of paths (tracks, roads, rivers) loaded from the scenery. +// +// The source of truth for path geometry is TTrack::m_paths (the same data the scenery export writes). +// Edits modify m_paths and then commit() rebuilds everything derived from it: the segments, placement +// in the scene cells, connections with neighbouring paths and the render geometry. +class editor_track +{ + public: + // identifies an editable point of a path + enum class point_kind { start, control1, control2, end }; + struct point_ref + { + int path{-1}; // index into TTrack::m_paths + point_kind kind{point_kind::start}; + bool valid() const { return path >= 0; } + bool operator==(point_ref const &Other) const { return path == Other.path && kind == Other.kind; } + }; + + // snapshot of the editable state of a path, used by the undo history + struct state + { + std::vector paths; + double velocity{-1.0}; + float switchvelocity{-1.f}; + float width{1.435f}; + float friction{0.15f}; + float sounddistance{-1.f}; + int quality{20}; + int damage{0}; + int environment{0}; + material_handle material1{0}; + material_handle material2{0}; + material_handle material3{0}; + float texlength{4.f}; + float texheight{0.6f}; + float texwidth{0.9f}; + float texslope{0.9f}; + }; + static state capture(TTrack const &Track); + // restores provided state; follow with commit() to rebuild the path + static void apply(TTrack &Track, state const &State); + + // checks whether geometry of the path can be modified. returns: true if it can, otherwise false and the reason + static bool can_edit_geometry(TTrack const &Track, std::string &Reason); + // checks whether the path type is supported by the editor at all + static bool is_supported(TTrack const &Track); + + // rebuilds provided paths after modification of their source data (m_paths): segments, placement in the region, + // connections with neighbours and render geometry of the paths and all their old and new neighbours + static void commit(std::vector const &Tracks); + // rebuilds the path after change of parameters which don't affect its course (width, textures, etc) + static void commit_parameters(TTrack &Track); + + // world position of specified point. for control points it's the absolute position of the handle; + // a zero (straight) control vector is presented at 1/3 of the chord so it can be grabbed + static glm::dvec3 point_position(TTrack const &Track, point_ref const &Point); + // moves specified point to provided world position. moving an end point carries its control vector along. + // switches keep the start points of both their paths together + static void move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position); + // moves the whole path by provided offset + static void translate(TTrack &Track, glm::dvec3 const &Offset); + // rotates the whole path around vertical axis going through provided pivot point + static void rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle); + // pivot used for rotation of the whole path + static glm::dvec3 pivot(TTrack const &Track); + + // lists paths currently connected to provided path + static std::vector neighbours(TTrack const &Track); + // lists end points of other paths connected to the end point located at provided position, + // as pairs of the neighbour and its matching point + static std::vector> connected_points(TTrack const &Track, glm::dvec3 const &Position); + // checks whether end point located at provided position is connected to another path + static bool is_connected(TTrack const &Track, point_ref const &Point); + + // candidate to connect a dragged end point with + struct snap_target + { + TTrack *track{nullptr}; + point_ref point; + glm::dvec3 position{0.0}; + glm::dvec3 direction{0.0}; // direction from the point into the target path + double distance{0.0}; + }; + // finds the nearest free end point of another path of the same category as provided path, within specified radius. + // paths listed in Exclude are skipped, and connections to them don't count as occupying an end point + static snap_target find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); + // places the point at the target and optionally aligns the control vector with the target path + static void snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent); + + // names of the materials used by the path; "none" for missing ones + static std::string material_name(material_handle const Material); + static material_handle fetch_material(std::string const &Name); + + // height of the trackbed edge as written in the scenery (without roll fix adjustment) + static float texture_height(TTrack const &Track); + static void texture_height(TTrack &Track, float const Height); + static double velocity(TTrack const &Track); + static void velocity(TTrack &Track, double const Velocity); + static void damage(TTrack &Track, int const Damage); + + private: + // unlinks the path from all its neighbours + static void disconnect(TTrack &Track); + // links the path with paths whose free end points match its own free end points + static void join(TTrack &Track); + // links specified end of a regular path with matching end of another path + static void connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid); + // recalculates the trackbed transition flag + static void update_transition(TTrack &Track); + // sets active connections of a switch to specified path, without moving the blades + static void bind_switch_path(TTrack &Switch, int const Path); + static void store_switch_path(TTrack &Switch, int const Path); + // rebuilds render geometry of the path in the bank of the section it's placed in + static void rebuild_geometry(TTrack &Track); +}; diff --git a/world/Track.cpp b/world/Track.cpp index 75f0ec6f..3900bd77 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -709,6 +709,8 @@ void TTrack::init_segments( bool const Initial ) { // so repeated initialization doesn't accumulate it fTexHeight1 -= fTexHeightOffset; fTexHeightOffset = 0.f; + // the roll flag is re-evaluated below, the transition flag is set again when the path gets connected + iTrapezoid = 0; switch (eType) { // Ra: łuki segmentowane co 5m albo 314-kątem foremnym @@ -1355,10 +1357,9 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const // wypełnianie tablic VBO // discards references to current render geometry and generates it anew in specified bank // NOTE: geometry banks can't release chunks, so the old data stays in the bank, unused +// with null bank the geometry is only discarded, to be created later along with the rest of the owning section void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { - if( Bank == null_handle ) { return; } - Geometry1.clear(); Geometry2.clear(); if( SwitchExtension ) { @@ -1366,6 +1367,8 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { } m_sleeper_local_transforms.clear(); + if( Bank == null_handle ) { return; } + create_geometry( Bank ); }