mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 02:49:19 +02:00
feat(roads): editor menu and window for building roads, lanes shown as trajectories
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<world_triangle> &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);
|
||||
|
||||
|
||||
@@ -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 <array>
|
||||
#include <chrono>
|
||||
@@ -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<std::pair<TTrack *, editor_track::state>> tracks;
|
||||
std::vector<TTrack *> created;
|
||||
std::vector<TTrack *> removed;
|
||||
std::vector<std::pair<road_node *, road_node::state>> roads; // road pieces with what they were like before the change
|
||||
std::vector<road_node *> roads_created;
|
||||
std::vector<road_node *> 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<segment_data> road_preview(editor_road::loose_end &Snap, std::string &Error) const;
|
||||
void push_road_snapshot(std::vector<std::pair<road_node *, road_node::state>> States, std::vector<road_node *> Created, std::vector<road_node *> Removed);
|
||||
void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo);
|
||||
road_tool m_roadtool;
|
||||
};
|
||||
|
||||
839
application/editormoderoad.cpp
Normal file
839
application/editormoderoad.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
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<char>(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<segment_data> 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<road_node::state> 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<road_node *>{tool.selected}};
|
||||
std::vector<std::pair<road_node *, road_node::state>> before;
|
||||
std::vector<std::pair<road_node *, road_node::state>> 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<std::pair<road_node *, road_node::state>> States, std::vector<road_node *> Created, std::vector<road_node *> 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<EditorSnapshot> &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<std::string const *> 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<int>(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<int>(lane));
|
||||
auto &data{State.lanes[lane]};
|
||||
auto const id{State.lane_id(lane)};
|
||||
ImGui::TextColored(lane >= static_cast<std::size_t>(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<double>(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<std::size_t>(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<double>(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());
|
||||
}
|
||||
@@ -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<void()> Save, std::function<void()> Export)
|
||||
|
||||
@@ -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<void()> 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<void()> Renderer)
|
||||
{
|
||||
@@ -82,6 +87,7 @@ class editor_ui : public ui_layer
|
||||
model_set_ref m_insertset;
|
||||
std::function<void()> m_filloptions;
|
||||
std::function<void()> m_trackoptions;
|
||||
std::function<void()> m_menuoptions;
|
||||
std::function<void()> m_gizmooptions;
|
||||
scene::basic_node *m_node{nullptr}; // currently bound scene node, if any
|
||||
};
|
||||
|
||||
564
editor/editorRoad.cpp
Normal file
564
editor/editorRoad.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
|
||||
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<road_node *> &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<road_node *> roads_at(glm::dvec3 const &Point, road_node const *Exclude)
|
||||
{
|
||||
std::vector<road_node *> 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<TTrack *> 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<road_node *> editor_road::neighbours(road_node const &Road)
|
||||
{
|
||||
std::vector<road_node *> 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 <class Change_>
|
||||
void editor_road::refresh(std::vector<road_node *> const &Roads, Change_ Change)
|
||||
{
|
||||
// the pieces next to the changed ones keep their shape, but what they're joined with changes
|
||||
std::vector<road_node *> 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<TTrack *> 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<road_node *> editor_road::create(std::vector<road_node::state> const &States)
|
||||
{
|
||||
static int counter{0};
|
||||
std::vector<road_node *> 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<std::pair<road_node *, road_node::state>> const &Changes)
|
||||
{
|
||||
std::vector<road_node *> 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<road_node *> const &Roads)
|
||||
{
|
||||
std::vector<road_node *> 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<road_node *> const &Roads)
|
||||
{
|
||||
std::vector<road_node *> 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<float>(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<road_node *> editor_road::chain(road_node &Road)
|
||||
{
|
||||
std::vector<road_node *> 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<double>::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<segment_data> 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<segment_data> 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<int>(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<double>(i) / count);
|
||||
piece.points[segment_data::point::end] = glm::mix(Start, End, static_cast<double>(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<float>(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<int>(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<float>(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<std::string> const &editor_road::materials()
|
||||
{
|
||||
static std::vector<std::string> names;
|
||||
static bool scanned{false};
|
||||
if (scanned)
|
||||
return names;
|
||||
scanned = true;
|
||||
std::set<std::string> 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<char>(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<char>(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;
|
||||
}
|
||||
65
editor/editorRoad.hpp
Normal file
65
editor/editorRoad.hpp
Normal file
@@ -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 <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#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<road_node *> create(std::vector<road_node::state> const &States);
|
||||
// replaces definitions of the pieces and makes their lanes and geometry anew
|
||||
static void apply(std::vector<std::pair<road_node *, road_node::state>> const &Changes);
|
||||
// takes the pieces out of the scenery. they're kept around, so they can be brought back
|
||||
static void remove(std::vector<road_node *> const &Roads);
|
||||
static void revive(std::vector<road_node *> 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<road_node *> 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<segment_data> 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<std::string> const &materials();
|
||||
|
||||
private:
|
||||
// takes down what depends on shape of the listed pieces, lets the caller change them, then builds it all again
|
||||
template <class Change_>
|
||||
static void refresh(std::vector<road_node *> const &Roads, Change_ Change);
|
||||
static std::vector<road_node *> neighbours(road_node const &Road);
|
||||
};
|
||||
@@ -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<std::string> 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<road_node *> savedroads;
|
||||
std::vector<road_node const *> 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<std::string> 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
|
||||
|
||||
@@ -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<std::string, scene::layer_item> 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 );
|
||||
}
|
||||
}
|
||||
|
||||
569
world/Road.cpp
569
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<std::string, 7> 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<std::size_t>(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<int>(change_none), static_cast<int>(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<std::size_t>(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<std::size_t>(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<float>(std::max(1.0, std::abs(axis.radius) - turn * offset));
|
||||
}
|
||||
if (Lane < static_cast<std::size_t>(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<double>(path.rolls[0])))) * 0.75;
|
||||
path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast<double>(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 <along the axis> <against the axis>
|
||||
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<std::string>();
|
||||
replace_slashes(m_surface);
|
||||
m_state.surface = Input.getToken<std::string>();
|
||||
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 <width> <drop>
|
||||
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<std::string>()};
|
||||
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<std::string>();
|
||||
m_state.environment = Input.getToken<std::string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -407,37 +584,20 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset)
|
||||
token = Input.getToken<std::string>();
|
||||
}
|
||||
|
||||
// the paths of the lanes are loaded from text, so what goes into it has to be something they understand
|
||||
static std::array<std::string, 7> 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<std::size_t>(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<std::size_t>(m_backward + pair - 1)};
|
||||
auto const backward{static_cast<std::size_t>(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<std::size_t>(m_state.backward + pair - 1)};
|
||||
auto const backward{static_cast<std::size_t>(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<float>(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<TTrack *> road_node::release_lanes()
|
||||
{
|
||||
std::vector<TTrack *> 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<TTrack *> road_node::release_turns()
|
||||
{
|
||||
std::vector<TTrack *> 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<scene::shape_node> road_node::create_shapes() const
|
||||
{
|
||||
@@ -567,22 +768,24 @@ std::vector<scene::shape_node> 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<double>(texture_ratio(m_surface) * m_texturelength)};
|
||||
loft(vertices(m_surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast<float>(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast<float>(0.5 - halfwidth / tile)}}, 0.0, length, step, m_texturelength);
|
||||
auto const tile{static_cast<double>(texture_ratio(road.surface)) * texturelength};
|
||||
loft(vertices(road.surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast<float>(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast<float>(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<scene::shape_node> 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<double>(texture_ratio(data.material) * m_texturelength)};
|
||||
auto const tile{static_cast<double>(texture_ratio(data.material)) * texturelength};
|
||||
profile.push_back({direction * halfwidth, 0.0, 0.f});
|
||||
profile.push_back({direction * halfwidth, m_kerbheight, static_cast<float>(m_kerbheight / tile)});
|
||||
profile.push_back({direction * (halfwidth + data.width), m_kerbheight, static_cast<float>((m_kerbheight + data.width) / tile)});
|
||||
profile.push_back({direction * halfwidth, road.kerbheight, static_cast<float>(road.kerbheight / tile)});
|
||||
profile.push_back({direction * (halfwidth + data.width), road.kerbheight, static_cast<float>((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<scene::shape_node> 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<scene::shape_node> 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<std::size_t>(m_backward)};
|
||||
offset -= road.lanes[boundary].width;
|
||||
auto const opposite{road.backward > 0 && boundary + 1 == static_cast<std::size_t>(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<scene::shape_node> 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<std::size_t>(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<float>(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<std::size_t>(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<float>(std::max(1.0, std::abs(m_axis.radius) - turn * offset));
|
||||
}
|
||||
if (Lane < static_cast<std::size_t>(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<double>(path.rolls[0])))) * 0.75;
|
||||
path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast<double>(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<double>::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<float>(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<float>(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<double>::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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
127
world/Road.h
127
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<lane_data> lanes;
|
||||
std::vector<int> changes; // permissions to change the lane, one for each pair of neighbouring lanes
|
||||
std::string surface{"asphaltdark1"};
|
||||
float texturelength{4.f};
|
||||
std::array<side_data, 2> 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<TTrack *> release_lanes();
|
||||
std::vector<TTrack *> release_turns();
|
||||
// paths of the lanes, in the order of the lanes, and the ones made to close the loose ends
|
||||
std::vector<TTrack *> const &tracks() const
|
||||
{
|
||||
return m_tracks;
|
||||
}
|
||||
std::vector<TTrack *> 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<scene::shape_node> 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<lane_data> 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<int> 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<lane_data> m_lanes;
|
||||
std::vector<int> m_changes;
|
||||
std::string m_surface{"asphaltdark1"};
|
||||
float m_texturelength{4.f};
|
||||
std::array<side_data, 2> 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<TTrack *> m_turns; // paths made to close loose ends of the road
|
||||
state m_state;
|
||||
std::vector<TTrack *> m_tracks;
|
||||
std::vector<TTrack *> m_turns;
|
||||
bool m_merged{false};
|
||||
// geometry put in the scene by show()
|
||||
gfx::geometrybank_handle m_bank{0, 0};
|
||||
std::vector<gfx::geometry_handle> 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<road_node>
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user