From e4083d507f7e7eeec20c327e9029a392eecb9d40 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 3 Oct 2026 18:21:10 +0200 Subject: [PATCH] feat(roads): editor menu and window for building roads, lanes shown as trajectories --- CMakeLists.txt | 2 + application/editormode.cpp | 50 +- application/editormode.h | 42 +- application/editormoderoad.cpp | 839 +++++++++++++++++++++++ application/editoruilayer.cpp | 3 + application/editoruilayer.h | 6 + editor/editorRoad.cpp | 564 +++++++++++++++ editor/editorRoad.hpp | 65 ++ scene/scenelayerssave.cpp | 72 ++ simulation/simulationstateserializer.cpp | 4 +- world/Road.cpp | 569 +++++++++------ world/Road.h | 127 ++-- 12 files changed, 2094 insertions(+), 249 deletions(-) create mode 100644 application/editormoderoad.cpp create mode 100644 editor/editorRoad.cpp create mode 100644 editor/editorRoad.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 58b3e4ca..bdda3ea9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,11 +172,13 @@ set(SOURCES "editor/editorIncludeInfo.cpp" "editor/editorTerrain.cpp" "editor/editorTrack.cpp" +"editor/editorRoad.cpp" "editor/editorAlignment.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" +"application/editormoderoad.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index dd8bafa2..37a779dc 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -340,6 +340,9 @@ editor_mode::editor_mode() { 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(); }); + ui()->set_menu_options([this]() { render_road_menu(); }); + m_roadtool.settings.normalize(); + road_select(nullptr); // 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); @@ -687,6 +690,12 @@ void editor_mode::undo_last() return; } + if (snap.action == EditorSnapshot::Action::RoadEdit) + { + restore_road_snapshot(snap, g_redo, true); + return; + } + if (snap.action == EditorSnapshot::Action::Delete) { // undo delete -> recreate model @@ -782,6 +791,12 @@ void editor_mode::redo_last() return; } + if (snap.action == EditorSnapshot::Action::RoadEdit) + { + restore_road_snapshot(snap, m_history, false); + return; + } + // handle delete redo (re-delete) separately if (snap.action == EditorSnapshot::Action::Delete) { @@ -1023,6 +1038,11 @@ bool editor_mode::update() render_track_window(); } + // roads: trajectories of the lanes, and the tools of the road window + update_road_tool(); + draw_road_overlay(); + render_road_window(); + // --- 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(); @@ -2708,6 +2728,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_DELETE: + if (is_press(Action) && road_delete()) + { + break; + } if (is_press(Action) && m_instance != 0) { // include of a scenery template. what it shows goes out of sight, the directive is erased on save @@ -2751,6 +2775,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_ESCAPE: + if (is_press(Action)) + { + road_cancel(); + } if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) { m_track_point = {}; @@ -2769,6 +2797,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_K: + if (is_press(Action) && road_split()) + { + break; + } if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && selected_track() != nullptr) { auto *track{selected_track()}; @@ -2841,6 +2873,21 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } + // with the road window open the left button works its tools, whichever edit mode is on + if (m_roadtool.window && Button == GLFW_MOUSE_BUTTON_LEFT) + { + if (is_press(Action)) + { + // NOTE: the pick is resolved a few frames later, by then it's known whether the press was meant for the UI + GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) { + if (viewport_click()) + road_click(); + }); + } + m_input.mouse.button(Button, Action); + return; + } + if (Button == GLFW_MOUSE_BUTTON_LEFT) { @@ -3053,7 +3100,8 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::Move ? "MOV" : s.action == EditorSnapshot::Action::Rotate ? "ROT" : s.action == EditorSnapshot::Action::Scale ? "SCA" : - s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : "OTH", + s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : + s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : "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 262b1b1b..f7cfff48 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorOrthophoto.hpp" #include "editor/editorTrack.hpp" #include "editor/editorAlignment.hpp" +#include "editor/editorRoad.hpp" #include #include @@ -90,7 +91,7 @@ class editor_mode : public application_mode struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit }; Action action{Action::Other}; std::string node_name; // node identifier (basic_node::name()) @@ -106,6 +107,9 @@ class editor_mode : public application_mode std::vector> tracks; std::vector created; std::vector removed; + std::vector> roads; // road pieces with what they were like before the change + std::vector roads_created; + std::vector roads_removed; }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -481,4 +485,40 @@ class editor_mode : public application_mode float m_track_snap_radius{1.0f}; bool m_track_align_tangent{true}; bool m_track_drag_connected{true}; + + // roads: a menu and a window of their own, independent from the edit modes of the toolset + struct road_tool + { + bool window{false}; // the road window is open, the left mouse button belongs to its tools + bool lanes{true}; // lanes of the roads are drawn as trajectories + int tool{0}; // 0: select, 1: build + road_node *selected{nullptr}; + road_node::state settings; // layout shown in the window: of the selected piece, or of the pieces about to be built + bool whole{true}; // layout changes go to every piece of the road the selected one belongs to + bool chain{false}; // the point to build from is set + glm::dvec3 point{0.0}; // where the next piece starts + glm::dvec2 direction{0.0, 1.0}; // the way it has to leave that point + bool hasdirection{false}; + bool reversed{false}; // the pieces are laid from their ends, to carry on a road from its start + glm::dvec3 mouse{0.0}; + std::string status; + char surface[128]{}; + char sides[2][128]{}; + char filter[64]{"asph"}; + }; + void render_road_menu(); + void render_road_window(); + bool render_road_layout(road_node::state &State); + void draw_road_overlay() const; + void update_road_tool(); + void road_click(); + void road_select(road_node *Road); + void road_apply(); + bool road_delete(); + bool road_split(); + void road_cancel(); + std::vector road_preview(editor_road::loose_end &Snap, std::string &Error) const; + void push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed); + void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); + road_tool m_roadtool; }; diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp new file mode 100644 index 00000000..27210dc2 --- /dev/null +++ b/application/editormoderoad.cpp @@ -0,0 +1,839 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/Road.h" +#include "simulation/simulation.h" + +#include "imgui/imgui.h" +#include +#include +#include + +namespace +{ + +// the same helper the track tools use to draw over the viewport +class screen_projection +{ + public: + screen_projection() + { + ImGuiIO const &io = ImGui::GetIO(); + m_size = io.DisplaySize; + m_camera = GfxRenderer->Camera_Position(); + float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; + m_viewprojection = editor_mode::projection_matrix(aspect) * 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); + } + 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; + } + 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; +}; + +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{0.0} && cp2 == glm::dvec3{0.0}) + 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; +} + +bool is_curved(segment_data const &Path) +{ + return Path.points[segment_data::point::control1] != glm::dvec3{0.0} || Path.points[segment_data::point::control2] != glm::dvec3{0.0}; +} + +void copy_text(char *Buffer, std::size_t const Size, std::string const &Text) +{ + std::strncpy(Buffer, Text.c_str(), Size - 1); + Buffer[Size - 1] = 0; +} + +std::string lower_case(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return Text; +} + +double const kSnapRadius{4.0}; // an end of a road this close to the cursor is what the click is taken to mean +double const kReach{3000.0}; // clicks landing further than this are taken for misses +double const kLaneRange{600.0}; // lanes are drawn for the roads this close to the camera + +ImU32 const kForwardColor{IM_COL32(70, 230, 110, 230)}; +ImU32 const kBackwardColor{IM_COL32(255, 170, 50, 230)}; +ImU32 const kTurnColor{IM_COL32(210, 210, 210, 150)}; +ImU32 const kSelectedColor{IM_COL32(40, 220, 255, 230)}; +ImU32 const kPreviewColor{IM_COL32(255, 210, 60, 255)}; +ImU32 const kRefusedColor{IM_COL32(240, 60, 60, 255)}; +ImU32 const kSnapColor{IM_COL32(255, 60, 255, 255)}; + +} + +void editor_mode::render_road_menu() +{ + if (ImGui::BeginMenu("Roads")) + { + ImGui::MenuItem("Road editor", nullptr, &m_roadtool.window); + ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes); + ImGui::EndMenu(); + } +} + +void editor_mode::update_road_tool() +{ + auto &tool{m_roadtool}; + if (tool.selected != nullptr && tool.selected->m_editorremoved) + tool.selected = nullptr; + if (false == tool.window) + { + tool.chain = false; + return; + } + tool.mouse = Global.pCamera.Pos + GfxRenderer->Mouse_Position(); +} + +void editor_mode::road_select(road_node *Road) +{ + auto &tool{m_roadtool}; + tool.selected = Road; + if (Road != nullptr) + { + tool.settings = Road->definition(); + std::string reason; + tool.status = "Selected: " + (Road->name().empty() ? std::string{"(unnamed road)"} : Road->name()); + if (false == editor_road::can_edit(*Road, &reason)) + tool.status += ". It can't be changed: " + reason; + } + copy_text(tool.surface, sizeof(tool.surface), tool.settings.surface); + for (int side = 0; side < 2; ++side) + copy_text(tool.sides[side], sizeof(tool.sides[side]), tool.settings.sides[side].material); +} + +void editor_mode::road_cancel() +{ + m_roadtool.chain = false; +} + +std::vector editor_mode::road_preview(editor_road::loose_end &Snap, std::string &Error) const +{ + auto const &tool{m_roadtool}; + Snap = editor_road::find_end(tool.mouse, kSnapRadius); + // the end the road is being drawn from isn't somewhere to lead it to + if (Snap.road != nullptr && glm::distance(Snap.position, tool.point) < 0.5) + Snap = {}; + glm::dvec2 enddirection{0.0, 1.0}; + if (Snap.road != nullptr) + { + glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z}; + if (glm::length(outwards) > 1e-6) + enddirection = -glm::normalize(outwards); + else + Snap = {}; + } + return editor_road::plan(tool.point, tool.hasdirection ? &tool.direction : nullptr, Snap.road != nullptr ? Snap.position : tool.mouse, Snap.road != nullptr ? &enddirection : nullptr, Global.ctrlState, Error); +} + +void editor_mode::road_click() +{ + auto &tool{m_roadtool}; + glm::dvec3 const camera{Global.pCamera.Pos}; + glm::dvec3 const ground{camera + GfxRenderer->Mouse_Position()}; + if (glm::distance(ground, camera) > kReach) + { + tool.status = "The click didn't land on anything near enough"; + return; + } + tool.mouse = ground; + if (tool.tool == 0) + { + auto *road{editor_road::nearest(ground, 1.5)}; + if (road == nullptr) + tool.status = "No road here"; + road_select(road); + return; + } + + std::string reason; + if (false == editor_road::available(&reason)) + { + tool.status = reason; + return; + } + if (false == tool.chain) + { + // first click sets where the road starts. started from a loose end of a road it carries that road on + auto const end{editor_road::find_end(ground, kSnapRadius)}; + glm::dvec2 const outwards{end.outwards.x, end.outwards.z}; + if (end.road != nullptr && glm::length(outwards) > 1e-6) + { + tool.point = end.position; + tool.direction = glm::normalize(outwards); + tool.hasdirection = true; + // carried on from its start the road keeps the direction of its axis, so the lanes stay what they were + tool.reversed = (false == end.atend); + auto const layout{end.road->definition()}; + tool.selected = nullptr; + tool.settings = layout; + road_select(nullptr); + tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; + } + else + { + tool.point = ground; + tool.hasdirection = false; + tool.reversed = false; + tool.selected = nullptr; + tool.status = "Start set"; + } + tool.chain = true; + return; + } + + editor_road::loose_end snap; + std::string error; + auto pieces{road_preview(snap, error)}; + if (pieces.empty()) + { + tool.status = "Can't build this: " + error; + return; + } + // where the next piece is going to carry on from + auto const &last{pieces.back()}; + auto const endpoint{last.points[segment_data::point::end]}; + auto const tangent{last.points[segment_data::point::control2] != glm::dvec3{0.0} ? -last.points[segment_data::point::control2] : last.points[segment_data::point::end] - last.points[segment_data::point::start]}; + if (tool.reversed) + { + for (auto &piece : pieces) + { + std::swap(piece.points[segment_data::point::start], piece.points[segment_data::point::end]); + std::swap(piece.points[segment_data::point::control1], piece.points[segment_data::point::control2]); + } + std::reverse(pieces.begin(), pieces.end()); + } + std::vector states; + for (auto const &piece : pieces) + { + auto state{tool.settings}; + state.axis = piece; + states.emplace_back(state); + } + auto const created{editor_road::create(states)}; + if (created.empty()) + { + tool.status = "The road wasn't created"; + return; + } + push_road_snapshot({}, created, {}); + if (snap.road != nullptr) + { + tool.chain = false; + tool.status = "Joined with \"" + snap.road->name() + "\""; + return; + } + glm::dvec2 const planar{tangent.x, tangent.z}; + tool.point = endpoint; + if (glm::length(planar) > 1e-6) + { + tool.direction = glm::normalize(planar); + tool.hasdirection = true; + } + tool.status = std::to_string(created.size()) + (created.size() == 1 ? " piece added" : " pieces added"); +} + +void editor_mode::road_apply() +{ + auto &tool{m_roadtool}; + if (tool.selected == nullptr) + return; + tool.settings.normalize(); + auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; + std::vector> before; + std::vector> changes; + for (auto *road : pieces) + { + std::string reason; + if (false == editor_road::can_edit(*road, &reason)) + { + tool.status = "\"" + road->name() + "\" can't be changed: " + reason; + // what the window shows goes back to what the road is like + road_select(tool.selected); + tool.status = "\"" + road->name() + "\" can't be changed: " + reason; + return; + } + auto state{tool.settings}; + state.axis = road->definition().axis; + if (road != tool.selected && road->definition().changes.size() == state.changes.size()) + { + // where the lanes can be changed is set for each piece on its own + state.changes = road->definition().changes; + } + before.emplace_back(road, road->definition()); + changes.emplace_back(road, state); + } + editor_road::apply(changes); + push_road_snapshot(before, {}, {}); + tool.settings = tool.selected->definition(); + tool.status = (pieces.size() == 1 ? std::string{"Changed"} : "Changed " + std::to_string(pieces.size()) + " pieces"); +} + +bool editor_mode::road_delete() +{ + auto &tool{m_roadtool}; + if (false == tool.window || tool.selected == nullptr) + return false; + auto *road{tool.selected}; + std::string reason; + if (false == editor_road::can_edit(*road, &reason)) + { + tool.status = "Can't delete: " + reason; + return true; + } + editor_road::remove({road}); + push_road_snapshot({}, {}, {road}); + tool.selected = nullptr; + tool.status = "\"" + road->name() + "\" deleted, Ctrl+Z brings it back"; + return true; +} + +bool editor_mode::road_split() +{ + auto &tool{m_roadtool}; + if (false == tool.window || tool.selected == nullptr) + return false; + auto *road{tool.selected}; + std::string reason; + if (false == editor_road::can_edit(*road, &reason)) + { + tool.status = "Can't split: " + reason; + return true; + } + auto const before{road->definition()}; + auto *created{editor_road::split(*road, editor_road::nearest_parameter(before, tool.mouse))}; + if (created == nullptr) + { + tool.status = "Can't split this close to the end"; + return true; + } + push_road_snapshot({{road, before}}, {created}, {}); + road_select(road); + tool.status = "Split in two: \"" + road->name() + "\" and \"" + created->name() + "\""; + return true; +} + +void editor_mode::push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed) +{ + if (States.empty() && Created.empty() && Removed.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 const *road = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::RoadEdit; + snap.node_name = road->name(); + snap.position = road->location(); + snap.roads = std::move(States); + snap.roads_created = std::move(Created); + snap.roads_removed = std::move(Removed); + m_history.push_back(std::move(snap)); + g_redo.clear(); +} + +void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo) +{ + if (Snapshot.roads.empty() && Snapshot.roads_created.empty() && Snapshot.roads_removed.empty()) + return; + + // what the roads are like now is what the opposite operation brings back + EditorSnapshot current{Snapshot}; + current.roads.clear(); + for (auto const &entry : Snapshot.roads) + current.roads.emplace_back(entry.first, entry.first->definition()); + Opposite.push_back(std::move(current)); + + if (Undo) + { + editor_road::remove(Snapshot.roads_created); + editor_road::revive(Snapshot.roads_removed); + } + else + { + editor_road::revive(Snapshot.roads_created); + editor_road::remove(Snapshot.roads_removed); + } + editor_road::apply(Snapshot.roads); + + auto &tool{m_roadtool}; + tool.chain = false; + if (tool.selected != nullptr) + road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); +} + +// draws controls for the layout of a road. returns: true if the layout was changed in a way which calls for the road to be made anew +bool editor_mode::render_road_layout(road_node::state &State) +{ + auto &tool{m_roadtool}; + bool changed{false}; + // typed values are taken when confirmed, so the road isn't made anew with each digit + auto const number = [](char const *Label, float &Value, float const Step, char const *Format) { + ImGui::SetNextItemWidth(120.0f); + return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue); + }; + auto const material = [&tool](char const *Label, char *Buffer, std::size_t const Size, std::string &Value) { + bool picked{false}; + ImGui::PushID(Label); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::InputText("##name", Buffer, Size, ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::IsItemDeactivatedAfterEdit()) + { + auto const name{lower_case(Buffer)}; + if (name != Value && name.find(' ') == std::string::npos) + { + Value = name.empty() ? std::string{"none"} : name; + picked = true; + } + copy_text(Buffer, Size, Value); + } + ImGui::SameLine(); + if (ImGui::Button("...")) + ImGui::OpenPopup("pick"); + ImGui::SameLine(); + ImGui::TextUnformatted(Label); + if (ImGui::BeginPopup("pick")) + { + ImGui::TextDisabled("Materials and images found in the texture folder"); + ImGui::SetNextItemWidth(200.0f); + ImGui::InputText("Filter", tool.filter, sizeof(tool.filter)); + auto const filter{lower_case(tool.filter)}; + std::vector listed; + for (auto const &name : editor_road::materials()) + if (filter.empty() || name.find(filter) != std::string::npos) + listed.emplace_back(&name); + ImGui::BeginChild("list", ImVec2(340.0f, 280.0f), true); + ImGuiListClipper clipper(static_cast(listed.size())); + while (clipper.Step()) + { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) + { + if (ImGui::Selectable(listed[i]->c_str(), *listed[i] == Value)) + { + Value = *listed[i]; + copy_text(Buffer, Size, Value); + picked = true; + ImGui::CloseCurrentPopup(); + } + } + } + ImGui::EndChild(); + ImGui::EndPopup(); + } + ImGui::PopID(); + return picked; + }; + + // lanes + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt("Lanes along the axis", &State.forward)) + changed = true; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt("Lanes against the axis", &State.backward)) + changed = true; + if (changed) + { + // a different set of lanes starts with the common width and speed limit, and the default rules for changing lanes + State.lanes.clear(); + State.changes.clear(); + State.normalize(); + } + if (number("Lane width [m]", State.lanewidth, 0.25f, "%.2f")) + { + State.lanewidth = std::clamp(State.lanewidth, 1.0f, 10.0f); + for (auto &lane : State.lanes) + lane.width = State.lanewidth; + changed = true; + } + float limit{State.velocity > 0.0f ? State.velocity : 0.0f}; + if (number("Speed limit [km/h], 0: none", limit, 10.0f, "%.0f")) + { + State.velocity = (limit > 0.0f ? limit : -1.0f); + for (auto &lane : State.lanes) + lane.velocity = State.velocity; + changed = true; + } + + // appearance + ImGui::Separator(); + char const *colours[]{"none", "white", "orange"}; + int colour{State.markings == road_node::marking_colour::white ? 1 : State.markings == road_node::marking_colour::orange ? 2 : 0}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo("Markings", &colour, colours, 3)) + { + State.markings = (colour == 1 ? road_node::marking_colour::white : colour == 2 ? road_node::marking_colour::orange : road_node::marking_colour::none); + changed = true; + } + if (material("Surface", tool.surface, sizeof(tool.surface), State.surface)) + changed = true; + if (number("Texture length [m]", State.texturelength, 0.5f, "%.2f")) + { + State.texturelength = std::max(0.1f, State.texturelength); + changed = true; + } + char const *sidenames[]{"Left side", "Right side"}; + char const *sidetypes[]{"none", "shoulder", "sidewalk"}; + for (int side = 0; side < 2; ++side) + { + ImGui::PushID(side); + auto &data{State.sides[side]}; + int type{data.type == road_node::side_type::shoulder ? 1 : data.type == road_node::side_type::sidewalk ? 2 : 0}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo(sidenames[side], &type, sidetypes, 3)) + { + data.type = (type == 1 ? road_node::side_type::shoulder : type == 2 ? road_node::side_type::sidewalk : road_node::side_type::none); + if (data.type != road_node::side_type::none && data.width <= 0.0f) + data.width = 1.5f; + changed = true; + } + if (data.type != road_node::side_type::none) + { + ImGui::Indent(); + if (number("Width [m]", data.width, 0.25f, "%.2f")) + { + data.width = std::clamp(data.width, 0.1f, 20.0f); + changed = true; + } + if (material("Material", tool.sides[side], sizeof(tool.sides[side]), data.material)) + changed = true; + if (data.material.empty() || data.material == "none") + ImGui::TextDisabled("Pick a material to have the side drawn"); + ImGui::Unindent(); + } + ImGui::PopID(); + } + if (State.sides[0].type == road_node::side_type::sidewalk || State.sides[1].type == road_node::side_type::sidewalk) + { + if (number("Kerb height [m]", State.kerbheight, 0.01f, "%.2f")) + { + State.kerbheight = std::clamp(State.kerbheight, 0.0f, 1.0f); + changed = true; + } + } + if (State.sides[0].type == road_node::side_type::shoulder || State.sides[1].type == road_node::side_type::shoulder) + { + if (number("Bank width [m]", State.slope.x, 0.25f, "%.2f")) + { + State.slope.x = std::clamp(State.slope.x, 0.0f, 20.0f); + changed = true; + } + if (number("Bank drop [m]", State.slope.y, 0.1f, "%.2f")) + { + State.slope.y = std::clamp(State.slope.y, 0.0f, 20.0f); + changed = true; + } + } + + // single lanes, and the rules for changing between them + if (ImGui::CollapsingHeader("Lanes and lane changes", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::TextDisabled("Left to right when facing along the axis. f: along the axis, b: against it"); + for (std::size_t lane = 0; lane < State.lanes.size(); ++lane) + { + ImGui::PushID(static_cast(lane)); + auto &data{State.lanes[lane]}; + auto const id{State.lane_id(lane)}; + ImGui::TextColored(lane >= static_cast(State.backward) ? ImVec4(0.3f, 0.9f, 0.45f, 1.0f) : ImVec4(1.0f, 0.67f, 0.2f, 1.0f), "%-3s", id.c_str()); + ImGui::SameLine(); + if (number("m##width", data.width, 0.25f, "%.2f")) + { + data.width = std::clamp(data.width, 1.0f, 10.0f); + changed = true; + } + ImGui::SameLine(); + float lanelimit{data.velocity > 0.0f ? data.velocity : 0.0f}; + if (number("km/h##velocity", lanelimit, 10.0f, "%.0f")) + { + data.velocity = (lanelimit > 0.0f ? lanelimit : -1.0f); + changed = true; + } + if (lane + 1 < State.lanes.size() && lane < State.changes.size()) + { + // the entries are in the order of the stored values: none, to the right, to the left, both + auto const next{State.lane_id(lane + 1)}; + auto const toright{"only " + id + " to " + next}; + auto const toleft{"only " + next + " to " + id}; + char const *modes[]{"no lane change", toright.c_str(), toleft.c_str(), "lane change both ways"}; + ImGui::Indent(); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::Combo("##change", &State.changes[lane], modes, 4)) + changed = true; + ImGui::Unindent(); + } + ImGui::PopID(); + } + } + return changed; +} + +void editor_mode::render_road_window() +{ + auto &tool{m_roadtool}; + if (false == tool.window) + return; + ImGui::SetNextWindowSize(ImVec2(440.0f, 620.0f), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin("Roads###roadeditor", &tool.window)) + { + ImGui::End(); + return; + } + std::string reason; + if (false == editor_road::available(&reason)) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", reason.c_str()); + + auto const previoustool{tool.tool}; + ImGui::RadioButton("Select", &tool.tool, 0); + ImGui::SameLine(); + ImGui::RadioButton("Build", &tool.tool, 1); + if (tool.tool != previoustool) + { + tool.chain = false; + if (tool.tool == 1) + tool.selected = nullptr; + } + ImGui::SameLine(); + ImGui::Checkbox("Show lanes", &tool.lanes); + + if (tool.tool == 1) + { + ImGui::TextDisabled("LMB: start, then each next point. Ctrl: straight ahead. Esc: finish"); + ImGui::TextDisabled("Start or end on a loose end of a road to join it"); + if (tool.chain) + { + if (ImGui::Button("Finish (Esc)")) + tool.chain = false; + } + ImGui::Separator(); + // what's set here is what the next pieces get + render_road_layout(tool.settings); + } + else if (tool.selected != nullptr) + { + auto const &road{*tool.selected}; + ImGui::Text("%s, %.1f m, %d + %d lanes", road.name().c_str(), road.definition().length(), road.definition().forward, road.definition().backward); + if (ImGui::Button("Delete (Del)")) + road_delete(); + ImGui::SameLine(); + ImGui::TextDisabled("K: split at the cursor"); + ImGui::Checkbox("Changes go to the whole road", &tool.whole); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nSplit a piece (K) to get a zone with rules of its own."); + ImGui::Separator(); + if (tool.selected != nullptr && render_road_layout(tool.settings)) + road_apply(); + } + else + { + ImGui::TextDisabled("LMB: select a road piece"); + } + if (false == tool.status.empty()) + { + ImGui::Separator(); + ImGui::TextWrapped("%s", tool.status.c_str()); + } + ImGui::End(); +} + +void editor_mode::draw_road_overlay() const +{ + auto const &tool{m_roadtool}; + if (false == tool.lanes && false == tool.window) + return; + + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + glm::dvec3 const camera{Global.pCamera.Pos}; + auto const drawpath = [&](segment_data const &Path, ImU32 const Color, float const Thickness, int const Samples) { + 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, Color, Thickness); + previous = next; + } + }; + + // lanes, as the vehicles see them: a line for each, with an arrow pointing where the traffic goes + if (tool.lanes) + { + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const range{glm::distance(road->location(), camera)}; + if (range > kLaneRange + 0.5 * road->definition().length()) + continue; + auto const samples{is_curved(road->definition().axis) ? (range < 150.0 ? 12 : 6) : 1}; + auto const against{static_cast(road->definition().backward)}; + auto const &tracks{road->tracks()}; + for (std::size_t lane = 0; lane < tracks.size(); ++lane) + { + if (tracks[lane] == nullptr || tracks[lane]->m_paths.empty()) + continue; + auto const &path{tracks[lane]->m_paths.front()}; + auto const color{lane >= against ? kForwardColor : kBackwardColor}; + drawpath(path, color, 2.0f, samples); + if (range > 300.0) + continue; + auto const middle{path_point(path, 0.5)}; + auto direction{path_point(path, 0.52) - middle}; + if (glm::length(direction) < 1e-6) + continue; + direction = glm::normalize(direction); + glm::dvec3 const across{direction.z, 0.0, -direction.x}; + auto const tip{middle + direction * 1.2}; + projection.line(drawlist, tip, middle - direction * 0.6 + across * 0.6, color, 2.0f); + projection.line(drawlist, tip, middle - direction * 0.6 - across * 0.6, color, 2.0f); + } + for (auto const *turn : road->turns()) + { + if (turn != nullptr && false == turn->m_paths.empty()) + drawpath(turn->m_paths.front(), kTurnColor, 1.5f, 8); + } + } + } + if (false == tool.window) + return; + + // outline of a road piece: its axis and the edges of its surface + auto const drawroad = [&](road_node::state const &State, ImU32 const Color) { + auto const samples{is_curved(State.axis) ? 12 : 1}; + auto const halfwidth{0.5 * State.width()}; + drawpath(State.axis, Color, 3.0f, samples); + glm::dvec3 previous[2]; + for (int i = 0; i <= samples; ++i) + { + auto const t{static_cast(i) / samples}; + auto const position{path_point(State.axis, t)}; + auto const tangent{State.tangent(t)}; + glm::dvec3 across{tangent.z, 0.0, -tangent.x}; + if (glm::length(across) > 1e-6) + across = glm::normalize(across); + glm::dvec3 const edges[2]{position + across * halfwidth, position - across * halfwidth}; + if (i > 0) + { + projection.line(drawlist, previous[0], edges[0], Color, 1.5f); + projection.line(drawlist, previous[1], edges[1], Color, 1.5f); + } + previous[0] = edges[0]; + previous[1] = edges[1]; + } + }; + auto const drawpoint = [&](glm::dvec3 const &Point, ImU32 const Color, float const Radius, bool const Filled) { + ImVec2 screen; + if (false == projection.project(Point, screen)) + return; + if (Filled) + drawlist->AddCircleFilled(screen, Radius, Color); + else + drawlist->AddCircle(screen, Radius, Color, 20, 3.0f); + }; + + if (tool.selected != nullptr && false == tool.selected->m_editorremoved) + { + auto const &state{tool.selected->definition()}; + drawroad(state, kSelectedColor); + drawpoint(state.axis.points[segment_data::point::start], kSelectedColor, 6.0f, true); + drawpoint(state.axis.points[segment_data::point::end], kSelectedColor, 6.0f, true); + } + + std::string hint; + if (tool.tool == 1) + { + if (tool.chain) + { + editor_road::loose_end snap; + std::string error; + auto const pieces{road_preview(snap, error)}; + drawpoint(tool.point, kPreviewColor, 6.0f, true); + if (pieces.empty()) + { + projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f); + hint = error + " "; + } + for (auto const &piece : pieces) + { + auto state{tool.settings}; + state.axis = piece; + drawroad(state, kPreviewColor); + } + if (snap.road != nullptr) + drawpoint(snap.position, kSnapColor, 14.0f, false); + hint += "LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece"; + } + else + { + auto const end{editor_road::find_end(tool.mouse, kSnapRadius)}; + if (end.road != nullptr) + drawpoint(end.position, kSnapColor, 14.0f, false); + hint = "LMB: set where the road starts; on a loose end of a road (ring) to carry it on"; + } + } + else + { + hint = tool.selected != nullptr ? "LMB: select a road piece K: split at the cursor Del: delete Ctrl+Z: undo" : "LMB: select a road piece"; + } + // shown above the place the track tools put their hints at + ImGuiIO const &io = ImGui::GetIO(); + ImVec2 const position{12.0f, io.DisplaySize.y - 56.0f}; + auto const size{ImGui::CalcTextSize(hint.c_str())}; + 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()); +} diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index aaef5783..f0c5d6c2 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -143,6 +143,9 @@ void editor_ui::render_menu_contents() ImGui::MenuItem(STR_C("Include database"), nullptr, &m_includespanel.is_open); ImGui::EndMenu(); } + + if (m_menuoptions) + m_menuoptions(); } void editor_ui::set_file_actions(std::function Save, std::function Export) diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f4b3c971..31cfc530 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -54,6 +54,11 @@ class editor_ui : public ui_layer { m_trackoptions = std::move(Renderer); } + // draws menus of the editor mode in the menu bar, past the ones of the user interface + void set_menu_options(std::function Renderer) + { + m_menuoptions = 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) { @@ -82,6 +87,7 @@ class editor_ui : public ui_layer model_set_ref m_insertset; std::function m_filloptions; std::function m_trackoptions; + std::function m_menuoptions; std::function m_gizmooptions; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any }; diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp new file mode 100644 index 00000000..bb2039b2 --- /dev/null +++ b/editor/editorRoad.cpp @@ -0,0 +1,564 @@ +/* +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/editorRoad.hpp" + +#include "editor/editorTrack.hpp" +#include "world/Track.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include + +namespace +{ + +double const kSamePoint{0.25}; // ends of two pieces this close are taken for joined +double const kPieceLength{50.0}; // longest piece the planner makes +double const kPieceTurn{glm::radians(30.0)}; // largest turn of a single piece +double const kShortestRoad{2.0}; +double const kTightestRadius{6.0}; + +void add_unique(std::vector &Roads, road_node *Road) +{ + if (Road != nullptr && std::find(Roads.begin(), Roads.end(), Road) == Roads.end()) + Roads.emplace_back(Road); +} + +glm::dvec3 axis_end(road_node::state const &State, bool const Atend) +{ + return State.axis.points[Atend ? segment_data::point::end : segment_data::point::start]; +} + +// roads other than the specified one, with an end at specified point +std::vector roads_at(glm::dvec3 const &Point, road_node const *Exclude) +{ + std::vector result; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road == Exclude || road->m_editorremoved) + continue; + for (auto const atend : {false, true}) + { + if (glm::distance(axis_end(road->definition(), atend), Point) < kSamePoint) + { + result.emplace_back(road); + break; + } + } + } + return result; +} + +void retire(std::vector const &Tracks) +{ + for (auto *track : Tracks) + if (track != nullptr) + editor_track::retire(*track); +} + +// smallest radius of the bend made by a bezier curve, seen from above; 0 if the curve is practically straight +double tightest_radius(segment_data const &Path) +{ + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + auto const p1{p0 + Path.points[segment_data::point::control1]}; + auto const p2{p3 + Path.points[segment_data::point::control2]}; + double curvature{0.0}; + for (int i = 0; i <= 16; ++i) + { + auto const t{i / 16.0}; + auto const u{1.0 - t}; + auto const first{3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2)}; + auto const second{6.0 * u * (p2 - 2.0 * p1 + p0) + 6.0 * t * (p3 - 2.0 * p2 + p1)}; + auto const speedsquared{first.x * first.x + first.z * first.z}; + if (speedsquared < 1e-9) + continue; + curvature = std::max(curvature, std::abs(second.x * first.z - second.z * first.x) / (speedsquared * std::sqrt(speedsquared))); + } + return curvature > 1e-5 ? 1.0 / curvature : 0.0; +} + +} + +bool editor_road::available(std::string *Reason) +{ + if (Global.NvRenderer) + { + if (Reason != nullptr) + *Reason = "Geometry of the roads can't be rebuilt with the experimental renderer"; + return false; + } + if (simulation::Region == nullptr) + { + if (Reason != nullptr) + *Reason = "No scenery is loaded"; + return false; + } + return true; +} + +bool editor_road::can_edit(road_node const &Road, std::string *Reason) +{ + std::string reason; + if (false == available(&reason)) + { + // reason was filled by the call + } + else if (Road.merged()) + { + reason = "the scenery wasn't opened for editing (-edit), geometry of the roads it came with can't be taken apart"; + } + else if (false == scene::Layers.editable(&Road, &reason)) + { + // reason was filled by the call + } + else if (Road.occupied()) + { + reason = "there are vehicles on it"; + } + else + { + // the paths made to turn the vehicles back at the loose ends of the neighbours go away too + for (auto const *neighbour : neighbours(Road)) + for (auto const *turn : neighbour->turns()) + if (turn != nullptr && false == turn->Dynamics.empty()) + reason = "there are vehicles turning back next to it"; + } + if (false == reason.empty() && Reason != nullptr) + *Reason = reason; + return reason.empty(); +} + +std::vector editor_road::neighbours(road_node const &Road) +{ + std::vector result; + if (Road.m_editorremoved) + return result; + for (auto const atend : {false, true}) + for (auto *road : roads_at(axis_end(Road.definition(), atend), &Road)) + add_unique(result, road); + return result; +} + +template +void editor_road::refresh(std::vector const &Roads, Change_ Change) +{ + // the pieces next to the changed ones keep their shape, but what they're joined with changes + std::vector affected; + for (auto *road : Roads) + { + add_unique(affected, road); + for (auto *neighbour : neighbours(*road)) + add_unique(affected, neighbour); + } + for (auto *road : affected) + retire(road->release_turns()); + for (auto *road : Roads) + { + road->hide(); + retire(road->release_lanes()); + } + + Change(); + + // the change could have moved the pieces next to other ones + for (auto *road : Roads) + { + for (auto *neighbour : neighbours(*road)) + { + if (std::find(affected.begin(), affected.end(), neighbour) == affected.end()) + { + retire(neighbour->release_turns()); + affected.emplace_back(neighbour); + } + } + } + std::vector lanes; + for (auto *road : Roads) + { + if (road->m_editorremoved) + continue; + road->create_lanes(); + road->show(); + lanes.insert(lanes.end(), road->tracks().begin(), road->tracks().end()); + } + // joins the lanes with the lanes of the neighbours, by the ends they share + editor_track::commit(lanes); + for (auto *road : affected) + if (false == road->m_editorremoved) + road->close_ends(); +} + +std::vector editor_road::create(std::vector const &States) +{ + static int counter{0}; + std::vector created; + if (false == available()) + return created; + for (auto const &state : States) + { + scene::node_data data; + data.type = "road"; + data.range_max = -1.0; + data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing + // the name is what the lanes are known by to the vehicles put on them, so each piece needs one of its own + do + { + data.name = "road_" + std::to_string(++counter); + } while (simulation::Roads.find(data.name) != nullptr); + auto *road{new road_node(data)}; + road->define(state); + road->mark_dirty(); + simulation::Roads.insert(road); + scene::Layers.count(road->layer(), scene::layer_item::track); + created.emplace_back(road); + } + refresh(created, []() {}); + return created; +} + +void editor_road::apply(std::vector> const &Changes) +{ + std::vector roads; + for (auto const &change : Changes) + add_unique(roads, change.first); + if (roads.empty()) + return; + refresh(roads, [&]() { + for (auto const &change : Changes) + { + change.first->define(change.second); + change.first->mark_dirty(); + } + }); +} + +void editor_road::remove(std::vector const &Roads) +{ + std::vector roads; + for (auto *road : Roads) + if (road != nullptr && false == road->m_editorremoved) + add_unique(roads, road); + if (roads.empty()) + return; + refresh(roads, [&]() { + for (auto *road : roads) + { + road->m_editorremoved = true; + scene::Layers.count(road->layer(), scene::layer_item::track, -1); + } + }); +} + +void editor_road::revive(std::vector const &Roads) +{ + std::vector roads; + for (auto *road : Roads) + if (road != nullptr && road->m_editorremoved) + add_unique(roads, road); + if (roads.empty()) + return; + refresh(roads, [&]() { + for (auto *road : roads) + { + road->m_editorremoved = false; + road->mark_dirty(); + scene::Layers.count(road->layer(), scene::layer_item::track); + } + }); +} + +road_node *editor_road::split(road_node &Road, double const T) +{ + if (T <= 0.001 || T >= 0.999) + return nullptr; + auto first{Road.definition()}; + auto second{first}; + auto const &path{Road.definition().axis}; + auto const p0{path.points[segment_data::point::start]}; + auto const p3{path.points[segment_data::point::end]}; + if (path.points[segment_data::point::control1] == glm::dvec3{0.0} && path.points[segment_data::point::control2] == glm::dvec3{0.0}) + { + auto const middle{glm::mix(p0, p3, T)}; + first.axis.points[segment_data::point::end] = middle; + second.axis.points[segment_data::point::start] = middle; + } + else + { + auto const p1{p0 + path.points[segment_data::point::control1]}; + auto const p2{p3 + path.points[segment_data::point::control2]}; + auto const p01{glm::mix(p0, p1, T)}; + auto const p12{glm::mix(p1, p2, T)}; + auto const p23{glm::mix(p2, p3, T)}; + auto const p012{glm::mix(p01, p12, T)}; + auto const p123{glm::mix(p12, p23, T)}; + auto const middle{glm::mix(p012, p123, T)}; + first.axis.points[segment_data::point::control1] = p01 - p0; + first.axis.points[segment_data::point::control2] = p012 - middle; + first.axis.points[segment_data::point::end] = middle; + second.axis.points[segment_data::point::start] = middle; + second.axis.points[segment_data::point::control1] = p123 - middle; + second.axis.points[segment_data::point::control2] = p23 - p3; + } + auto const roll{static_cast(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * T)}; + first.axis.rolls[1] = roll; + second.axis.rolls[0] = roll; + apply({{&Road, first}}); + auto const created{create({second})}; + return created.empty() ? nullptr : created.front(); +} + +std::vector editor_road::chain(road_node &Road) +{ + std::vector result{&Road}; + for (std::size_t idx = 0; idx < result.size(); ++idx) + { + for (auto const atend : {false, true}) + { + // a road goes on only where exactly two pieces meet + auto const next{roads_at(axis_end(result[idx]->definition(), atend), result[idx])}; + if (next.size() == 1) + add_unique(result, next.front()); + } + } + return result; +} + +double editor_road::nearest_parameter(road_node::state const &State, glm::dvec3 const &Point) +{ + auto const distance = [&](double const T) { + auto const position{State.point(T)}; + return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z}); + }; + double best{0.0}; + for (int i = 1; i <= 64; ++i) + if (distance(i / 64.0) < distance(best)) + best = i / 64.0; + double step{1.0 / 64.0}; + for (int i = 0; i < 24; ++i) + { + step *= 0.5; + if (best - step >= 0.0 && distance(best - step) < distance(best)) + best -= step; + else if (best + step <= 1.0 && distance(best + step) < distance(best)) + best += step; + } + return best; +} + +road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin) +{ + road_node *result{nullptr}; + double best{std::numeric_limits::max()}; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const &state{road->definition()}; + auto const reach{0.5 * state.width() + std::max(state.sides[0].width, state.sides[1].width) + Margin}; + if (glm::distance(road->location(), Point) > 0.5 * state.length() + reach + 10.0) + continue; + auto const position{state.point(nearest_parameter(state, Point))}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; + // roads passing over or under the point don't count + if (distance > reach || std::abs(position.y - Point.y) > 3.0) + continue; + if (distance < best) + { + best = distance; + result = road; + } + } + return result; +} + +editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double const Radius) +{ + loose_end result; + double best{Radius}; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const &state{road->definition()}; + for (auto const atend : {false, true}) + { + auto const position{axis_end(state, atend)}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty()) + continue; + best = distance; + result.road = road; + result.atend = atend; + result.position = position; + result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0)); + } + } + return result; +} + +std::vector editor_road::plan(glm::dvec3 const &Start, glm::dvec2 const *Direction, glm::dvec3 const &Target, glm::dvec2 const *Enddirection, bool const Straight, std::string &Error) +{ + std::vector pieces; + glm::dvec2 const chord{Target.x - Start.x, Target.z - Start.z}; + auto const span{glm::length(chord)}; + if (span < kShortestRoad) + { + Error = "Too short"; + return pieces; + } + // straight road, cut into pieces of reasonable length + auto const straight = [&](glm::dvec3 const &End) { + auto const count{std::max(1, static_cast(std::ceil(glm::distance(Start, End) / kPieceLength)))}; + for (int i = 0; i < count; ++i) + { + segment_data piece; + piece.points[segment_data::point::start] = glm::mix(Start, End, static_cast(i) / count); + piece.points[segment_data::point::end] = glm::mix(Start, End, static_cast(i + 1) / count); + pieces.emplace_back(piece); + } + }; + + if (Direction != nullptr && Enddirection != nullptr) + { + // both ends are tied, which leaves a single curve with the tangents set + if (glm::dot(*Direction, chord) < 0.1 * span || glm::dot(*Enddirection, chord) < 0.1 * span) + { + Error = "The road would have to turn back to meet the end"; + return pieces; + } + auto const rise{(Target.y - Start.y) / 3.0}; + segment_data piece; + piece.points[segment_data::point::start] = Start; + piece.points[segment_data::point::end] = Target; + piece.points[segment_data::point::control1] = glm::dvec3{Direction->x * span / 3.0, rise, Direction->y * span / 3.0}; + piece.points[segment_data::point::control2] = glm::dvec3{-Enddirection->x * span / 3.0, -rise, -Enddirection->y * span / 3.0}; + auto const radius{tightest_radius(piece)}; + if (radius == 0.0) + { + pieces.clear(); + straight(Target); + return pieces; + } + if (radius < kTightestRadius) + { + Error = "The bend is too tight"; + return pieces; + } + piece.radius = static_cast(radius); + pieces.emplace_back(piece); + return pieces; + } + if (Direction == nullptr && Enddirection != nullptr) + { + // only the far end is tied: the same road as the one leading back from there, turned around + glm::dvec2 const back{-*Enddirection}; + auto reversed{plan(Target, &back, Start, nullptr, Straight, Error)}; + for (auto &piece : reversed) + { + std::swap(piece.points[segment_data::point::start], piece.points[segment_data::point::end]); + std::swap(piece.points[segment_data::point::control1], piece.points[segment_data::point::control2]); + } + std::reverse(reversed.begin(), reversed.end()); + return reversed; + } + if (Direction == nullptr) + { + straight(Target); + return pieces; + } + + // the road leaves the start in set direction, and reaches the target with an arc + auto const forward{*Direction}; + glm::dvec2 const left{forward.y, -forward.x}; + auto const along{glm::dot(chord, forward)}; + auto const side{glm::dot(chord, left)}; + if (Straight || std::abs(side) < 0.02 * span) + { + if (along < kShortestRoad) + { + Error = "The target is behind the start"; + return pieces; + } + straight(glm::dvec3{Start.x + forward.x * along, Target.y, Start.z + forward.y * along}); + return pieces; + } + auto const turn{2.0 * std::atan2(side, along)}; // positive to the left + auto const radius{span * span / (2.0 * side)}; // likewise + if (std::abs(turn) > glm::radians(170.0)) + { + Error = "The road would have to turn back to reach the target"; + return pieces; + } + if (std::abs(radius) < kTightestRadius) + { + Error = "The bend is too tight"; + return pieces; + } + glm::dvec2 const start{Start.x, Start.z}; + auto const centre{start + left * radius}; + auto const count{std::max(1, static_cast(std::ceil(std::max(std::abs(turn) / kPieceTurn, std::abs(radius * turn) / kPieceLength))))}; + auto const handle{4.0 / 3.0 * std::tan(std::abs(turn) / count / 4.0) * std::abs(radius)}; + auto const rise{(Target.y - Start.y) / count}; + for (int i = 0; i < count; ++i) + { + auto const angle0{turn * i / count}; + auto const angle1{turn * (i + 1) / count}; + auto const point0{centre - left * (radius * std::cos(angle0)) + forward * (radius * std::sin(angle0))}; + auto const point1{centre - left * (radius * std::cos(angle1)) + forward * (radius * std::sin(angle1))}; + auto const tangent0{forward * std::cos(angle0) + left * std::sin(angle0)}; + auto const tangent1{forward * std::cos(angle1) + left * std::sin(angle1)}; + segment_data piece; + piece.points[segment_data::point::start] = glm::dvec3{point0.x, Start.y + rise * i, point0.y}; + piece.points[segment_data::point::end] = glm::dvec3{point1.x, Start.y + rise * (i + 1), point1.y}; + piece.points[segment_data::point::control1] = glm::dvec3{tangent0.x * handle, rise / 3.0, tangent0.y * handle}; + piece.points[segment_data::point::control2] = glm::dvec3{-tangent1.x * handle, -rise / 3.0, -tangent1.y * handle}; + piece.radius = static_cast(std::abs(radius)); + pieces.emplace_back(piece); + } + // the arc is worked out from the start, make sure it ends exactly at the target + pieces.back().points[segment_data::point::end] = Target; + return pieces; +} + +std::vector const &editor_road::materials() +{ + static std::vector names; + static bool scanned{false}; + if (scanned) + return names; + scanned = true; + std::set found; + std::error_code error; + for (std::filesystem::directory_iterator file{Global.asCurrentTexturePath, std::filesystem::directory_options::skip_permission_denied, error}, end; file != end; file.increment(error)) + { + if (error || false == file->is_regular_file(error)) + continue; + auto extension{file->path().extension().string()}; + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (extension != ".mat" && extension != ".dds" && extension != ".tga" && extension != ".png" && extension != ".bmp" && extension != ".jpg" && extension != ".ktx" && extension != ".tex") + continue; + auto name{file->path().stem().string()}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + // names with spaces can't be put in a scenery file + if (name.empty() || name.find(' ') != std::string::npos) + continue; + found.emplace(name); + } + names.assign(found.begin(), found.end()); + return names; +} diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp new file mode 100644 index 00000000..40f21cb4 --- /dev/null +++ b/editor/editorRoad.hpp @@ -0,0 +1,65 @@ +/* +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/Road.h" + +// editor operations on the roads. a road in the scenery is a string of pieces joined end to end, +// each piece being a road node with lanes and geometry of its own +class editor_road +{ + public: + // an end of a road piece nothing is attached to + struct loose_end + { + road_node *road{nullptr}; + bool atend{false}; + glm::dvec3 position{0.0}; + glm::dvec3 outwards{0.0}; // direction the road would continue in + }; + // true if the roads can be built and changed at all. optionally explains why they can't + static bool available(std::string *Reason = nullptr); + // true if specified road piece can be changed or removed. optionally explains why it can't + static bool can_edit(road_node const &Road, std::string *Reason = nullptr); + // makes road pieces out of provided definitions, with their lanes joined to the neighbours and geometry in the scene + static std::vector create(std::vector const &States); + // replaces definitions of the pieces and makes their lanes and geometry anew + static void apply(std::vector> const &Changes); + // takes the pieces out of the scenery. they're kept around, so they can be brought back + static void remove(std::vector const &Roads); + static void revive(std::vector const &Roads); + // cuts the piece in two at specified point of its axis. returns: the new piece, holding the part past the cut + static road_node *split(road_node &Road, double const T); + // the pieces which make a single road together with specified one, in no particular order + static std::vector chain(road_node &Road); + // the piece under specified point, or within specified distance from it + static road_node *nearest(glm::dvec3 const &Point, double const Margin); + // value of the curve parameter for the point of the axis nearest to specified point + static double nearest_parameter(road_node::state const &State, glm::dvec3 const &Point); + // end of a piece with nothing attached, within specified distance from specified point + static loose_end find_end(glm::dvec3 const &Point, double const Radius); + // lays out axes of the pieces for a road leading from a point to another. the directions are optional requirements + // for the road at its ends; without them it starts straight at the target, with them it bends to meet them. + // returns: the axes in order, or nothing if there's no reasonable road to make, with the reason in Error + static std::vector plan(glm::dvec3 const &Start, glm::dvec2 const *Direction, glm::dvec3 const &Target, glm::dvec2 const *Enddirection, bool const Straight, std::string &Error); + // names of materials and images present in the texture folder, to pick from + static std::vector const &materials(); + + private: + // takes down what depends on shape of the listed pieces, lets the caller change them, then builds it all again + template + static void refresh(std::vector const &Roads, Change_ Change); + static std::vector neighbours(road_node const &Road); +}; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index c45e24e8..71bc26b3 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "scene/scenelayers.h" #include "world/Track.h" +#include "world/Road.h" #include "simulation/simulation.h" #include "model/AnimModel.h" @@ -793,6 +794,69 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[source.layer].edits.emplace_back(std::move(edit)); rewritten.emplace(path); } + // roads. the lanes aren't saved, they're generated from the definition of the road + std::vector savedroads; + std::vector droppedroads; + auto const road_text = [](road_node &Road, glm::dvec3 const &Offset) { + auto const definition{Road.definition()}; + auto local{definition}; + local.axis.points[segment_data::point::start] -= Offset; + local.axis.points[segment_data::point::end] -= Offset; + Road.define(local); + std::string text; + Road.export_as_text(text); + Road.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->from_template()) + { + continue; + } + auto const nodelayer{resolve(road->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(road)}; + if (lookup == m_sources.end()) + { + if (road->m_editorremoved || false == road->dirty()) + { + continue; + } + created[nodelayer].emplace_back(road, road_text(*road, layer(nodelayer).context_insert().offset)); + savedroads.push_back(road); + continue; + } + auto const &source{lookup->second}; + if ((false == road->m_editorremoved && false == road->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (road->m_editorremoved) + { + droppedroads.push_back(road); + } + else + { + edit.text = road_text(*road, source.context.offset); + edit.length = edit.text.size(); + edit.node = road; + savedroads.push_back(road); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(road); + } for (auto const &erased : m_erased) { file_patch::edit edit; @@ -1236,6 +1300,14 @@ save_result node_layers::save(std::vector const &Rootstatements) { m_sources.erase(path); } + for (auto *road : savedroads) + { + road->m_dirty = false; + } + for (auto const *road : droppedroads) + { + m_sources.erase(road); + } if (removedterrain) { // binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index e6f60934..82843960 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -519,6 +519,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch scene::Groups.insert( scene::Groups.handle(), road ); // the lanes are regular paths, registered right away so they get joined with their neighbours along with the tracks road->create_lanes(); + scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "traction" ) { @@ -693,6 +694,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch static std::unordered_map const itemtypes { { "dynamic", scene::layer_item::vehicle }, { "track", scene::layer_item::track }, + { "road", scene::layer_item::track }, { "traction", scene::layer_item::traction }, { "tractionpowersource", scene::layer_item::powersource }, { "model", scene::layer_item::model }, @@ -1398,7 +1400,7 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons // roads scmfile << "// roads\n"; for( auto const *road : Roads.sequence() ) { - if( road != nullptr && road->dirty() == Dirty && road->group() == null_handle ) { + if( road != nullptr && false == road->m_editorremoved && road->dirty() == Dirty && road->group() == null_handle ) { road->export_as_text( scmfile ); } } diff --git a/world/Road.cpp b/world/Road.cpp index f52a4e51..7553b5eb 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -269,6 +269,182 @@ float texture_ratio(std::string const &Material) } // namespace +// brings the content to a usable form: values within their limits, an entry for each lane and for each pair of neighbours +void road_node::state::normalize() +{ + forward = std::clamp(forward, 0, 8); + backward = std::clamp(backward, 0, 8); + if (forward + backward == 0) + { + forward = 1; + } + lanewidth = std::max(1.f, lanewidth); + if (texturelength < 0.01f) + { + texturelength = 4.f; + } + if (surface.empty()) + { + surface = "none"; + } + // the paths of the lanes are loaded from text, so what goes into it has to be something they understand + static std::array const environments{"flat", "mountains", "mountain", "canyon", "tunnel", "bridge", "bank"}; + if (std::find(environments.begin(), environments.end(), environment) == environments.end()) + { + environment = "flat"; + } + auto const count{static_cast(forward + backward)}; + if (lanes.size() != count) + { + lane_data fresh; + fresh.width = lanewidth; + fresh.velocity = velocity; + lanes.assign(count, fresh); + } + for (auto &lane : lanes) + { + lane.width = std::max(1.f, lane.width); + } + if (changes.size() != count - 1) + { + changes.resize(count - 1); + for (std::size_t boundary = 0; boundary < changes.size(); ++boundary) + { + changes[boundary] = default_change(boundary); + } + } + for (auto &change : changes) + { + change = std::clamp(change, static_cast(change_none), static_cast(change_both)); + } +} + +// identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet +std::string road_node::state::lane_id(std::size_t const Lane) const +{ + auto const against{static_cast(backward)}; + return Lane < against ? "b" + std::to_string(against - Lane) : "f" + std::to_string(Lane - against + 1); +} + +// number of a lane in the left to right order, -1 if there's no lane with such identifier +int road_node::state::lane_index(std::string const &Id) const +{ + if (Id.size() < 2 || Id.size() > 3 || (Id[0] != 'f' && Id[0] != 'b')) + { + return -1; + } + int number{0}; + for (std::size_t idx = 1; idx < Id.size(); ++idx) + { + if (Id[idx] < '0' || Id[idx] > '9') + { + return -1; + } + number = number * 10 + (Id[idx] - '0'); + } + if (number < 1 || number > (Id[0] == 'f' ? forward : backward)) + { + return -1; + } + return Id[0] == 'f' ? backward + number - 1 : backward - number; +} + +// permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise +int road_node::state::default_change(std::size_t const Boundary) const +{ + auto const opposite{backward > 0 && forward > 0 && Boundary + 1 == static_cast(backward)}; + if (false == opposite) + { + return change_both; + } + // overtaking across the oncoming lane is left for the simple two lane roads + return (forward == 1 && backward == 1) ? change_both : change_none; +} + +// distance of the middle of a lane from the axis, positive to the left when facing along the axis +double road_node::state::lane_offset(std::size_t const Lane) const +{ + auto offset{0.5 * width()}; + for (std::size_t idx = 0; idx < Lane && idx < lanes.size(); ++idx) + { + offset -= lanes[idx].width; + } + return Lane < lanes.size() ? offset - 0.5 * lanes[Lane].width : offset; +} + +// combined width of the lanes +double road_node::state::width() const +{ + double total{0.0}; + for (auto const &lane : lanes) + { + total += lane.width; + } + return total; +} + +// shape of the middle of a lane, laid out in the direction of travel +segment_data road_node::state::lane_path(std::size_t const Lane) const +{ + road_axis const centre{axis}; + auto const offset{lane_offset(Lane)}; + auto const begin{centre.frame(0.0)}; + auto const end{centre.frame(1.0)}; + + segment_data path; + path.points[segment_data::point::start] = begin.position + begin.left * offset; + path.points[segment_data::point::end] = end.position + end.left * offset; + if (centre.curved()) + { + // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors + path.points[segment_data::point::control1] = centre.control(0) * std::max(0.05, 1.0 - offset * std::cos(begin.roll) * centre.curvature(0.0)); + path.points[segment_data::point::control2] = centre.control(1) * std::max(0.05, 1.0 - offset * std::cos(end.roll) * centre.curvature(1.0)); + // if the roll changes along the road its sides rise or fall relative to the axis + auto const climb{offset * (std::sin(end.roll) - std::sin(begin.roll)) / 3.0}; + path.points[segment_data::point::control1].y += climb; + path.points[segment_data::point::control2].y -= climb; + } + path.rolls = axis.rolls; + if (axis.radius != 0.f) + { + // the lanes on the inner side of the bend have it tighter + auto const turn{centre.curvature(0.5) >= 0.0 ? 1.0 : -1.0}; + path.radius = static_cast(std::max(1.0, std::abs(axis.radius) - turn * offset)); + } + if (Lane < static_cast(backward)) + { + // the lane goes against the axis + std::swap(path.points[segment_data::point::start], path.points[segment_data::point::end]); + std::swap(path.points[segment_data::point::control1], path.points[segment_data::point::control2]); + path.rolls = {-axis.rolls[1], -axis.rolls[0]}; + } + if (Global.bRollFix) + { + // with this setting on a rolled path gets raised when it's set up, by an amount fit for a railway track. + // the surface of the road stays where it is, so the lane is lowered beforehand to make up for it + path.points[segment_data::point::start].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[0])))) * 0.75; + path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[1])))) * 0.75; + } + return path; +} + +// length of the axis +double road_node::state::length() const +{ + return road_axis{axis}.length(); +} + +// position on the axis and direction of the axis, for specified value of the curve parameter +glm::dvec3 road_node::state::point(double const T) const +{ + return road_axis{axis}.point(T); +} + +glm::dvec3 road_node::state::tangent(double const T) const +{ + return road_axis{axis}.frame(T).tangent; +} + road_node::road_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} // restores content of the node from provided input stream; reads up to and including the closing 'endroad' @@ -276,7 +452,8 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) { auto const label{m_name.empty() ? std::string{"unnamed road"} : "road \"" + m_name + "\""}; - m_axis.deserialize(Input, Offset); + m_state = state{}; + m_state.axis.deserialize(Input, Offset); // properties of the lanes can precede the statement which sets how many of them there are, so they wait until everything is read struct lane_property @@ -301,17 +478,17 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) { // lanes Input.getTokens(2); - Input >> m_forward >> m_backward; + Input >> m_state.forward >> m_state.backward; } else if (token == "width") { Input.getTokens(); - Input >> m_lanewidth; + Input >> m_state.lanewidth; } else if (token == "velocity") { Input.getTokens(); - Input >> m_velocity; + Input >> m_state.velocity; } else if (token == "lane") { @@ -335,13 +512,13 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) } else if (token == "surface") { - m_surface = Input.getToken(); - replace_slashes(m_surface); + m_state.surface = Input.getToken(); + replace_slashes(m_state.surface); } else if (token == "texlength") { Input.getTokens(); - Input >> m_texturelength; + Input >> m_state.texturelength; } else if (token == "side") { @@ -360,11 +537,11 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) } if (where == "left" || where == "both") { - m_sides[0] = side; + m_state.sides[0] = side; } if (where == "right" || where == "both") { - m_sides[1] = side; + m_state.sides[1] = side; } if (where != "left" && where != "right" && where != "both") { @@ -374,18 +551,18 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) else if (token == "kerb") { Input.getTokens(); - Input >> m_kerbheight; + Input >> m_state.kerbheight; } else if (token == "slope") { // slope Input.getTokens(2); - Input >> m_slope.x >> m_slope.y; + Input >> m_state.slope.x >> m_state.slope.y; } else if (token == "markings") { auto const colour{Input.getToken()}; - m_markings = (colour == "white" ? marking_colour::white : colour == "orange" ? marking_colour::orange : marking_colour::none); + m_state.markings = (colour == "white" ? marking_colour::white : colour == "orange" ? marking_colour::orange : marking_colour::none); if (colour != "white" && colour != "orange" && colour != "none") { ErrorLog("Bad road: unknown colour of markings \"" + colour + "\" defined for " + label); @@ -394,11 +571,11 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) else if (token == "friction") { Input.getTokens(); - Input >> m_friction; + Input >> m_state.friction; } else if (token == "environment") { - m_environment = Input.getToken(); + m_state.environment = Input.getToken(); } else { @@ -407,37 +584,20 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) token = Input.getToken(); } - // the paths of the lanes are loaded from text, so what goes into it has to be something they understand - static std::array const environments{"flat", "mountains", "mountain", "canyon", "tunnel", "bridge", "bank"}; - if (std::find(environments.begin(), environments.end(), m_environment) == environments.end()) - { - ErrorLog("Bad road: unknown environment \"" + m_environment + "\" defined for " + label); - m_environment = "flat"; - } - m_forward = std::clamp(m_forward, 0, 8); - m_backward = std::clamp(m_backward, 0, 8); - if (m_forward + m_backward == 0) + if (m_state.forward + m_state.backward <= 0) { ErrorLog("Bad road: " + label + " has no lanes, a single one is used instead"); - m_forward = 1; } - m_lanewidth = std::max(1.f, m_lanewidth); - if (m_texturelength < 0.01f) + auto const environment{m_state.environment}; + m_state.normalize(); + if (environment != m_state.environment) { - m_texturelength = 4.f; - } - if (m_surface.empty()) - { - m_surface = "none"; + ErrorLog("Bad road: unknown environment \"" + environment + "\" defined for " + label); } - lane_data lanetemplate; - lanetemplate.width = m_lanewidth; - lanetemplate.velocity = m_velocity; - m_lanes.assign(static_cast(m_forward + m_backward), lanetemplate); for (auto const &property : laneproperties) { - auto const lane{lane_index(property.lane)}; + auto const lane{m_state.lane_index(property.lane)}; if (lane < 0) { ErrorLog("Bad road: " + label + " has no lane \"" + property.lane + "\""); @@ -445,27 +605,21 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) } if (property.property == "width") { - m_lanes[lane].width = std::max(1.f, property.value); + m_state.lanes[lane].width = std::max(1.f, property.value); } else if (property.property == "velocity") { - m_lanes[lane].velocity = property.value; + m_state.lanes[lane].velocity = property.value; } else { ErrorLog("Bad road: unknown lane property: \"" + property.property + "\" defined for " + label); } } - - m_changes.resize(m_lanes.size() - 1); - for (std::size_t boundary = 0; boundary < m_changes.size(); ++boundary) - { - m_changes[boundary] = default_change(boundary); - } for (auto const &property : changeproperties) { - auto const first{lane_index(property.first)}; - auto const second{lane_index(property.second)}; + auto const first{m_state.lane_index(property.first)}; + auto const second{m_state.lane_index(property.second)}; if (first < 0 || second < 0 || std::abs(first - second) != 1) { ErrorLog("Bad road: lanes \"" + property.first + "\" and \"" + property.second + "\" aren't neighbours on " + label); @@ -473,7 +627,7 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) } // the permissions are stored for the pair taken left to right auto const firstonleft{first < second}; - auto &change{m_changes[std::min(first, second)]}; + auto &change{m_state.changes[std::min(first, second)]}; if (property.mode == "both") { change = change_both; @@ -496,15 +650,25 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) } } - location(road_axis{m_axis}.frame(0.5).position); + location(m_state.point(0.5)); +} + +// replaces what the node knows about the road +void road_node::define(state const &State) +{ + m_state = State; + m_state.normalize(); + location(m_state.point(0.5)); + m_area.radius = -1.f; } // creates paths of the lanes and hands them over to the simulation void road_node::create_lanes() { - for (std::size_t lane = 0; lane < m_lanes.size(); ++lane) + m_tracks.assign(m_state.lanes.size(), nullptr); + for (std::size_t lane = 0; lane < m_state.lanes.size(); ++lane) { - m_lanes[lane].track = create_track(m_name.empty() ? std::string{} : m_name + ":" + lane_id(lane), lane_path(lane), m_lanes[lane].width, m_lanes[lane].velocity); + m_tracks[lane] = create_track(m_name.empty() ? std::string{} : m_name + ":" + m_state.lane_id(lane), m_state.lane_path(lane), m_state.lanes[lane].width, m_state.lanes[lane].velocity); } } @@ -512,17 +676,21 @@ void road_node::create_lanes() void road_node::close_ends() { // NOTE: without the link a vehicle reaching the end would be sent back on the lane it came by, against its direction - road_axis const axis{m_axis}; + if (m_tracks.size() != m_state.lanes.size()) + { + return; + } + road_axis const axis{m_state.axis}; for (int end = 0; end < 2; ++end) { // the lanes going along the axis leave the road at its end, the ones going against it leave at its start auto const outwards{end == 1 ? axis.frame(1.0).tangent : -axis.frame(0.0).tangent}; - for (int pair = 1; pair <= std::min(m_forward, m_backward); ++pair) + for (int pair = 1; pair <= std::min(m_state.forward, m_state.backward); ++pair) { - auto const forward{static_cast(m_backward + pair - 1)}; - auto const backward{static_cast(m_backward - pair)}; - auto *exit{m_lanes[end == 1 ? forward : backward].track}; - auto *entry{m_lanes[end == 1 ? backward : forward].track}; + auto const forward{static_cast(m_state.backward + pair - 1)}; + auto const backward{static_cast(m_state.backward - pair)}; + auto *exit{m_tracks[end == 1 ? forward : backward]}; + auto *entry{m_tracks[end == 1 ? backward : forward]}; if (exit == nullptr || entry == nullptr || exit->trNext != nullptr || entry->trPrev != nullptr) { continue; @@ -534,7 +702,7 @@ void road_node::close_ends() path.points[segment_data::point::control1] = outwards * std::max(turn_reach, 1.5 * span); path.points[segment_data::point::control2] = outwards * std::max(turn_reach, 1.5 * span); path.radius = static_cast(std::max(2.0, 0.5 * span)); - auto *turn{create_track(m_name.empty() ? std::string{} : m_name + ":" + (end == 1 ? "endturn" : "startturn") + std::to_string(pair), path, m_lanes[forward].width, turn_velocity)}; + auto *turn{create_track(m_name.empty() ? std::string{} : m_name + ":" + (end == 1 ? "endturn" : "startturn") + std::to_string(pair), path, m_state.lanes[forward].width, turn_velocity)}; exit->ConnectNextPrev(turn, 0); turn->ConnectNextPrev(entry, 0); m_turns.emplace_back(turn); @@ -542,6 +710,39 @@ void road_node::close_ends() } } +// gives up the paths of the lanes +std::vector road_node::release_lanes() +{ + std::vector released; + released.swap(m_tracks); + released.erase(std::remove(released.begin(), released.end(), nullptr), released.end()); + return released; +} + +// gives up the paths made to close the loose ends +std::vector road_node::release_turns() +{ + std::vector released; + released.swap(m_turns); + return released; +} + +// true if there's a vehicle on any path of the road +bool road_node::occupied() const +{ + for (auto const *tracks : {&m_tracks, &m_turns}) + { + for (auto const *track : *tracks) + { + if (track != nullptr && false == track->Dynamics.empty()) + { + return true; + } + } + } + return false; +} + // generates geometry of the surface, the sides and the markings std::vector road_node::create_shapes() const { @@ -567,22 +768,24 @@ std::vector road_node::create_shapes() const return batches.back().vertices; }; - road_axis const axis{m_axis}; + auto const &road{m_state}; + road_axis const axis{road.axis}; auto const length{axis.length()}; auto const step{axis.step()}; - auto const halfwidth{0.5 * width()}; + auto const halfwidth{0.5 * road.width()}; + double const texturelength{road.texturelength}; // surface. the image is centered on the axis, and repeated if the road is wider - if (m_surface != "none") + if (road.surface != "none") { - auto const tile{static_cast(texture_ratio(m_surface) * m_texturelength)}; - loft(vertices(m_surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast(0.5 - halfwidth / tile)}}, 0.0, length, step, m_texturelength); + auto const tile{static_cast(texture_ratio(road.surface)) * texturelength}; + loft(vertices(road.surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast(0.5 - halfwidth / tile)}}, 0.0, length, step, texturelength); } // sides for (int side = 0; side < 2; ++side) { - auto const &data{m_sides[side]}; + auto const &data{road.sides[side]}; if (data.type == side_type::none || data.width <= 0.f || data.material.empty() || data.material == "none") { continue; @@ -595,37 +798,37 @@ std::vector road_node::create_shapes() const // the image is laid out like the one of the legacy roads: the edge of the surface on its right, the bank on its left half profile.push_back({direction * halfwidth, 0.0, 1.f}); profile.push_back({direction * (halfwidth + data.width), 0.0, 0.5f}); - if (m_slope.x > 0.f) + if (road.slope.x > 0.f) { - profile.push_back({direction * (halfwidth + data.width + m_slope.x), -m_slope.y, 0.f}); + profile.push_back({direction * (halfwidth + data.width + road.slope.x), -road.slope.y, 0.f}); } } else { // the image starts at the foot of the kerb, and is laid out in its own proportions - auto const tile{static_cast(texture_ratio(data.material) * m_texturelength)}; + auto const tile{static_cast(texture_ratio(data.material)) * texturelength}; profile.push_back({direction * halfwidth, 0.0, 0.f}); - profile.push_back({direction * halfwidth, m_kerbheight, static_cast(m_kerbheight / tile)}); - profile.push_back({direction * (halfwidth + data.width), m_kerbheight, static_cast((m_kerbheight + data.width) / tile)}); + profile.push_back({direction * halfwidth, road.kerbheight, static_cast(road.kerbheight / tile)}); + profile.push_back({direction * (halfwidth + data.width), road.kerbheight, static_cast((road.kerbheight + data.width) / tile)}); } if (side == 0) { // the loft takes the corners left to right std::reverse(profile.begin(), profile.end()); } - loft(vertices(data.material, lighting_data{}), axis, profile, 0.0, length, step, m_texturelength); + loft(vertices(data.material, lighting_data{}), axis, profile, 0.0, length, step, texturelength); } // markings. these are painted with plain colour instead of an image - if (m_markings != marking_colour::none) + if (road.markings != marking_colour::none) { lighting_data paint; - paint.diffuse = (m_markings == marking_colour::white ? glm::vec4{0.92f, 0.92f, 0.92f, 1.f} : glm::vec4{0.95f, 0.5f, 0.08f, 1.f}); + paint.diffuse = (road.markings == marking_colour::white ? glm::vec4{0.92f, 0.92f, 0.92f, 1.f} : glm::vec4{0.95f, 0.5f, 0.08f, 1.f}); paint.ambient = paint.diffuse; auto &lines{vertices("colored", paint)}; auto const solid = [&](double const Offset) { - loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, 0.0, length, std::min(step, dash_step), m_texturelength); + loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, 0.0, length, std::min(step, dash_step), texturelength); }; // the dashes are fitted so the gaps at both ends of the road are a half of the regular one, which makes them match the next piece auto const dashed = [&](double const Offset) { @@ -634,7 +837,7 @@ std::vector road_node::create_shapes() const for (int dash = 0; dash < count; ++dash) { auto const from{(dash + 1.0 / 3.0) * period}; - loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, from, from + period / 3.0, dash_step, m_texturelength); + loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, from, from + period / 3.0, dash_step, texturelength); } }; @@ -647,12 +850,12 @@ std::vector road_node::create_shapes() const } // lines between the lanes. a lane can be left across a dashed line, or across a pair of lines if the dashed one is on its side auto offset{halfwidth}; - for (std::size_t boundary = 0; boundary < m_changes.size(); ++boundary) + for (std::size_t boundary = 0; boundary < road.changes.size() && boundary < road.lanes.size(); ++boundary) { - offset -= m_lanes[boundary].width; - auto const opposite{m_backward > 0 && boundary + 1 == static_cast(m_backward)}; + offset -= road.lanes[boundary].width; + auto const opposite{road.backward > 0 && boundary + 1 == static_cast(road.backward)}; auto const pair{0.5 * (line_width + line_spacing)}; - switch (m_changes[boundary]) + switch (road.changes[boundary]) { case change_both: { @@ -704,113 +907,57 @@ std::vector road_node::create_shapes() const return shapes; } -// identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet -std::string road_node::lane_id(std::size_t const Lane) const +// puts geometry of the road in the scene as shapes of its own, which can be taken back +void road_node::show() { - auto const backward{static_cast(m_backward)}; - return Lane < backward ? "b" + std::to_string(backward - Lane) : "f" + std::to_string(Lane - backward + 1); + if (m_section != nullptr || m_merged || simulation::Region == nullptr || false == simulation::Region->point_inside(location())) + { + return; + } + auto shapes{create_shapes()}; + if (shapes.empty()) + { + return; + } + auto §ion{simulation::Region->section(location())}; + // the rest of the section has to be built by now, or it'd count our shapes as waiting for it too + section.create_geometry(); + if (m_bank.bank == 0 && m_bank.chunk == 0) + { + m_bank = GfxRenderer->Create_Bank(); + } + for (auto &shape : shapes) + { + // shapes held by a section are drawn relative to its centre + shape.origin(section.m_area.center); + auto const centre{shape.data().area.center}; + auto const radius{shape.radius()}; + shape.create_geometry(m_bank); + m_geometry.emplace_back(shape.data().geometry); + section.m_shapes.emplace_back(std::move(shape)); + // the section may need to reach further to keep the road from being culled at its edges + section.m_area.radius = std::max(section.m_area.radius, static_cast(glm::length(section.m_area.center - centre) + radius)); + } + m_section = §ion; } -// number of a lane in the left to right order, -1 if the road has no lane with such identifier -int road_node::lane_index(std::string const &Id) const +// takes back geometry put in the scene by show() +void road_node::hide() { - if (Id.size() < 2 || Id.size() > 3 || (Id[0] != 'f' && Id[0] != 'b')) + if (m_section == nullptr) { - return -1; + return; } - int number{0}; - for (std::size_t idx = 1; idx < Id.size(); ++idx) - { - if (Id[idx] < '0' || Id[idx] > '9') - { - return -1; - } - number = number * 10 + (Id[idx] - '0'); - } - if (number < 1 || number > (Id[0] == 'f' ? m_forward : m_backward)) - { - return -1; - } - return Id[0] == 'f' ? m_backward + number - 1 : m_backward - number; -} - -// permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise -int road_node::default_change(std::size_t const Boundary) const -{ - auto const opposite{m_backward > 0 && m_forward > 0 && Boundary + 1 == static_cast(m_backward)}; - if (false == opposite) - { - return change_both; - } - // overtaking across the oncoming lane is left for the simple two lane roads - return (m_forward == 1 && m_backward == 1) ? change_both : change_none; -} - -// distance of the middle of a lane from the axis, positive to the left when facing along the axis -double road_node::lane_offset(std::size_t const Lane) const -{ - auto offset{0.5 * width()}; - for (std::size_t idx = 0; idx < Lane && idx < m_lanes.size(); ++idx) - { - offset -= m_lanes[idx].width; - } - return Lane < m_lanes.size() ? offset - 0.5 * m_lanes[Lane].width : offset; -} - -// combined width of the lanes -double road_node::width() const -{ - double width{0.0}; - for (auto const &lane : m_lanes) - { - width += lane.width; - } - return width; -} - -// shape of the middle of a lane, laid out in the direction of travel -segment_data road_node::lane_path(std::size_t const Lane) const -{ - road_axis const axis{m_axis}; - auto const offset{lane_offset(Lane)}; - auto const begin{axis.frame(0.0)}; - auto const end{axis.frame(1.0)}; - - segment_data path; - path.points[segment_data::point::start] = begin.position + begin.left * offset; - path.points[segment_data::point::end] = end.position + end.left * offset; - if (axis.curved()) - { - // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors - path.points[segment_data::point::control1] = axis.control(0) * std::max(0.05, 1.0 - offset * std::cos(begin.roll) * axis.curvature(0.0)); - path.points[segment_data::point::control2] = axis.control(1) * std::max(0.05, 1.0 - offset * std::cos(end.roll) * axis.curvature(1.0)); - // if the roll changes along the road its sides rise or fall relative to the axis - auto const climb{offset * (std::sin(end.roll) - std::sin(begin.roll)) / 3.0}; - path.points[segment_data::point::control1].y += climb; - path.points[segment_data::point::control2].y -= climb; - } - path.rolls = m_axis.rolls; - if (m_axis.radius != 0.f) - { - // the lanes on the inner side of the bend have it tighter - auto const turn{axis.curvature(0.5) >= 0.0 ? 1.0 : -1.0}; - path.radius = static_cast(std::max(1.0, std::abs(m_axis.radius) - turn * offset)); - } - if (Lane < static_cast(m_backward)) - { - // the lane goes against the axis - std::swap(path.points[segment_data::point::start], path.points[segment_data::point::end]); - std::swap(path.points[segment_data::point::control1], path.points[segment_data::point::control2]); - path.rolls = {-m_axis.rolls[1], -m_axis.rolls[0]}; - } - if (Global.bRollFix) - { - // with this setting on a rolled path gets raised when it's set up, by an amount fit for a railway track. - // the surface of the road stays where it is, so the lane is lowered beforehand to make up for it - path.points[segment_data::point::start].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[0])))) * 0.75; - path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[1])))) * 0.75; - } - return path; + // our shapes are told from the others by their geometry. the renderer reclaims the chunks once they're no longer drawn + auto &shapes{m_section->m_shapes}; + shapes.erase(std::remove_if(shapes.begin(), shapes.end(), + [this](scene::shape_node const &Shape) { + auto const handle{Shape.data().geometry}; + return std::any_of(m_geometry.begin(), m_geometry.end(), [&handle](gfx::geometry_handle const &Own) { return Own.bank == handle.bank && Own.chunk == handle.chunk; }); + }), + shapes.end()); + m_geometry.clear(); + m_section = nullptr; } // creates a path for the vehicles and registers it with the simulation @@ -823,7 +970,7 @@ TTrack *road_node::create_track(std::string const &Name, segment_data const &Pat // the path is put together the way the scenery defines an invisible road, to have it set up by the code which loads these std::ostringstream text; text.precision(std::numeric_limits::digits10); - text << "road " << glm::distance(start, end) << ' ' << Width << ' ' << m_friction << ' ' << m_sounddistance << ' ' << m_quality << " 0 " << m_environment << " unvis " << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' + text << "road " << glm::distance(start, end) << ' ' << Width << ' ' << m_state.friction << ' ' << m_state.sounddistance << ' ' << m_state.quality << " 0 " << m_state.environment << " unvis " << 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 << ' '; if (Velocity > 0.f) { @@ -841,7 +988,11 @@ TTrack *road_node::create_track(std::string const &Name, segment_data const &Pat track->m_road = this; if (false == simulation::Paths.insert(track)) { - ErrorLog("Bad scenario: duplicate track name \"" + track->name() + "\" generated for a road"); + // NOTE: the table points the name at the newest path, which is what a road made anew in the editor needs + if (false == m_editorremoved && false == dirty()) + { + ErrorLog("Bad scenario: duplicate track name \"" + track->name() + "\" generated for a road"); + } } simulation::Region->insert_and_register(track); return track; @@ -850,7 +1001,7 @@ TTrack *road_node::create_track(std::string const &Name, segment_data const &Pat // radius() subclass details, calculates node's bounding radius float road_node::radius_() { - return static_cast(0.5 * road_axis{m_axis}.length() + 0.5 * width() + std::max(m_sides[0].width, m_sides[1].width) + m_slope.x); + return static_cast(0.5 * m_state.length() + 0.5 * m_state.width() + std::max(m_state.sides[0].width, m_state.sides[1].width) + m_state.slope.x); } // serialize() subclass details, sends content of the subclass to provided stream @@ -868,55 +1019,56 @@ void road_node::deserialize_(std::istream &Input) // export() subclass details, sends basic content of the class in legacy (text) format to provided stream void road_node::export_as_text_(std::ostream &Output) const { + auto const &road{m_state}; Output << "road "; // axis auto const precision{Output.precision(std::numeric_limits::digits10)}; - Output << m_axis.points[segment_data::point::start].x << ' ' << m_axis.points[segment_data::point::start].y << ' ' << m_axis.points[segment_data::point::start].z << ' ' << m_axis.rolls[0] << ' ' - << m_axis.points[segment_data::point::control1].x << ' ' << m_axis.points[segment_data::point::control1].y << ' ' << m_axis.points[segment_data::point::control1].z << ' ' - << m_axis.points[segment_data::point::control2].x << ' ' << m_axis.points[segment_data::point::control2].y << ' ' << m_axis.points[segment_data::point::control2].z << ' ' - << m_axis.points[segment_data::point::end].x << ' ' << m_axis.points[segment_data::point::end].y << ' ' << m_axis.points[segment_data::point::end].z << ' ' << m_axis.rolls[1] << ' ' - << m_axis.radius << ' '; + Output << road.axis.points[segment_data::point::start].x << ' ' << road.axis.points[segment_data::point::start].y << ' ' << road.axis.points[segment_data::point::start].z << ' ' << road.axis.rolls[0] << ' ' + << road.axis.points[segment_data::point::control1].x << ' ' << road.axis.points[segment_data::point::control1].y << ' ' << road.axis.points[segment_data::point::control1].z << ' ' + << road.axis.points[segment_data::point::control2].x << ' ' << road.axis.points[segment_data::point::control2].y << ' ' << road.axis.points[segment_data::point::control2].z << ' ' + << road.axis.points[segment_data::point::end].x << ' ' << road.axis.points[segment_data::point::end].y << ' ' << road.axis.points[segment_data::point::end].z << ' ' << road.axis.rolls[1] << ' ' + << road.axis.radius << ' '; Output.precision(precision); // lanes - Output << "lanes " << m_forward << ' ' << m_backward << ' ' << "width " << m_lanewidth << ' '; - if (m_velocity > 0.f) + Output << "lanes " << road.forward << ' ' << road.backward << ' ' << "width " << road.lanewidth << ' '; + if (road.velocity > 0.f) { - Output << "velocity " << m_velocity << ' '; + Output << "velocity " << road.velocity << ' '; } - for (std::size_t lane = 0; lane < m_lanes.size(); ++lane) + for (std::size_t lane = 0; lane < road.lanes.size(); ++lane) { - if (m_lanes[lane].width != m_lanewidth) + if (road.lanes[lane].width != road.lanewidth) { - Output << "lane " << lane_id(lane) << " width " << m_lanes[lane].width << ' '; + Output << "lane " << road.lane_id(lane) << " width " << road.lanes[lane].width << ' '; } - if (m_lanes[lane].velocity != m_velocity) + if (road.lanes[lane].velocity != road.velocity) { - Output << "lane " << lane_id(lane) << " velocity " << m_lanes[lane].velocity << ' '; + Output << "lane " << road.lane_id(lane) << " velocity " << road.lanes[lane].velocity << ' '; } } - for (std::size_t boundary = 0; boundary < m_changes.size(); ++boundary) + for (std::size_t boundary = 0; boundary < road.changes.size(); ++boundary) { - if (m_changes[boundary] == default_change(boundary)) + if (road.changes[boundary] == road.default_change(boundary)) { continue; } - Output << "change " << lane_id(boundary) << ' ' << lane_id(boundary + 1) << ' ' << (m_changes[boundary] == change_both ? "both" : m_changes[boundary] == change_toright ? "ab" : m_changes[boundary] == change_toleft ? "ba" : "none") << ' '; + Output << "change " << road.lane_id(boundary) << ' ' << road.lane_id(boundary + 1) << ' ' << (road.changes[boundary] == change_both ? "both" : road.changes[boundary] == change_toright ? "ab" : road.changes[boundary] == change_toleft ? "ba" : "none") << ' '; } // appearance - Output << "surface " << m_surface << ' ' << "texlength " << m_texturelength << ' '; + Output << "surface " << road.surface << ' ' << "texlength " << road.texturelength << ' '; char const *sidenames[]{"left", "right"}; for (int side = 0; side < 2; ++side) { - auto const &data{m_sides[side]}; + auto const &data{road.sides[side]}; if (data.type == side_type::none) { continue; } Output << "side " << sidenames[side] << ' ' << (data.type == side_type::shoulder ? "shoulder" : "sidewalk") << ' ' << data.width << ' ' << (data.material.empty() ? "none" : data.material) << ' '; } - Output << "kerb " << m_kerbheight << ' ' << "slope " << m_slope.x << ' ' << m_slope.y << ' ' << "markings " << (m_markings == marking_colour::white ? "white" : m_markings == marking_colour::orange ? "orange" : "none") << ' '; + Output << "kerb " << road.kerbheight << ' ' << "slope " << road.slope.x << ' ' << road.slope.y << ' ' << "markings " << (road.markings == marking_colour::white ? "white" : road.markings == marking_colour::orange ? "orange" : "none") << ' '; // properties passed on to the lanes - Output << "friction " << m_friction << ' ' << "environment " << m_environment << ' '; + Output << "friction " << road.friction << ' ' << "environment " << road.environment << ' '; // footer Output << "endroad" << "\n"; @@ -934,7 +1086,7 @@ void road_table::InitRoads() } } -// generates geometry of the roads and inserts it in the region +// generates geometry of the roads and puts it in the scene void road_table::create_geometry(scene::scratch_data &Scratchpad) { if (simulation::Region == nullptr) @@ -947,11 +1099,20 @@ void road_table::create_geometry(scene::scratch_data &Scratchpad) { continue; } + if (Global.editor_session && false == Global.NvRenderer) + { + // scenery opened for editing: the road keeps its geometry apart, so it can be changed later. + // NOTE: the experimental renderer draws only what the scenery had when it was loaded + road->show(); + continue; + } + // otherwise the geometry joins the rest of the scenery, which lets it be drawn along with it at no extra cost for (auto &shape : road->create_shapes()) { - // the geometry is already where it belongs, so the placement set by the scenery isn't to be applied to it + // it's already where it belongs, so the placement set by the scenery isn't to be applied to it simulation::Region->insert(shape, Scratchpad, false); } + road->merged(true); } } diff --git a/world/Road.h b/world/Road.h index 5d4184c1..c444ab8a 100644 --- a/world/Road.h +++ b/world/Road.h @@ -23,6 +23,7 @@ http://mozilla.org/MPL/2.0/. namespace scene { struct scratch_data; +class basic_section; } // a piece of a road for wheeled traffic: an axis, and a cross-section which stays the same along it. @@ -60,7 +61,6 @@ class road_node : public scene::basic_node { float width{3.5f}; float velocity{-1.f}; // speed limit, none if negative - TTrack *track{nullptr}; // the generated path; owned by the path table, like every other track }; struct side_data { @@ -68,37 +68,96 @@ class road_node : public scene::basic_node float width{0.f}; std::string material; }; + // everything the scenery says about the road. the lanes go left to right when facing along the axis: + // the ones going against it, outermost first, then the ones going along it + struct state + { + segment_data axis; + int forward{1}; // number of lanes going along the axis + int backward{1}; // number of lanes going against the axis + float lanewidth{3.5f}; // what a lane gets unless it's told otherwise + float velocity{-1.f}; + std::vector lanes; + std::vector changes; // permissions to change the lane, one for each pair of neighbouring lanes + std::string surface{"asphaltdark1"}; + float texturelength{4.f}; + std::array sides; // left, right + float kerbheight{0.12f}; + glm::vec2 slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder + marking_colour markings{marking_colour::white}; + float friction{0.85f}; + float sounddistance{25.f}; + int quality{15}; + std::string environment{"flat"}; + + // brings the content to a usable form: values within their limits, an entry for each lane and for each pair of neighbours + void normalize(); + // identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet + std::string lane_id(std::size_t const Lane) const; + // number of a lane in the left to right order, -1 if there's no lane with such identifier + int lane_index(std::string const &Id) const; + // permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise + int default_change(std::size_t const Boundary) const; + // distance of the middle of a lane from the axis, positive to the left when facing along the axis + double lane_offset(std::size_t const Lane) const; + // combined width of the lanes + double width() const; + // shape of the middle of a lane, laid out in the direction of travel + segment_data lane_path(std::size_t const Lane) const; + // length of the axis + double length() const; + // position on the axis and direction of the axis, for specified value of the curve parameter + glm::dvec3 point(double const T) const; + glm::dvec3 tangent(double const T) const; + }; // constructors explicit road_node(scene::node_data const &Nodedata); // methods // restores content of the node from provided input stream; reads up to and including the closing 'endroad' void import(cParser &Input, glm::dvec3 const &Offset); + state const &definition() const + { + return m_state; + } + // replaces what the node knows about the road. the lanes and the geometry stay the way they were made, + // it's up to the caller to take them down beforehand and make them again + void define(state const &State); // creates paths of the lanes and hands them over to the simulation void create_lanes(); // links lanes of opposite directions at an end of the road which leads nowhere, so the vehicles can turn back void close_ends(); + // gives up the paths of the lanes, or the ones made to close the loose ends. the paths belong to the path table, + // the caller is expected to take them out of use + std::vector release_lanes(); + std::vector release_turns(); + // paths of the lanes, in the order of the lanes, and the ones made to close the loose ends + std::vector const &tracks() const + { + return m_tracks; + } + std::vector const &turns() const + { + return m_turns; + } + // true if there's a vehicle on any path of the road + bool occupied() const; // generates geometry of the surface, the sides and the markings std::vector create_shapes() const; - // identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet - std::string lane_id(std::size_t const Lane) const; - // distance of the middle of a lane from the axis, positive to the left when facing along the axis - double lane_offset(std::size_t const Lane) const; - // combined width of the lanes - double width() const; - // lanes, left to right when facing along the axis: the ones going against it, outermost first, then the ones going along it - std::vector const &lanes() const + // puts geometry of the road in the scene as shapes of its own, which can be taken back + void show(); + // takes back geometry put in the scene by show() + void hide(); + // marks geometry of the road as mixed with other shapes of the scenery; such road can't be changed anymore + void merged(bool const Merged) { - return m_lanes; + m_merged = Merged; } - // permissions to change the lane, one for each pair of neighbouring lanes - std::vector const &lane_changes() const + bool merged() const { - return m_changes; - } - segment_data const &axis() const - { - return m_axis; + return m_merged; } + // members + bool m_editorremoved{false}; // removed in the editor; kept around so the removal can be undone private: // methods @@ -110,33 +169,17 @@ class road_node : public scene::basic_node void deserialize_(std::istream &Input) override; // export() subclass details, sends basic content of the class in legacy (text) format to provided stream void export_as_text_(std::ostream &Output) const override; - // number of a lane in the left to right order, -1 if the road has no lane with such identifier - int lane_index(std::string const &Id) const; - // permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise - int default_change(std::size_t const Boundary) const; - // shape of the middle of a lane, laid out in the direction of travel - segment_data lane_path(std::size_t const Lane) const; // creates a path for the vehicles and registers it with the simulation TTrack *create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity); // members - segment_data m_axis; - int m_forward{1}; // number of lanes going along the axis - int m_backward{1}; // number of lanes going against the axis - float m_lanewidth{3.5f}; - float m_velocity{-1.f}; - std::vector m_lanes; - std::vector m_changes; - std::string m_surface{"asphaltdark1"}; - float m_texturelength{4.f}; - std::array m_sides; // left, right - float m_kerbheight{0.12f}; - glm::vec2 m_slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder - marking_colour m_markings{marking_colour::white}; - float m_friction{0.85f}; - float m_sounddistance{25.f}; - int m_quality{15}; - std::string m_environment{"flat"}; - std::vector m_turns; // paths made to close loose ends of the road + state m_state; + std::vector m_tracks; + std::vector m_turns; + bool m_merged{false}; + // geometry put in the scene by show() + gfx::geometrybank_handle m_bank{0, 0}; + std::vector m_geometry; + scene::basic_section *m_section{nullptr}; }; // collection of roads present in the scene @@ -146,7 +189,7 @@ class road_table : public basic_table public: // legacy style initialization, to be performed when the tracks are already joined void InitRoads(); - // generates geometry of the roads and inserts it in the region + // generates geometry of the roads and puts it in the scene void create_geometry(scene::scratch_data &Scratchpad); };