17
0
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:
2026-10-03 18:21:10 +02:00
parent 2f417fddcc
commit e4083d507f
12 changed files with 2094 additions and 249 deletions

View File

@@ -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"

View File

@@ -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);

View File

@@ -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;
};

View 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());
}

View File

@@ -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)

View File

@@ -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
View 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
View 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);
};

View File

@@ -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

View File

@@ -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 );
}
}

View File

@@ -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 &section{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 = &section;
}
// 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);
}
}

View File

@@ -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);
};