17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 10:59:20 +02:00

feat(roads): junctions, height above the ground and rounded profile following the terrain

This commit is contained in:
2026-10-03 20:28:37 +02:00
parent e4083d507f
commit daeed40f15
13 changed files with 1963 additions and 103 deletions

View File

@@ -231,11 +231,13 @@ namespace
};
// appends triangles of a shape node which overlap specified XZ rectangle
void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out)
void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out, bool const Skiproads = false)
{
auto const &data = Shape.data();
if (editor_orthophoto::owns(data.geometry))
return; // the orthophoto laid over the ground isn't ground itself
if (Skiproads && owned_shapes::owns(data.geometry))
return; // neither are the roads, for the ones which ask where to put a road
if (data.area.radius >= 0.0f)
{
double const r = data.area.radius;
@@ -275,7 +277,7 @@ namespace
// ground triangles overlapping an XZ rectangle (Min/Max are x,z). shapes hold the (legacy) terrain,
// model instances at least ModelRadius large (if ModelsAsGround) the terrain tiles
void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector<world_triangle> &Out)
void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector<world_triangle> &Out, bool const Skiproads = false)
{
glm::dvec2 const center = (Min + Max) * 0.5;
float const radius = static_cast<float>(glm::length(Max - Min) * 0.5);
@@ -283,7 +285,7 @@ namespace
for (auto *section : sections)
{
for (auto const &shape : section->m_shapes)
gather_shape_triangles(shape, Min, Max, Out);
gather_shape_triangles(shape, Min, Max, Out, Skiproads);
for (auto &cell : section->m_cells)
{
double const r = cell.m_area.radius;
@@ -349,6 +351,71 @@ editor_mode::editor_mode() {
});
}
std::vector<double> editor_mode::ground_heights(std::vector<glm::dvec3> const &Points, bool const Fresh)
{
std::vector<double> heights;
heights.reserve(Points.size());
for (auto const &point : Points)
heights.push_back(point.y);
if (Points.empty() || simulation::Region == nullptr)
return heights;
glm::dvec2 bmin{Points.front().x, Points.front().z};
glm::dvec2 bmax{bmin};
for (auto const &point : Points)
{
bmin = glm::min(bmin, glm::dvec2{point.x, point.z});
bmax = glm::max(bmax, glm::dvec2{point.x, point.z});
}
// the road tools ask about the same place frame after frame, so the ground is gathered with room to spare and kept for a moment
struct ground_cache
{
glm::dvec2 min{0.0};
glm::dvec2 max{0.0};
std::unique_ptr<triangle_grid> grid;
double time{0.0};
};
static ground_cache cache;
double constexpr cache_margin = 60.0;
double constexpr cache_lifetime = 5.0;
double const now = ImGui::GetTime();
if (Fresh || cache.grid == nullptr || now < cache.time || now - cache.time > cache_lifetime || bmin.x < cache.min.x || bmin.y < cache.min.y || bmax.x > cache.max.x || bmax.y > cache.max.y)
{
cache.min = bmin - glm::dvec2{cache_margin};
cache.max = bmax + glm::dvec2{cache_margin};
std::vector<world_triangle> triangles;
// the same ground as the area fill uses, terrain tile models included
gather_ground_triangles(cache.min, cache.max, true, 50.0f, triangles, true);
cache.grid = std::make_unique<triangle_grid>(std::move(triangles), cache.min, cache.max);
cache.time = now;
}
// surfaces much higher than the point are something it's under, rather than the ground
double constexpr height_tolerance = 50.0;
auto const terrains = active_terrains();
for (std::size_t i = 0; i < Points.size(); ++i)
{
auto const &p = Points[i];
double const ceiling = p.y + height_tolerance;
double y = 0.0;
bool found = cache.grid->height_at(p.x, p.z, ceiling, y);
for (editor_terrain *terrain : terrains)
{
if (!terrain->contains(p.x, p.z))
continue;
double const h = terrain->height_at(p.x, p.z);
if (h <= ceiling && (!found || h > y))
{
y = h;
found = true;
}
}
if (found)
heights[i] = y;
}
return heights;
}
editor_ui *editor_mode::ui() const
{
return static_cast<editor_ui *>(m_userinterface.get());

View File

@@ -110,6 +110,8 @@ class editor_mode : public application_mode
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;
std::vector<junction_node *> junctions_created;
std::vector<junction_node *> junctions_removed;
};
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
@@ -491,8 +493,9 @@ class editor_mode : public application_mode
{
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
int tool{0}; // 0: select, 1: build, 2: junction
road_node *selected{nullptr};
junction_node *junction{nullptr}; // selected junction
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
@@ -500,6 +503,18 @@ class editor_mode : public application_mode
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
double grade{0.0}; // slope the next piece has to start with, to carry on what it's attached to
bool hasgrade{false};
float offset{0.1f}; // height above the ground the roads and junctions are built at
bool follow{true}; // built roads are led over the ground between the clicked points
int kind{0}; // kind of the junctions being placed, an editor_road::junction_kind
float crossingspeed{30.f}; // speed limit on the junctions being placed
// what a click would make at the moment, worked out once a frame for display
std::vector<segment_data> preview;
junction_node::state previewjunction;
bool hasjunction{false};
editor_road::loose_end previewsnap;
std::string previewerror;
glm::dvec3 mouse{0.0};
std::string status;
char surface[128]{};
@@ -513,12 +528,20 @@ class editor_mode : public application_mode
void update_road_tool();
void road_click();
void road_select(road_node *Road);
void junction_select(junction_node *Junction);
void junction_click();
bool junction_preview(junction_node::state &State, editor_road::loose_end &Snap, std::string &Error) const;
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);
// Fresh: the ground is looked up anew instead of going by what was gathered for the previous frames
std::vector<segment_data> road_preview(editor_road::loose_end &Snap, std::string &Error, bool const Fresh);
void road_profile(std::vector<segment_data> &Pieces, editor_road::loose_end const &Snap, bool const Fresh);
// height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height
std::vector<double> ground_heights(std::vector<glm::dvec3> const &Points, bool const Fresh);
void push_road_snapshot(std::vector<std::pair<road_node *, road_node::state>> States, std::vector<road_node *> Created, std::vector<road_node *> Removed, std::vector<junction_node *> Junctionscreated = {},
std::vector<junction_node *> Junctionsremoved = {});
void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo);
road_tool m_roadtool;
};

View File

@@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/.
#include "imgui/imgui.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
namespace
@@ -108,10 +109,16 @@ std::string lower_case(std::string 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
double const kLongestStretch{2000.0}; // a single click doesn't lead a road further than this
// lengths of the pieces a road following the ground is tried with, from the longest; the first one to fit the ground is taken
double const kProfileSteps[]{50.0, 25.0, 12.5};
double const kSunkTolerance{0.05}; // how far under the height it's built at the road can get between the points it's led through
double const kRaisedTolerance{0.5}; // and how far over it
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 kWayColor{IM_COL32(120, 200, 255, 170)};
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)};
@@ -134,12 +141,29 @@ void editor_mode::update_road_tool()
auto &tool{m_roadtool};
if (tool.selected != nullptr && tool.selected->m_editorremoved)
tool.selected = nullptr;
if (tool.junction != nullptr && tool.junction->m_editorremoved)
tool.junction = nullptr;
tool.preview.clear();
tool.hasjunction = false;
tool.previewsnap = {};
tool.previewerror.clear();
if (false == tool.window)
{
tool.chain = false;
return;
}
tool.mouse = Global.pCamera.Pos + GfxRenderer->Mouse_Position();
glm::dvec3 const camera{Global.pCamera.Pos};
tool.mouse = camera + GfxRenderer->Mouse_Position();
if (glm::distance(tool.mouse, camera) > kReach)
{
tool.previewerror = "The cursor isn't over anything near enough";
return;
}
// what a click would make at the moment, to be drawn over the view
if (tool.tool == 1 && tool.chain)
tool.preview = road_preview(tool.previewsnap, tool.previewerror, false);
else if (tool.tool == 2)
tool.hasjunction = junction_preview(tool.previewjunction, tool.previewsnap, tool.previewerror);
}
void editor_mode::road_select(road_node *Road)
@@ -148,6 +172,7 @@ void editor_mode::road_select(road_node *Road)
tool.selected = Road;
if (Road != nullptr)
{
tool.junction = nullptr;
tool.settings = Road->definition();
std::string reason;
tool.status = "Selected: " + (Road->name().empty() ? std::string{"(unnamed road)"} : Road->name());
@@ -159,20 +184,33 @@ void editor_mode::road_select(road_node *Road)
copy_text(tool.sides[side], sizeof(tool.sides[side]), tool.settings.sides[side].material);
}
void editor_mode::junction_select(junction_node *Junction)
{
auto &tool{m_roadtool};
tool.junction = Junction;
if (Junction == nullptr)
return;
tool.selected = nullptr;
std::string reason;
tool.status = "Selected: " + Junction->name();
if (false == editor_road::can_edit(*Junction, &reason))
tool.status += ". It can't be removed: " + reason;
}
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
std::vector<segment_data> editor_mode::road_preview(editor_road::loose_end &Snap, std::string &Error, bool const Fresh)
{
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)
if (Snap.valid() && glm::distance(Snap.position, tool.point) < 0.5)
Snap = {};
glm::dvec2 enddirection{0.0, 1.0};
if (Snap.road != nullptr)
if (Snap.valid())
{
glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z};
if (glm::length(outwards) > 1e-6)
@@ -180,7 +218,204 @@ std::vector<segment_data> editor_mode::road_preview(editor_road::loose_end &Snap
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);
// in the open the road is put over what's under the cursor
auto const target{Snap.valid() ? Snap.position : tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}};
if (glm::distance(tool.point, target) > kLongestStretch)
{
Error = "Too far from the last point";
return {};
}
if (Snap.junction != nullptr)
{
// the junction has its ways through made for a set of lanes at each arm
auto const arriving{tool.reversed ? tool.settings.backward : tool.settings.forward};
auto const leaving{tool.reversed ? tool.settings.forward : tool.settings.backward};
if (arriving != Snap.backward || leaving != Snap.forward || std::abs(tool.settings.lanewidth - Snap.width) > 0.01f)
{
char width[32];
std::snprintf(width, sizeof(width), "%.2f", Snap.width);
Error = "This arm of the junction takes a road with " + std::to_string(Snap.backward) + " lane(s) leading into it and " + std::to_string(Snap.forward) + " out of it, " + width + " m wide each";
return {};
}
}
auto pieces{editor_road::plan(tool.point, tool.hasdirection ? &tool.direction : nullptr, target, Snap.valid() ? &enddirection : nullptr, Global.ctrlState, Error)};
road_profile(pieces, Snap, Fresh);
return pieces;
}
// gives the pieces laid out by the planner their heights: over the ground if the road is to follow it, with the slopes
// of the pieces matched where they meet, and with what the road is attached to
void editor_mode::road_profile(std::vector<segment_data> &Pieces, editor_road::loose_end const &Snap, bool const Fresh)
{
auto const &tool{m_roadtool};
if (Pieces.empty())
return;
// an end of a road or an arm of a junction is reached with the slope it has
double endgrade{0.0};
if (Snap.valid())
{
auto const run{glm::length(glm::dvec2{Snap.outwards.x, Snap.outwards.z})};
endgrade = (run > 1e-6 ? -Snap.outwards.y / run : 0.0);
}
auto const *firstgrade{tool.hasgrade ? &tool.grade : nullptr};
auto const *lastgrade{Snap.valid() ? &endgrade : nullptr};
if (false == tool.follow)
{
// straight from a point to the next one, the way a ramp or a viaduct goes
std::vector<double> heights;
for (auto const &piece : Pieces)
heights.emplace_back(piece.points[segment_data::point::start].y);
heights.emplace_back(Pieces.back().points[segment_data::point::end].y);
editor_road::profile(Pieces, heights, firstgrade, lastgrade);
return;
}
auto const planned{Pieces};
bool fresh{Fresh};
for (auto const step : kProfileSteps)
{
Pieces = editor_road::refine(planned, step);
auto const count{Pieces.size()};
// the ground is looked up where the pieces meet, and in the middle of each to see how the result fits it
std::vector<glm::dvec3> points;
points.reserve(2 * count + 1);
for (auto const &piece : Pieces)
points.emplace_back(piece.points[segment_data::point::start]);
points.emplace_back(Pieces.back().points[segment_data::point::end]);
for (auto const &piece : Pieces)
points.emplace_back(path_point(piece, 0.5));
// the planner leads the road between its ends, which are over the ground already
for (auto &point : points)
point.y -= tool.offset;
auto ground{ground_heights(points, fresh)};
fresh = false;
for (auto &height : ground)
height += tool.offset;
std::vector<double> heights(ground.begin(), ground.begin() + count + 1);
// the ends are where they were put
heights.front() = Pieces.front().points[segment_data::point::start].y;
heights.back() = Pieces.back().points[segment_data::point::end].y;
editor_road::profile(Pieces, heights, firstgrade, lastgrade);
// shorter pieces are called for if the ground shows through the road, or the road hangs over it.
// the pieces attached to something are let off, as they have to get to its height one way or another
bool fits{true};
for (std::size_t i = 0; i < count; ++i)
{
if ((i == 0 && firstgrade != nullptr) || (i + 1 == count && lastgrade != nullptr))
continue;
auto const over{path_point(Pieces[i], 0.5).y - ground[count + 1 + i]};
if (over < -kSunkTolerance || over > kRaisedTolerance)
{
fits = false;
break;
}
}
if (fits)
break;
}
}
// lays out the junction a click would make. returns: false if there's none to make, with the reason in Error unless
// it's a matter of the first click being still ahead
bool editor_mode::junction_preview(junction_node::state &State, editor_road::loose_end &Snap, std::string &Error) const
{
auto const &tool{m_roadtool};
auto const kind{static_cast<editor_road::junction_kind>(std::clamp(tool.kind, 0, 3))};
Snap = {};
junction_node::state state;
// the look goes after the road the junction is made for
auto const dress = [&state](road_node::state const &Layout) {
state.surface = Layout.surface;
state.texturelength = Layout.texturelength;
state.markings = Layout.markings;
state.friction = Layout.friction;
state.sounddistance = Layout.sounddistance;
state.quality = Layout.quality;
state.environment = Layout.environment;
};
if (false == tool.chain)
{
// at a loose end of a road the junction is made for that road, and put right past its end
Snap = editor_road::find_end(tool.mouse, kSnapRadius, false);
if (Snap.road == nullptr)
return false;
auto const &layout{Snap.road->definition()};
glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z};
if (glm::length(outwards) < 1e-6 || layout.lanes.empty())
{
Error = "There's no telling which way this road goes at its end";
return false;
}
// the ways through the junction start and end where the lanes of the road are expected, which takes lanes of the same width
auto const width{layout.lanes.front().width};
for (auto const &lane : layout.lanes)
{
if (std::abs(lane.width - width) > 0.01f)
{
Error = "Lanes of this road differ in width, and a junction takes roads with lanes of the same width";
return false;
}
}
state = editor_road::junction(kind, Snap.position, glm::normalize(outwards), Snap.atend ? layout.forward : layout.backward, Snap.atend ? layout.backward : layout.forward, width);
dress(layout);
}
else
{
// in the open the centre is set already, and the cursor shows where the first road comes from
glm::dvec2 const away{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
if (glm::length(away) < 1.0)
{
Error = "Move the cursor away from the centre to turn the junction";
return false;
}
auto const heading{-glm::normalize(away)};
auto const &layout{tool.settings};
auto const reach{editor_road::junction_reach(layout.forward + layout.backward, layout.lanewidth)};
glm::dvec3 const entry{tool.point.x - heading.x * reach, tool.point.y, tool.point.z - heading.y * reach};
state = editor_road::junction(kind, entry, heading, layout.forward, layout.backward, layout.lanewidth);
dress(layout);
}
state.velocity = tool.crossingspeed;
state.normalize();
State = state;
return true;
}
void editor_mode::junction_click()
{
auto &tool{m_roadtool};
junction_node::state state;
editor_road::loose_end snap;
std::string error;
if (false == junction_preview(state, snap, error))
{
if (false == error.empty())
{
tool.status = "Can't make the junction: " + error;
}
else if (false == tool.chain)
{
// away from the roads the first click sets the centre, the second one turns the junction
tool.point = tool.mouse + glm::dvec3{0.0, tool.offset, 0.0};
tool.chain = true;
tool.status = "Centre set";
}
return;
}
std::string reason;
if (snap.valid() && false == editor_road::can_join(snap, &reason))
{
tool.status = "Can't make the junction here: " + reason;
return;
}
auto const created{editor_road::create(std::vector<junction_node::state>{state})};
if (created.empty())
{
tool.status = "The junction wasn't created";
return;
}
push_road_snapshot({}, {}, {}, created, {});
tool.chain = false;
tool.status = "\"" + created.front()->name() + "\" made" + (snap.road != nullptr ? " at the end of \"" + snap.road->name() + "\"" : std::string{}) + ". Lead roads out of its arms with the Build tool";
}
void editor_mode::road_click()
@@ -197,9 +432,11 @@ void editor_mode::road_click()
if (tool.tool == 0)
{
auto *road{editor_road::nearest(ground, 1.5)};
if (road == nullptr)
auto *junction{road == nullptr ? editor_road::nearest_junction(ground) : nullptr};
if (road == nullptr && junction == nullptr)
tool.status = "No road here";
road_select(road);
junction_select(junction);
return;
}
@@ -209,30 +446,58 @@ void editor_mode::road_click()
tool.status = reason;
return;
}
if (tool.tool == 2)
{
junction_click();
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
// first click sets where the road starts. started from a loose end of a road it carries that road on,
// started from an arm of a junction it's the road the junction is made for there
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)
if (end.valid() && glm::length(outwards) > 1e-6)
{
if (false == editor_road::can_join(end, &reason))
{
tool.status = "Can't build from here: " + reason;
return;
}
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;
tool.grade = end.outwards.y / glm::length(outwards);
tool.hasgrade = true;
tool.junction = nullptr;
if (end.road != nullptr)
{
// 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);
tool.settings = end.road->definition();
tool.status = "Carrying on \"" + end.road->name() + "\" with its layout";
}
else
{
tool.reversed = false;
tool.settings.forward = end.forward;
tool.settings.backward = end.backward;
tool.settings.lanewidth = end.width;
tool.settings.lanes.clear();
tool.settings.changes.clear();
tool.settings.normalize();
tool.status = "Leading a road out of \"" + end.junction->name() + "\", with the lanes the junction has there";
}
road_select(nullptr);
tool.status = "Carrying on \"" + end.road->name() + "\" with its layout";
}
else
{
tool.point = ground;
tool.point = ground + glm::dvec3{0.0, tool.offset, 0.0};
tool.hasdirection = false;
tool.hasgrade = false;
tool.reversed = false;
tool.selected = nullptr;
tool.junction = nullptr;
tool.status = "Start set";
}
tool.chain = true;
@@ -241,16 +506,22 @@ void editor_mode::road_click()
editor_road::loose_end snap;
std::string error;
auto pieces{road_preview(snap, error)};
auto pieces{road_preview(snap, error, true)};
if (pieces.empty())
{
tool.status = "Can't build this: " + error;
return;
}
if (snap.valid() && false == editor_road::can_join(snap, &reason))
{
tool.status = "Can't join here: " + reason;
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]};
auto const grade{editor_road::end_grade(last)};
if (tool.reversed)
{
for (auto &piece : pieces)
@@ -274,10 +545,10 @@ void editor_mode::road_click()
return;
}
push_road_snapshot({}, created, {});
if (snap.road != nullptr)
if (snap.valid())
{
tool.chain = false;
tool.status = "Joined with \"" + snap.road->name() + "\"";
tool.status = "Joined with \"" + (snap.road != nullptr ? snap.road->name() : snap.junction->name()) + "\"";
return;
}
glm::dvec2 const planar{tangent.x, tangent.z};
@@ -287,6 +558,8 @@ void editor_mode::road_click()
tool.direction = glm::normalize(planar);
tool.hasdirection = true;
}
tool.grade = grade;
tool.hasgrade = true;
tool.status = std::to_string(created.size()) + (created.size() == 1 ? " piece added" : " pieces added");
}
@@ -329,16 +602,30 @@ void editor_mode::road_apply()
bool editor_mode::road_delete()
{
auto &tool{m_roadtool};
if (false == tool.window || tool.selected == nullptr)
if (false == tool.window || (tool.selected == nullptr && tool.junction == nullptr))
return false;
auto *road{tool.selected};
std::string reason;
if (tool.junction != nullptr)
{
auto *junction{tool.junction};
if (false == editor_road::can_edit(*junction, &reason))
{
tool.status = "Can't delete: " + reason;
return true;
}
editor_road::remove(std::vector<junction_node *>{junction});
push_road_snapshot({}, {}, {}, {}, {junction});
tool.junction = nullptr;
tool.status = "\"" + junction->name() + "\" deleted, Ctrl+Z brings it back";
return true;
}
auto *road{tool.selected};
if (false == editor_road::can_edit(*road, &reason))
{
tool.status = "Can't delete: " + reason;
return true;
}
editor_road::remove({road});
editor_road::remove(std::vector<road_node *>{road});
push_road_snapshot({}, {}, {road});
tool.selected = nullptr;
tool.status = "\"" + road->name() + "\" deleted, Ctrl+Z brings it back";
@@ -370,29 +657,43 @@ bool editor_mode::road_split()
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)
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, std::vector<junction_node *> Junctionscreated,
std::vector<junction_node *> Junctionsremoved)
{
if (States.empty() && Created.empty() && Removed.empty())
if (States.empty() && Created.empty() && Removed.empty() && Junctionscreated.empty() && Junctionsremoved.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();
// the entry is listed under the name of the first thing it's about
scene::basic_node const *node{nullptr};
if (false == States.empty())
node = States.front().first;
else if (false == Created.empty())
node = Created.front();
else if (false == Removed.empty())
node = Removed.front();
else if (false == Junctionscreated.empty())
node = Junctionscreated.front();
else
node = Junctionsremoved.front();
EditorSnapshot snap;
snap.action = EditorSnapshot::Action::RoadEdit;
snap.node_name = road->name();
snap.position = road->location();
snap.node_name = node->name();
snap.position = node->location();
snap.roads = std::move(States);
snap.roads_created = std::move(Created);
snap.roads_removed = std::move(Removed);
snap.junctions_created = std::move(Junctionscreated);
snap.junctions_removed = std::move(Junctionsremoved);
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())
if (Snapshot.roads.empty() && Snapshot.roads_created.empty() && Snapshot.roads_removed.empty() && Snapshot.junctions_created.empty() && Snapshot.junctions_removed.empty())
return;
// what the roads are like now is what the opposite operation brings back
@@ -405,11 +706,15 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec
if (Undo)
{
editor_road::remove(Snapshot.roads_created);
editor_road::remove(Snapshot.junctions_created);
editor_road::revive(Snapshot.junctions_removed);
editor_road::revive(Snapshot.roads_removed);
}
else
{
editor_road::revive(Snapshot.roads_created);
editor_road::revive(Snapshot.junctions_created);
editor_road::remove(Snapshot.junctions_removed);
editor_road::remove(Snapshot.roads_removed);
}
editor_road::apply(Snapshot.roads);
@@ -418,6 +723,8 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec
tool.chain = false;
if (tool.selected != nullptr)
road_select(tool.selected->m_editorremoved ? nullptr : tool.selected);
if (tool.junction != nullptr && tool.junction->m_editorremoved)
tool.junction = nullptr;
}
// 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
@@ -642,28 +949,71 @@ void editor_mode::render_road_window()
ImGui::RadioButton("Select", &tool.tool, 0);
ImGui::SameLine();
ImGui::RadioButton("Build", &tool.tool, 1);
ImGui::SameLine();
ImGui::RadioButton("Junction", &tool.tool, 2);
if (tool.tool != previoustool)
{
tool.chain = false;
if (tool.tool == 1)
if (tool.tool != 0)
{
tool.selected = nullptr;
tool.junction = nullptr;
}
}
ImGui::SameLine();
ImGui::Checkbox("Show lanes", &tool.lanes);
// how the things being built are put on the ground
auto const placement = [&tool](bool const Road) {
ImGui::SetNextItemWidth(120.0f);
if (ImGui::InputFloat("Height above the ground [m]", &tool.offset, 0.05f, 0.25f, "%.2f"))
tool.offset = std::clamp(tool.offset, -10.0f, 100.0f);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "How far over the ground under the cursor the points are put.\nA few centimetres keep the surface from flickering where it lies on the ground.");
if (false == Road)
return;
ImGui::Checkbox("Follow the ground", &tool.follow);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "On: the road is led over the ground between the clicked points, in pieces as short as the ground calls for,\nwith the profile rounded so the slope doesn't change abruptly.\nOff: the road goes straight from a point to the next one, like a ramp or a viaduct.");
};
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");
ImGui::TextDisabled("Start or end on a loose end of a road or an arm of a junction to join it");
if (tool.chain)
{
if (ImGui::Button("Finish (Esc)"))
tool.chain = false;
}
placement(true);
ImGui::Separator();
// what's set here is what the next pieces get
render_road_layout(tool.settings);
}
else if (tool.tool == 2)
{
ImGui::TextDisabled("LMB on a loose end of a road: junction for that road, right past its end");
ImGui::TextDisabled("LMB elsewhere: the centre, then the side the first road comes from. Esc: start over");
char const *kinds[]{"Crossroads", "T: the road ends, roads to both sides", "T: the road goes on, a road to the left", "T: the road goes on, a road to the right"};
ImGui::SetNextItemWidth(280.0f);
ImGui::Combo("Kind", &tool.kind, kinds, 4);
ImGui::SetNextItemWidth(120.0f);
if (ImGui::InputFloat("Speed limit on the junction [km/h]", &tool.crossingspeed, 5.0f, 10.0f, "%.0f"))
tool.crossingspeed = std::clamp(tool.crossingspeed, 5.0f, 200.0f);
placement(false);
ImGui::Separator();
ImGui::TextWrapped("Away from the roads the junction is made for roads with the lanes set in the Build tool: %d + %d, %.2f m wide. Its surface and markings go after the road it's made for.", tool.settings.forward, tool.settings.backward,
tool.settings.lanewidth);
ImGui::TextWrapped("Roads are led out of the arms with the Build tool, and get the lanes the junction has there.");
}
else if (tool.junction != nullptr)
{
auto const &junction{*tool.junction};
ImGui::Text("%s, %d arms, %d ways through", junction.name().c_str(), static_cast<int>(junction.definition().arms.size()), static_cast<int>(junction.movements().size()));
ImGui::TextDisabled("Speed limit %.0f km/h. The ways through are made for the roads attached at the moment", junction.definition().velocity);
if (ImGui::Button("Delete (Del)"))
road_delete();
}
else if (tool.selected != nullptr)
{
auto const &road{*tool.selected};
@@ -681,7 +1031,7 @@ void editor_mode::render_road_window()
}
else
{
ImGui::TextDisabled("LMB: select a road piece");
ImGui::TextDisabled("LMB: select a road piece or a junction");
}
if (false == tool.status.empty())
{
@@ -748,6 +1098,14 @@ void editor_mode::draw_road_overlay() const
drawpath(turn->m_paths.front(), kTurnColor, 1.5f, 8);
}
}
// ways through the junctions
for (auto const *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved || glm::distance(junction->location(), camera) > kLaneRange)
continue;
for (auto const &way : junction->movements())
drawpath(way, kWayColor, 1.5f, 8);
}
}
if (false == tool.window)
return;
@@ -786,6 +1144,15 @@ void editor_mode::draw_road_overlay() const
drawlist->AddCircle(screen, Radius, Color, 20, 3.0f);
};
// outline of a junction: the edge of its surface, with the ends of the roads it's made for
auto const drawjunction = [&](junction_node::state const &State, ImU32 const Color) {
auto const outline{State.outline()};
for (std::size_t i = 0; i < outline.size(); ++i)
projection.line(drawlist, outline[i], outline[(i + 1) % outline.size()], Color, 2.0f);
for (auto const &arm : State.arms)
drawpoint(arm.position, Color, 5.0f, true);
};
if (tool.selected != nullptr && false == tool.selected->m_editorremoved)
{
auto const &state{tool.selected->definition()};
@@ -793,42 +1160,57 @@ void editor_mode::draw_road_overlay() const
drawpoint(state.axis.points[segment_data::point::start], kSelectedColor, 6.0f, true);
drawpoint(state.axis.points[segment_data::point::end], kSelectedColor, 6.0f, true);
}
if (tool.junction != nullptr && false == tool.junction->m_editorremoved)
drawjunction(tool.junction->definition(), kSelectedColor);
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())
if (tool.preview.empty())
{
projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f);
hint = error + " ";
hint = tool.previewerror + " ";
}
for (auto const &piece : pieces)
for (auto const &piece : tool.preview)
{
auto state{tool.settings};
state.axis = piece;
drawroad(state, kPreviewColor);
}
if (snap.road != nullptr)
drawpoint(snap.position, kSnapColor, 14.0f, false);
if (tool.previewsnap.valid())
drawpoint(tool.previewsnap.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)
if (end.valid())
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";
hint = "LMB: set where the road starts; on a loose end of a road or an arm of a junction (ring) to carry on from there";
}
}
else if (tool.tool == 2)
{
if (tool.chain)
drawpoint(tool.point, kPreviewColor, 6.0f, true);
if (tool.hasjunction)
drawjunction(tool.previewjunction, kPreviewColor);
else if (false == tool.previewerror.empty())
hint = tool.previewerror + " ";
if (tool.previewsnap.valid())
drawpoint(tool.previewsnap.position, kSnapColor, 14.0f, false);
hint += (tool.chain ? "LMB: make the junction, with the first road coming from the side of the cursor Esc: start over" :
tool.previewsnap.valid() ? "LMB: make the junction at the end of this road" :
"LMB: on a loose end of a road (ring) to make its junction there; elsewhere to set the centre of one");
}
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";
hint = tool.selected != nullptr ? "LMB: select a road piece or a junction K: split at the cursor Del: delete Ctrl+Z: undo" :
tool.junction != nullptr ? "LMB: select a road piece or a junction Del: delete Ctrl+Z: undo" :
"LMB: select a road piece or a junction";
}
// shown above the place the track tools put their hints at
ImGuiIO const &io = ImGui::GetIO();

View File

@@ -63,6 +63,72 @@ std::vector<road_node *> roads_at(glm::dvec3 const &Point, road_node const *Excl
return result;
}
// junctions with an arm at specified point
std::vector<junction_node *> junctions_at(glm::dvec3 const &Point)
{
std::vector<junction_node *> result;
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
for (auto const &arm : junction->definition().arms)
{
if (glm::distance(arm.position, Point) < kSamePoint)
{
result.emplace_back(junction);
break;
}
}
}
return result;
}
void add_unique(std::vector<junction_node *> &Junctions, junction_node *Junction)
{
if (Junction != nullptr && std::find(Junctions.begin(), Junctions.end(), Junction) == Junctions.end())
Junctions.emplace_back(Junction);
}
// part of a path between two values of its curve parameter
segment_data subpath(segment_data const &Path, double const From, double const To)
{
segment_data part{Path};
auto const &p0{Path.points[segment_data::point::start]};
auto const &p3{Path.points[segment_data::point::end]};
part.rolls = {static_cast<float>(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * From), static_cast<float>(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * To)};
if (Path.points[segment_data::point::control1] == glm::dvec3{0.0} && Path.points[segment_data::point::control2] == glm::dvec3{0.0})
{
part.points[segment_data::point::start] = glm::mix(p0, p3, From);
part.points[segment_data::point::end] = glm::mix(p0, p3, To);
return part;
}
auto const p1{p0 + Path.points[segment_data::point::control1]};
auto const p2{p3 + Path.points[segment_data::point::control2]};
auto const point = [&](double const T) {
auto 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;
};
auto const derivative = [&](double const T) {
auto const u{1.0 - T};
return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2);
};
// a part of a cubic curve is a cubic curve, with the tangents scaled to the part of the parameter range it takes
part.points[segment_data::point::start] = point(From);
part.points[segment_data::point::end] = point(To);
part.points[segment_data::point::control1] = derivative(From) * ((To - From) / 3.0);
part.points[segment_data::point::control2] = derivative(To) * (-(To - From) / 3.0);
return part;
}
// true if there's a vehicle on any of the paths turning the traffic back at the loose ends of the road
bool turning(road_node const &Road)
{
for (auto const *turn : Road.turns())
if (turn != nullptr && false == turn->Dynamics.empty())
return true;
return false;
}
void retire(std::vector<TTrack *> const &Tracks)
{
for (auto *track : Tracks)
@@ -138,6 +204,66 @@ bool editor_road::can_edit(road_node const &Road, std::string *Reason)
if (turn != nullptr && false == turn->Dynamics.empty())
reason = "there are vehicles turning back next to it";
}
if (reason.empty())
{
// so do the ways through the junctions it leads to
for (auto const atend : {false, true})
for (auto const *junction : junctions_at(axis_end(Road.definition(), atend)))
if (junction->occupied())
reason = "there are vehicles on the junction it leads to";
}
if (false == reason.empty() && Reason != nullptr)
*Reason = reason;
return reason.empty();
}
bool editor_road::can_edit(junction_node const &Junction, std::string *Reason)
{
std::string reason;
if (false == available(&reason))
{
// reason was filled by the call
}
else if (Junction.merged())
{
reason = "the scenery wasn't opened for editing (-edit), geometry of the junctions it came with can't be taken apart";
}
else if (false == scene::Layers.editable(&Junction, &reason))
{
// reason was filled by the call
}
else if (Junction.occupied())
{
reason = "there are vehicles on it";
}
else
{
// the roads attached to it get their loose ends closed anew
for (auto const &arm : Junction.definition().arms)
for (auto const *road : roads_at(arm.position, nullptr))
if (turning(*road))
reason = "there are vehicles turning back on the roads it leads to";
}
if (false == reason.empty() && Reason != nullptr)
*Reason = reason;
return reason.empty();
}
bool editor_road::can_join(loose_end const &End, std::string *Reason)
{
// what gets attached takes the place of the paths made for the end as it was
std::string reason;
if (End.road != nullptr && turning(*End.road))
reason = "there are vehicles turning back on the road";
if (End.junction != nullptr)
{
if (End.junction->occupied())
reason = "there are vehicles on the junction";
for (auto const &arm : End.junction->definition().arms)
for (auto const *road : roads_at(arm.position, nullptr))
if (turning(*road))
reason = "there are vehicles turning back on the roads the junction leads to";
}
if (false == reason.empty() && Reason != nullptr)
*Reason = reason;
return reason.empty();
@@ -155,38 +281,64 @@ std::vector<road_node *> editor_road::neighbours(road_node const &Road)
}
template <class Change_>
void editor_road::refresh(std::vector<road_node *> const &Roads, Change_ Change)
void editor_road::refresh(std::vector<road_node *> const &Roads, std::vector<junction_node *> const &Junctions, Change_ Change)
{
// the pieces next to the changed ones keep their shape, but what they're joined with changes
// the pieces and junctions next to the changed ones keep their shape, but what they're joined with changes.
// this takes down the paths made for the joints, so they can be made again once the change is done
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());
std::vector<junction_node *> crossings;
auto const gather = [&]() {
std::vector<road_node *> roads{affected};
std::vector<junction_node *> junctions{crossings};
for (auto *road : Roads)
{
add_unique(roads, road);
if (road->m_editorremoved)
continue;
for (auto *neighbour : neighbours(*road))
add_unique(roads, neighbour);
for (auto const atend : {false, true})
for (auto *junction : junctions_at(axis_end(road->definition(), atend)))
add_unique(junctions, junction);
}
for (auto *junction : Junctions)
add_unique(junctions, junction);
// a junction changes what the roads attached to it do at their ends
for (auto *junction : junctions)
if (false == junction->m_editorremoved)
for (auto const &arm : junction->definition().arms)
for (auto *road : roads_at(arm.position, nullptr))
add_unique(roads, road);
for (auto *road : roads)
{
if (std::find(affected.begin(), affected.end(), road) == affected.end())
{
retire(road->release_turns());
affected.emplace_back(road);
}
}
for (auto *junction : junctions)
{
if (std::find(crossings.begin(), crossings.end(), junction) == crossings.end())
{
retire(junction->release_links());
crossings.emplace_back(junction);
}
}
};
gather();
for (auto *road : Roads)
{
road->hide();
retire(road->release_lanes());
}
for (auto *junction : Junctions)
junction->hide();
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);
}
}
}
// the change could have moved things next to other ones
gather();
std::vector<TTrack *> lanes;
for (auto *road : Roads)
{
@@ -198,6 +350,13 @@ void editor_road::refresh(std::vector<road_node *> const &Roads, Change_ Change)
}
// joins the lanes with the lanes of the neighbours, by the ends they share
editor_track::commit(lanes);
for (auto *junction : Junctions)
if (false == junction->m_editorremoved)
junction->show();
// the junctions take the lanes which end at them, what's left loose after that is closed by the roads
for (auto *junction : crossings)
if (false == junction->m_editorremoved)
junction->create_links();
for (auto *road : affected)
if (false == road->m_editorremoved)
road->close_ends();
@@ -227,7 +386,7 @@ std::vector<road_node *> editor_road::create(std::vector<road_node::state> const
scene::Layers.count(road->layer(), scene::layer_item::track);
created.emplace_back(road);
}
refresh(created, []() {});
refresh(created, {}, []() {});
return created;
}
@@ -238,7 +397,7 @@ void editor_road::apply(std::vector<std::pair<road_node *, road_node::state>> co
add_unique(roads, change.first);
if (roads.empty())
return;
refresh(roads, [&]() {
refresh(roads, {}, [&]() {
for (auto const &change : Changes)
{
change.first->define(change.second);
@@ -255,7 +414,7 @@ void editor_road::remove(std::vector<road_node *> const &Roads)
add_unique(roads, road);
if (roads.empty())
return;
refresh(roads, [&]() {
refresh(roads, {}, [&]() {
for (auto *road : roads)
{
road->m_editorremoved = true;
@@ -272,7 +431,7 @@ void editor_road::revive(std::vector<road_node *> const &Roads)
add_unique(roads, road);
if (roads.empty())
return;
refresh(roads, [&]() {
refresh(roads, {}, [&]() {
for (auto *road : roads)
{
road->m_editorremoved = false;
@@ -282,6 +441,68 @@ void editor_road::revive(std::vector<road_node *> const &Roads)
});
}
std::vector<junction_node *> editor_road::create(std::vector<junction_node::state> const &States)
{
static int counter{0};
std::vector<junction_node *> created;
if (false == available())
return created;
for (auto const &state : States)
{
scene::node_data data;
data.type = "junction";
data.range_max = -1.0;
data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing
do
{
data.name = "junction_" + std::to_string(++counter);
} while (simulation::Junctions.find(data.name) != nullptr);
auto *junction{new junction_node(data)};
junction->define(state);
junction->mark_dirty();
simulation::Junctions.insert(junction);
scene::Layers.count(junction->layer(), scene::layer_item::track);
created.emplace_back(junction);
}
refresh({}, created, []() {});
return created;
}
void editor_road::remove(std::vector<junction_node *> const &Junctions)
{
std::vector<junction_node *> junctions;
for (auto *junction : Junctions)
if (junction != nullptr && false == junction->m_editorremoved)
add_unique(junctions, junction);
if (junctions.empty())
return;
refresh({}, junctions, [&]() {
for (auto *junction : junctions)
{
junction->m_editorremoved = true;
scene::Layers.count(junction->layer(), scene::layer_item::track, -1);
}
});
}
void editor_road::revive(std::vector<junction_node *> const &Junctions)
{
std::vector<junction_node *> junctions;
for (auto *junction : Junctions)
if (junction != nullptr && junction->m_editorremoved)
add_unique(junctions, junction);
if (junctions.empty())
return;
refresh({}, junctions, [&]() {
for (auto *junction : junctions)
{
junction->m_editorremoved = false;
junction->mark_dirty();
scene::Layers.count(junction->layer(), scene::layer_item::track);
}
});
}
road_node *editor_road::split(road_node &Road, double const T)
{
if (T <= 0.001 || T >= 0.999)
@@ -386,7 +607,29 @@ road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin)
return result;
}
editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double const Radius)
junction_node *editor_road::nearest_junction(glm::dvec3 const &Point)
{
junction_node *result{nullptr};
double best{std::numeric_limits<double>::max()};
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
auto const &state{junction->definition()};
// the junction takes the ground up to the ends of the roads it's made for
double reach{0.0};
for (auto const &arm : state.arms)
reach = std::max(reach, glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{state.centre.x, state.centre.z}));
auto const distance{glm::distance(glm::dvec2{state.centre.x, state.centre.z}, glm::dvec2{Point.x, Point.z})};
if (distance > reach || std::abs(state.centre.y - Point.y) > 3.0 || distance > best)
continue;
best = distance;
result = junction;
}
return result;
}
editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double const Radius, bool const Junctions)
{
loose_end result;
double best{Radius};
@@ -399,15 +642,38 @@ editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double con
{
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())
if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty() || false == junctions_at(position).empty())
continue;
best = distance;
result = loose_end{};
result.road = road;
result.atend = atend;
result.position = position;
result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0));
}
}
if (false == Junctions)
return result;
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
for (auto const &arm : junction->definition().arms)
{
auto const distance{glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{Point.x, Point.z})};
if (distance > best || std::abs(arm.position.y - Point.y) > 3.0 || false == roads_at(arm.position, nullptr).empty())
continue;
best = distance;
result = loose_end{};
result.junction = junction;
result.position = arm.position;
result.outwards = glm::dvec3{arm.direction.x, 0.0, arm.direction.y};
// a road leading away from the junction has the lanes going out of it as the ones going along its axis
result.forward = arm.outgoing;
result.backward = arm.incoming;
result.width = arm.width;
}
}
return result;
}
@@ -535,6 +801,147 @@ std::vector<segment_data> editor_road::plan(glm::dvec3 const &Start, glm::dvec2
return pieces;
}
double editor_road::planar_length(segment_data const &Path)
{
auto const whole{subpath(Path, 0.0, 1.0)};
auto const &p0{whole.points[segment_data::point::start]};
auto const &p3{whole.points[segment_data::point::end]};
if (whole.points[segment_data::point::control1] == glm::dvec3{0.0} && whole.points[segment_data::point::control2] == glm::dvec3{0.0})
return glm::distance(glm::dvec2{p0.x, p0.z}, glm::dvec2{p3.x, p3.z});
auto const p1{p0 + whole.points[segment_data::point::control1]};
auto const p2{p3 + whole.points[segment_data::point::control2]};
double length{0.0};
glm::dvec2 previous{p0.x, p0.z};
for (int i = 1; i <= 32; ++i)
{
auto const t{i / 32.0};
auto const u{1.0 - t};
auto const point{u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3};
glm::dvec2 const current{point.x, point.z};
length += glm::distance(previous, current);
previous = current;
}
return length;
}
double editor_road::end_grade(segment_data const &Path)
{
auto const &control{Path.points[segment_data::point::control2]};
if (control != glm::dvec3{0.0})
{
auto const run{glm::length(glm::dvec2{control.x, control.z})};
return run > 1e-6 ? -control.y / run : 0.0;
}
auto const length{planar_length(Path)};
return length > 1e-6 ? (Path.points[segment_data::point::end].y - Path.points[segment_data::point::start].y) / length : 0.0;
}
std::vector<segment_data> editor_road::refine(std::vector<segment_data> const &Pieces, double const Length)
{
std::vector<segment_data> result;
for (auto const &piece : Pieces)
{
auto const count{std::max(1, static_cast<int>(std::ceil(planar_length(piece) / std::max(1.0, Length) - 0.01)))};
for (int i = 0; i < count; ++i)
result.emplace_back(count == 1 ? piece : subpath(piece, static_cast<double>(i) / count, static_cast<double>(i + 1) / count));
}
return result;
}
void editor_road::profile(std::vector<segment_data> &Pieces, std::vector<double> Heights, double const *Startgrade, double const *Endgrade)
{
auto const count{Pieces.size()};
if (count == 0 || Heights.size() != count + 1)
return;
std::vector<double> lengths(count);
for (std::size_t i = 0; i < count; ++i)
lengths[i] = std::max(0.01, planar_length(Pieces[i]));
// the heights in between are evened out, so the road doesn't ride every dip. it's never lowered though,
// as that would let the ground the heights were taken from come through the surface
{
auto const sampled{Heights};
for (std::size_t i = 1; i < count; ++i)
Heights[i] = std::max(sampled[i], 0.25 * sampled[i - 1] + 0.5 * sampled[i] + 0.25 * sampled[i + 1]);
}
std::vector<double> slopes(count);
for (std::size_t i = 0; i < count; ++i)
slopes[i] = (Heights[i + 1] - Heights[i]) / lengths[i];
// slope at each joint is shared by the pieces on both its sides, which is what rounds the profile
std::vector<double> grades(count + 1);
grades.front() = (Startgrade != nullptr ? *Startgrade : slopes.front());
grades.back() = (Endgrade != nullptr ? *Endgrade : slopes.back());
for (std::size_t i = 1; i < count; ++i)
{
auto const before{slopes[i - 1]};
auto const after{slopes[i]};
if (before * after <= 0.0)
{
// a crest or a bottom
grades[i] = 0.0;
continue;
}
auto grade{(before * lengths[i] + after * lengths[i - 1]) / (lengths[i - 1] + lengths[i])};
// kept from overshooting the heights the road is led through
auto const limit{3.0 * std::min(std::abs(before), std::abs(after))};
grades[i] = std::clamp(grade, -limit, limit);
}
for (std::size_t i = 0; i < count; ++i)
{
auto &piece{Pieces[i]};
auto &start{piece.points[segment_data::point::start]};
auto &end{piece.points[segment_data::point::end]};
auto &control1{piece.points[segment_data::point::control1]};
auto &control2{piece.points[segment_data::point::control2]};
start.y = Heights[i];
end.y = Heights[i + 1];
if (control1 == glm::dvec3{0.0} && control2 == glm::dvec3{0.0})
{
if (std::abs(grades[i] - slopes[i]) < 1e-4 && std::abs(grades[i + 1] - slopes[i]) < 1e-4)
continue; // even slope, the piece stays a plain straight
// a straight needs its control points spelled out to bend up or down
control1 = (end - start) / 3.0;
control2 = (start - end) / 3.0;
}
control1.y = grades[i] * glm::length(glm::dvec2{control1.x, control1.z});
control2.y = -grades[i + 1] * glm::length(glm::dvec2{control2.x, control2.z});
}
}
double editor_road::junction_reach(int const Lanes, float const Width)
{
// half of the road, and room for the corners
return 0.5 * std::max(1, Lanes) * Width + 6.0;
}
junction_node::state editor_road::junction(junction_kind const Kind, glm::dvec3 const &Entry, glm::dvec2 const &Heading, int const Incoming, int const Outgoing, float const Width)
{
junction_node::state state;
auto const heading{glm::length(Heading) > 1e-6 ? glm::normalize(Heading) : glm::dvec2{0.0, 1.0}};
glm::dvec2 const left{heading.y, -heading.x};
auto const reach{junction_reach(Incoming + Outgoing, Width)};
auto const spot = [&](glm::dvec2 const &Direction, double const Distance) { return glm::dvec3{Entry.x + Direction.x * Distance, Entry.y, Entry.z + Direction.y * Distance}; };
state.centre = spot(heading, reach);
auto const arm = [&](glm::dvec2 const &Direction, int const In, int const Out) {
junction_node::arm_data data;
data.position = glm::dvec3{state.centre.x + Direction.x * reach, Entry.y, state.centre.z + Direction.y * reach};
data.direction = Direction;
data.incoming = In;
data.outgoing = Out;
data.width = Width;
state.arms.emplace_back(data);
};
// the road the junction is entered by, then the others: what comes in by one leaves by them
arm(-heading, Incoming, Outgoing);
if (Kind == junction_kind::cross || Kind == junction_kind::tee_left || Kind == junction_kind::tee_right)
arm(heading, Outgoing, Incoming);
if (Kind == junction_kind::cross || Kind == junction_kind::tee_sides || Kind == junction_kind::tee_left)
arm(left, Outgoing, Incoming);
if (Kind == junction_kind::cross || Kind == junction_kind::tee_sides || Kind == junction_kind::tee_right)
arm(-left, Outgoing, Incoming);
state.normalize();
return state;
}
std::vector<std::string> const &editor_road::materials()
{
static std::vector<std::string> names;

View File

@@ -17,22 +17,42 @@ http://mozilla.org/MPL/2.0/.
#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
// each piece being a road node with lanes and geometry of its own; roads meet at junctions
class editor_road
{
public:
// an end of a road piece nothing is attached to
// an end of a road piece, or an arm of a junction, nothing is attached to
struct loose_end
{
road_node *road{nullptr};
bool atend{false};
junction_node *junction{nullptr};
bool atend{false}; // of a road: the end of its axis rather than the start
glm::dvec3 position{0.0};
glm::dvec3 outwards{0.0}; // direction the road would continue in
glm::dvec3 outwards{0.0}; // direction a road would continue in
// of a junction arm: lanes of a road leading away from it
int forward{0};
int backward{0};
float width{0.f};
bool valid() const
{
return road != nullptr || junction != nullptr;
}
};
enum class junction_kind
{
cross, // four roads
tee_sides, // the road ends, with roads to both sides
tee_left, // the road goes on, with a road to the left
tee_right // the road goes on, with a road to the right
};
// 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
// true if specified road piece or junction can be changed or removed. optionally explains why it can't
static bool can_edit(road_node const &Road, std::string *Reason = nullptr);
static bool can_edit(junction_node const &Junction, std::string *Reason = nullptr);
// true if a road or a junction can be attached to specified loose end. optionally explains why it can't
static bool can_join(loose_end const &End, 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
@@ -40,26 +60,48 @@ class editor_road
// 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);
// the same for the junctions
static std::vector<junction_node *> create(std::vector<junction_node::state> const &States);
static void remove(std::vector<junction_node *> const &Junctions);
static void revive(std::vector<junction_node *> const &Junctions);
// 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);
// the junction specified point lies on
static junction_node *nearest_junction(glm::dvec3 const &Point);
// 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);
// end of a road with nothing attached, within specified distance from specified point; optionally arms of junctions too
static loose_end find_end(glm::dvec3 const &Point, double const Radius, bool const Junctions = true);
// 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);
// cuts the pieces so none is longer than specified
static std::vector<segment_data> refine(std::vector<segment_data> const &Pieces, double const Length);
// gives a string of pieces a smooth profile running through provided heights.
// Heights: one for the start of each piece and one for the end of the last. these of the ends are kept, the others are evened out:
// the dips are filled while the tops stay, so the ground the heights were taken from doesn't come through the road.
// Startgrade, Endgrade: slopes to keep at the ends, to carry on what the road joins; nullptr for none
static void profile(std::vector<segment_data> &Pieces, std::vector<double> Heights, double const *Startgrade, double const *Endgrade);
// length of the path seen from above
static double planar_length(segment_data const &Path);
// slope of the path at its end, in the direction it's laid out
static double end_grade(segment_data const &Path);
// distance from the centre of a junction to the ends of the roads it's made for
static double junction_reach(int const Lanes, float const Width);
// lays out a junction entered by a road. Entry: middle of the end of that road, Heading: the way the road goes there,
// Incoming, Outgoing: lanes of that road leading into the junction and out of it
static junction_node::state junction(junction_kind const Kind, glm::dvec3 const &Entry, glm::dvec2 const &Heading, int const Incoming, int const Outgoing, float const Width);
// 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
// takes down what depends on the listed pieces and junctions, 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 void refresh(std::vector<road_node *> const &Roads, std::vector<junction_node *> const &Junctions, Change_ Change);
static std::vector<road_node *> neighbours(road_node const &Road);
};

View File

@@ -857,6 +857,72 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(road);
}
// road junctions, likewise
std::vector<junction_node *> savedjunctions;
std::vector<junction_node const *> droppedjunctions;
auto const junction_text = [](junction_node &Junction, glm::dvec3 const &Offset) {
auto const definition{Junction.definition()};
auto local{definition};
local.centre -= Offset;
for (auto &arm : local.arms)
{
arm.position -= Offset;
}
Junction.define(local);
std::string text;
Junction.export_as_text(text);
Junction.define(definition);
text.erase(text.find_last_not_of(" \t\r\n") + 1);
return text;
};
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->from_template())
{
continue;
}
auto const nodelayer{resolve(junction->layer())};
if (false == is_output(nodelayer))
{
continue;
}
auto const lookup{m_sources.find(junction)};
if (lookup == m_sources.end())
{
if (junction->m_editorremoved || false == junction->dirty())
{
continue;
}
created[nodelayer].emplace_back(junction, junction_text(*junction, layer(nodelayer).context_insert().offset));
savedjunctions.push_back(junction);
continue;
}
auto const &source{lookup->second};
if ((false == junction->m_editorremoved && false == junction->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 (junction->m_editorremoved)
{
droppedjunctions.push_back(junction);
}
else
{
edit.text = junction_text(*junction, source.context.offset);
edit.length = edit.text.size();
edit.node = junction;
savedjunctions.push_back(junction);
}
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(junction);
}
for (auto const &erased : m_erased)
{
file_patch::edit edit;
@@ -1308,6 +1374,14 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
{
m_sources.erase(road);
}
for (auto *junction : savedjunctions)
{
junction->m_dirty = false;
}
for (auto const *junction : droppedjunctions)
{
m_sources.erase(junction);
}
if (removedterrain)
{
// binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load

View File

@@ -38,6 +38,7 @@ event_manager Events;
memory_table Memory;
path_table Paths;
road_table Roads;
junction_table Junctions;
traction_table Traction;
powergridsource_table Powergrid;
instance_table Instances;

View File

@@ -83,6 +83,7 @@ extern event_manager Events;
extern memory_table Memory;
extern path_table Paths;
extern road_table Roads;
extern junction_table Junctions;
extern traction_table Traction;
extern powergridsource_table Powergrid;
extern instance_table Instances;

View File

@@ -181,6 +181,7 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
// geometry of the roads is generated on each load instead of being kept in the binary terrain file,
// so it's inserted in the region only after that file had its chance to be written
simulation::Roads.create_geometry( Scratchpad );
simulation::Junctions.create_geometry( Scratchpad );
return false;
}
@@ -398,6 +399,8 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc
}
simulation::Paths.InitTracks();
// the junctions tie the lanes of the roads together, what's left loose after that gets closed by the roads
simulation::Junctions.InitJunctions();
simulation::Roads.InitRoads();
simulation::Traction.InitTraction();
simulation::Events.InitEvents();
@@ -521,6 +524,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
road->create_lanes();
scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } );
}
else if( nodedata.type == "junction" ) {
auto *junction { new junction_node( nodedata ) };
junction->import(
Input,
( Scratchpad.location.offset.empty() ?
glm::dvec3 { 0.0 } :
glm::dvec3 { Scratchpad.location.offset.top() } ) );
if( false == simulation::Junctions.insert( junction ) ) {
ErrorLog( "Bad scenario: duplicate junction name \"" + junction->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" );
}
scene::Groups.insert( scene::Groups.handle(), junction );
scene::Layers.track( junction, { sourcebegin, Input.TokenEnd() } );
}
else if( nodedata.type == "traction" ) {
auto *traction { deserialize_traction( Input, Scratchpad, nodedata ) };
@@ -695,6 +712,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
{ "dynamic", scene::layer_item::vehicle },
{ "track", scene::layer_item::track },
{ "road", scene::layer_item::track },
{ "junction", scene::layer_item::track },
{ "traction", scene::layer_item::traction },
{ "tractionpowersource", scene::layer_item::powersource },
{ "model", scene::layer_item::model },
@@ -1404,6 +1422,11 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons
road->export_as_text( scmfile );
}
}
for( auto const *junction : Junctions.sequence() ) {
if( junction != nullptr && false == junction->m_editorremoved && junction->dirty() == Dirty && junction->group() == null_handle ) {
junction->export_as_text( scmfile );
}
}
// traction
scmfile << "// traction\n";
for( auto const *traction : Traction.sequence() ) {
@@ -1459,7 +1482,7 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st
std::pair<int, int> state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) {
// statements which take more than a single token, with the tokens ending them
static std::unordered_map<std::string, std::string> const nodeends {
{ "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" },
{ "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "junction", "endjunction" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" },
{ "triangles", "endtri" }, { "triangle_strip", "endtri" }, { "triangle_fan", "endtri" }, { "lines", "endline" }, { "line_strip", "endline" }, { "line_loop", "endline" },
{ "memcell", "endmemcell" }, { "eventlauncher", "end" }, { "sound", "endsound" } };
static std::unordered_map<std::string, std::string> const statementends {

View File

@@ -45,6 +45,8 @@ struct light_array;
class particle_manager;
class road_node; // kawałek drogi, z którego powstają pasy ruchu
class road_table;
class junction_node;
class junction_table;
struct dictionary_source;
class trainset_desc;
class scenery_desc;

View File

@@ -907,26 +907,52 @@ std::vector<scene::shape_node> road_node::create_shapes() const
return shapes;
}
// puts geometry of the road in the scene as shapes of its own, which can be taken back
void road_node::show()
namespace
{
if (m_section != nullptr || m_merged || simulation::Region == nullptr || false == simulation::Region->point_inside(location()))
// banks holding geometry put in the scene by the owned shapes
std::unordered_set<std::uint32_t> ownedbanks;
double const junction_reach{0.25}; // an end of a road this close to an arm is attached to it
double const junction_fit{0.05}; // a lane has to end this close to where the junction expects it
int const corner_steps{6}; // pieces a rounded corner is made of
// direction on the ground as a vector in space
glm::dvec3 flat(glm::dvec2 const &Direction)
{
return glm::dvec3{Direction.x, 0.0, Direction.y};
}
// direction to the left of the provided one, on the ground
glm::dvec2 left_of(glm::dvec2 const &Direction)
{
return glm::dvec2{Direction.y, -Direction.x};
}
// angle of the turn from one direction to another, positive to the left
double turn_angle(glm::dvec2 const &From, glm::dvec2 const &To)
{
return std::atan2(glm::dot(To, left_of(From)), glm::dot(To, From));
}
} // namespace
// puts provided shapes in the section of the scene holding specified point
void owned_shapes::show(std::vector<scene::shape_node> Shapes, glm::dvec3 const &Location)
{
if (m_section != nullptr || Shapes.empty() || simulation::Region == nullptr || false == simulation::Region->point_inside(Location))
{
return;
}
auto shapes{create_shapes()};
if (shapes.empty())
{
return;
}
auto &section{simulation::Region->section(location())};
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();
ownedbanks.emplace(m_bank.bank);
}
for (auto &shape : shapes)
for (auto &shape : Shapes)
{
// shapes held by a section are drawn relative to its centre
shape.origin(section.m_area.center);
@@ -935,14 +961,14 @@ void road_node::show()
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
// the section may need to reach further to keep the shape 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;
}
// takes back geometry put in the scene by show()
void road_node::hide()
// takes the shapes back
void owned_shapes::hide()
{
if (m_section == nullptr)
{
@@ -960,6 +986,650 @@ void road_node::hide()
m_section = nullptr;
}
// true if specified geometry was put in the scene by any object of this class
bool owned_shapes::owns(gfx::geometry_handle const &Geometry)
{
return Geometry.bank != 0 && ownedbanks.count(Geometry.bank) != 0;
}
// puts geometry of the road in the scene as shapes of its own, which can be taken back
void road_node::show()
{
if (m_merged || m_shapes.shown())
{
return;
}
m_shapes.show(create_shapes(), location());
}
// takes back geometry put in the scene by show()
void road_node::hide()
{
m_shapes.hide();
}
// brings the content to a usable form
void junction_node::state::normalize()
{
for (auto &arm : arms)
{
arm.incoming = std::clamp(arm.incoming, 0, 8);
arm.outgoing = std::clamp(arm.outgoing, 0, 8);
arm.width = std::max(1.f, arm.width);
arm.direction = (glm::length(arm.direction) > 1e-6 ? glm::normalize(arm.direction) : glm::dvec2{0.0, 1.0});
}
if (texturelength < 0.01f)
{
texturelength = 4.f;
}
if (surface.empty())
{
surface = "none";
}
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";
}
}
// width of the road at specified arm
double junction_node::state::arm_width(std::size_t const Arm) const
{
return Arm < arms.size() ? static_cast<double>(arms[Arm].incoming + arms[Arm].outgoing) * arms[Arm].width : 0.0;
}
// where a lane of an arm meets the junction; the lanes of each kind are counted from the middle of the road, starting with 1
glm::dvec3 junction_node::state::lane_point(std::size_t const Arm, bool const Incoming, int const Lane) const
{
auto const &arm{arms[Arm]};
// seen from the junction the traffic coming in is on the left side of the road, and the one going out on the right
auto const divide{0.5 * arm_width(Arm) - static_cast<double>(arm.incoming) * arm.width};
auto const offset{Incoming ? divide + (Lane - 0.5) * arm.width : divide - (Lane - 0.5) * arm.width};
return arm.position + flat(left_of(arm.direction)) * offset;
}
// arms in the order they're met going around the junction
std::vector<std::size_t> junction_node::state::arm_order() const
{
std::vector<std::size_t> order(arms.size());
for (std::size_t idx = 0; idx < order.size(); ++idx)
{
order[idx] = idx;
}
// the order goes from the z axis towards the x axis, which makes triangles spanned from the centre face up
std::sort(order.begin(), order.end(), [this](std::size_t const Left, std::size_t const Right) { return std::atan2(arms[Left].direction.x, arms[Left].direction.y) < std::atan2(arms[Right].direction.x, arms[Right].direction.y); });
return order;
}
// edge of the surface: ends of the arms joined with rounded corners, going around the junction
std::vector<glm::dvec3> junction_node::state::outline(std::vector<std::vector<glm::dvec3>> *Corners) const
{
std::vector<glm::dvec3> points;
auto const order{arm_order()};
for (std::size_t idx = 0; idx < order.size(); ++idx)
{
auto const &arm{arms[order[idx]]};
auto const &next{arms[order[(idx + 1) % order.size()]]};
auto const halfwidth{0.5 * arm_width(order[idx])};
auto const nexthalfwidth{0.5 * arm_width(order[(idx + 1) % order.size()])};
// the end of the arm, then the corner leading to the next one
auto const right{arm.position - flat(left_of(arm.direction)) * halfwidth};
auto const left{arm.position + flat(left_of(arm.direction)) * halfwidth};
auto const nextright{next.position - flat(left_of(next.direction)) * nexthalfwidth};
points.emplace_back(right);
points.emplace_back(left);
// the corner is bent towards the point where the edges of both roads would meet, if there's a reasonable one
auto control{0.5 * (left + nextright)};
auto const cross{arm.direction.x * next.direction.y - arm.direction.y * next.direction.x};
if (std::abs(cross) > 0.05)
{
// left - arm.direction * t == nextright - next.direction * s
glm::dvec2 const delta{nextright.x - left.x, nextright.z - left.z};
auto const t{-(delta.x * next.direction.y - delta.y * next.direction.x) / cross};
auto const s{-(delta.x * arm.direction.y - delta.y * arm.direction.x) / cross};
if (t > 0.0 && s > 0.0 && t < 100.0 && s < 100.0)
{
control = left - flat(arm.direction) * t;
control.y = 0.5 * (left.y + nextright.y);
}
}
std::vector<glm::dvec3> corner{left};
for (int step = 1; step < corner_steps; ++step)
{
auto const t{static_cast<double>(step) / corner_steps};
auto const point{(1.0 - t) * (1.0 - t) * left + 2.0 * (1.0 - t) * t * control + t * t * nextright};
points.emplace_back(point);
corner.emplace_back(point);
}
corner.emplace_back(nextright);
if (Corners != nullptr)
{
Corners->emplace_back(std::move(corner));
}
}
return points;
}
junction_node::junction_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 'endjunction'
void junction_node::import(cParser &Input, glm::dvec3 const &Offset)
{
auto const label{m_name.empty() ? std::string{"unnamed junction"} : "junction \"" + m_name + "\""};
m_state = state{};
Input.getTokens(3);
Input >> m_state.centre.x >> m_state.centre.y >> m_state.centre.z;
m_state.centre += Offset;
auto token{Input.getToken<std::string>()};
while (false == token.empty() && token != "endjunction")
{
if (token == "arm")
{
// arm <position> <direction x z> <lanes in> <lanes out> <lane width>
arm_data arm;
Input.getTokens(3);
Input >> arm.position.x >> arm.position.y >> arm.position.z;
arm.position += Offset;
Input.getTokens(2);
Input >> arm.direction.x >> arm.direction.y;
Input.getTokens(3);
Input >> arm.incoming >> arm.outgoing >> arm.width;
m_state.arms.emplace_back(arm);
}
else if (token == "surface")
{
m_state.surface = Input.getToken<std::string>();
replace_slashes(m_state.surface);
}
else if (token == "texlength")
{
Input.getTokens();
Input >> m_state.texturelength;
}
else if (token == "markings")
{
auto const colour{Input.getToken<std::string>()};
m_state.markings = (colour == "white" ? road_node::marking_colour::white : colour == "orange" ? road_node::marking_colour::orange : road_node::marking_colour::none);
}
else if (token == "velocity")
{
Input.getTokens();
Input >> m_state.velocity;
}
else if (token == "friction")
{
Input.getTokens();
Input >> m_state.friction;
}
else if (token == "environment")
{
m_state.environment = Input.getToken<std::string>();
}
else
{
ErrorLog("Bad junction: unknown property: \"" + token + "\" defined for " + label);
}
token = Input.getToken<std::string>();
}
if (m_state.arms.size() < 2 || m_state.arms.size() > 4)
{
ErrorLog("Bad junction: " + label + " has " + std::to_string(m_state.arms.size()) + " arms, it takes 2 to 4");
if (m_state.arms.size() > 4)
{
m_state.arms.resize(4);
}
}
m_state.normalize();
location(m_state.centre);
}
void junction_node::define(state const &State)
{
m_state = State;
m_state.normalize();
location(m_state.centre);
m_area.radius = -1.f;
}
// makes paths leading through the junction, between the lanes of the roads attached to it at the moment
void junction_node::create_links()
{
m_movements.clear();
auto const &arms{m_state.arms};
// lanes of the roads attached to the arms; a road is attached if one of its ends is where the arm is
struct arm_lanes
{
std::vector<TTrack *> incoming;
std::vector<TTrack *> outgoing;
};
std::vector<arm_lanes> lanes(arms.size());
for (std::size_t arm = 0; arm < arms.size(); ++arm)
{
lanes[arm].incoming.assign(static_cast<std::size_t>(arms[arm].incoming), nullptr);
lanes[arm].outgoing.assign(static_cast<std::size_t>(arms[arm].outgoing), nullptr);
for (auto const *road : simulation::Roads.sequence())
{
if (road == nullptr || road->m_editorremoved || road->tracks().size() != road->definition().lanes.size())
{
continue;
}
auto const &layout{road->definition()};
for (auto const atend : {false, true})
{
if (glm::distance(layout.axis.points[atend ? segment_data::point::end : segment_data::point::start], arms[arm].position) > junction_reach)
{
continue;
}
// the lanes heading for this end of the road lead into the junction, the others lead out of it
for (int lane = 1; lane <= arms[arm].incoming; ++lane)
{
auto const index{layout.lane_index((atend ? "f" : "b") + std::to_string(lane))};
if (index >= 0 && road->tracks()[index] != nullptr && glm::distance(glm::dvec3{road->tracks()[index]->CurrentSegment()->FastGetPoint_1()}, m_state.lane_point(arm, true, lane)) < junction_fit)
{
lanes[arm].incoming[lane - 1] = road->tracks()[index];
}
}
for (int lane = 1; lane <= arms[arm].outgoing; ++lane)
{
auto const index{layout.lane_index((atend ? "b" : "f") + std::to_string(lane))};
if (index >= 0 && road->tracks()[index] != nullptr && glm::distance(glm::dvec3{road->tracks()[index]->CurrentSegment()->FastGetPoint_0()}, m_state.lane_point(arm, false, lane)) < junction_fit)
{
lanes[arm].outgoing[lane - 1] = road->tracks()[index];
}
}
}
}
}
enum class way
{
left,
straight,
right
};
struct exit_data
{
std::size_t arm;
double turn;
way kind;
TTrack *track;
};
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
auto const write_point = [&text](glm::dvec3 const &Point) { text << Point.x << ' ' << Point.y << ' ' << Point.z << ' '; };
for (std::size_t arm = 0; arm < arms.size(); ++arm)
{
auto const heading{-arms[arm].direction};
// the other arms with a road to leave by, from the leftmost to the rightmost
std::vector<exit_data> exits;
for (std::size_t other = 0; other < arms.size(); ++other)
{
if (other == arm || std::all_of(lanes[other].outgoing.begin(), lanes[other].outgoing.end(), [](TTrack const *Track) { return Track == nullptr; }))
{
continue;
}
exits.push_back({other, turn_angle(heading, arms[other].direction), way::straight, nullptr});
}
std::sort(exits.begin(), exits.end(), [](exit_data const &Left, exit_data const &Right) { return Left.turn > Right.turn; });
if (exits.size() == 3)
{
exits[0].kind = way::left;
exits[2].kind = way::right;
}
else
{
auto const sideways{glm::radians(50.0)};
for (auto &exit : exits)
{
exit.kind = (exit.turn > sideways ? way::left : exit.turn < -sideways ? way::right : way::straight);
}
if (exits.size() == 2 && exits[0].kind == exits[1].kind)
{
// two ways of the same kind are told apart by which one is more to the left
if (exits[0].kind == way::right)
{
exits[0].kind = way::straight;
}
else if (exits[0].kind == way::left)
{
exits[1].kind = way::straight;
}
else
{
exits[0].kind = way::left;
exits[1].kind = way::right;
}
}
}
auto const lanecount{static_cast<int>(lanes[arm].incoming.size())};
for (int lane = 1; lane <= lanecount; ++lane)
{
auto *entry{lanes[arm].incoming[lane - 1]};
if (entry == nullptr)
{
continue;
}
// with more lanes than one the inner lane takes the left turns, the outer one the right turns, and all go straight
std::vector<exit_data> ways;
for (auto const &exit : exits)
{
auto const allowed{lanecount == 1 || exit.kind == way::straight || (exit.kind == way::left && lane == 1) || (exit.kind == way::right && lane == lanecount)};
if (allowed)
{
ways.emplace_back(exit);
}
}
if (ways.empty())
{
ways = exits;
}
for (auto &exit : ways)
{
// the lane of the same number on the road to leave by, or the nearest one there is
auto const &candidates{lanes[exit.arm].outgoing};
for (int shift = 0; shift < static_cast<int>(candidates.size()) && exit.track == nullptr; ++shift)
{
for (auto const index : {lane - 1 - shift, lane - 1 + shift})
{
if (index >= 0 && index < static_cast<int>(candidates.size()) && candidates[index] != nullptr)
{
exit.track = candidates[index];
break;
}
}
}
if (exit.track == nullptr)
{
exit.track = candidates.back();
}
}
ways.erase(std::remove_if(ways.begin(), ways.end(), [](exit_data const &Exit) { return Exit.track == nullptr; }), ways.end());
if (ways.empty())
{
continue;
}
if (ways.size() == 3)
{
// the legacy crossroads has the way straight ahead at its second point, the left one at the third and the right one at the fourth
std::swap(ways[0], ways[1]);
}
// shape of the ways: each leaves the lane coming in the way that lane goes, and joins the lane going out the way that one goes
glm::dvec3 const start{entry->CurrentSegment()->FastGetPoint_1()};
std::vector<glm::dvec3> ends;
std::vector<glm::dvec3> endcontrols;
double reach{0.0};
for (auto const &exit : ways)
{
glm::dvec3 const end{exit.track->CurrentSegment()->FastGetPoint_0()};
auto const span{glm::distance(start, end)};
ends.emplace_back(end);
endcontrols.emplace_back(-flat(arms[exit.arm].direction) * (0.39 * span));
reach += 0.39 * span / ways.size();
}
auto const startcontrol{flat(heading) * reach};
for (std::size_t idx = 0; idx < ways.size(); ++idx)
{
segment_data movement;
movement.points[segment_data::point::start] = start;
movement.points[segment_data::point::control1] = startcontrol;
movement.points[segment_data::point::control2] = endcontrols[idx];
movement.points[segment_data::point::end] = ends[idx];
m_movements.emplace_back(movement);
}
text.str("");
text << (ways.size() == 1 ? "road " : "cross ") << glm::distance(start, ends[0]) << ' ' << arms[arm].width << ' ' << m_state.friction << ' ' << m_state.sounddistance << ' ' << m_state.quality << " 0 " << m_state.environment << " unvis ";
// first path: from the lane coming in to the first way out
write_point(start);
text << "0 ";
write_point(startcontrol);
write_point(endcontrols[0]);
write_point(ends[0]);
text << "0 0 ";
if (ways.size() == 2)
{
// second path of a crossroads of three roads starts where the first one does
write_point(start);
text << "0 ";
write_point(startcontrol);
write_point(endcontrols[1]);
write_point(ends[1]);
text << "0 0 ";
}
else if (ways.size() == 3)
{
// second path of a crossroads of four roads runs between its third and fourth point
write_point(ends[1]);
text << "0 ";
write_point(endcontrols[1]);
write_point(endcontrols[2]);
write_point(ends[2]);
text << "0 0 ";
}
if (m_state.velocity > 0.f)
{
text << "velocity " << m_state.velocity << ' ';
}
text << "endtrack";
auto *link{create_track(text.str(), m_links.size())};
// the path is tied to the lanes by hand: the lanes going out are shared by the paths from all the other arms,
// which is more than the automatic joining of path ends can express
if (ways.size() == 1)
{
entry->ConnectNextPrev(link, 0);
link->trNext = ways[0].track;
link->iNextDirection = 0;
if (ways[0].track->trPrev == nullptr)
{
ways[0].track->trPrev = link;
ways[0].track->iPrevDirection = 1;
}
}
else if (link->SwitchExtension != nullptr)
{
auto &extension{*link->SwitchExtension};
// the points of the crossroads are stored as: first, second, fourth, third
extension.pPrevs[0] = entry;
extension.iPrevDirection[0] = 1;
extension.pNexts[0] = ways[0].track;
extension.iNextDirection[0] = 0;
extension.pPrevs[1] = (ways.size() == 3 ? ways[2].track : ways[1].track);
extension.iPrevDirection[1] = 0;
extension.pNexts[1] = (ways.size() == 3 ? ways[1].track : nullptr);
extension.iNextDirection[1] = 0;
link->Switch(0);
entry->trNext = link;
entry->iNextDirection = 0;
// the lanes going out get to know one of the paths leading to them, for whatever goes backwards
int const ends2[]{1, 2};
int const ends3[]{1, 3, 2};
for (std::size_t idx = 0; idx < ways.size(); ++idx)
{
if (ways[idx].track->trPrev == nullptr)
{
ways[idx].track->trPrev = link;
ways[idx].track->iPrevDirection = (ways.size() == 3 ? ends3[idx] : ends2[idx]);
}
}
}
m_links.emplace_back(link);
}
}
}
// gives up the paths leading through the junction
std::vector<TTrack *> junction_node::release_links()
{
std::vector<TTrack *> released;
released.swap(m_links);
m_movements.clear();
return released;
}
// true if there's a vehicle on any path of the junction
bool junction_node::occupied() const
{
return std::any_of(m_links.begin(), m_links.end(), [](TTrack const *Track) { return Track != nullptr && false == Track->Dynamics.empty(); });
}
// generates geometry of the surface
std::vector<scene::shape_node> junction_node::create_shapes() const
{
std::vector<scene::shape_node> shapes;
std::vector<std::vector<glm::dvec3>> corners;
auto const points{m_state.outline(&corners)};
if (points.size() < 3)
{
return shapes;
}
auto const &centre{m_state.centre};
if (m_state.surface != "none")
{
// a fan of triangles spanned from the centre, with the image laid out flat over the ground
auto const tile{static_cast<double>(texture_ratio(m_state.surface)) * m_state.texturelength};
auto const vertex = [&](glm::dvec3 const &Point, glm::vec3 const &Normal) {
return world_vertex{Point, Normal, {static_cast<float>(0.5 + (Point.x - centre.x) / tile), static_cast<float>(0.5 + (Point.z - centre.z) / m_state.texturelength)}};
};
std::vector<world_vertex> vertices;
for (std::size_t idx = 0; idx < points.size(); ++idx)
{
auto const &current{points[idx]};
auto const &next{points[(idx + 1) % points.size()]};
auto normal{glm::cross(current - centre, next - centre)};
if (glm::length2(normal) < 1e-12)
{
continue;
}
glm::vec3 const facing{glm::normalize(normal)};
vertices.emplace_back(vertex(centre, facing));
vertices.emplace_back(vertex(current, facing));
vertices.emplace_back(vertex(next, facing));
}
if (false == vertices.empty())
{
scene::shape_node shape;
shape.make_terrain(GfxRenderer->Fetch_Material(m_state.surface), std::move(vertices), glm::dvec3{0.0});
shapes.emplace_back(std::move(shape));
}
}
if (m_state.markings != road_node::marking_colour::none)
{
// edge lines carried around the corners
std::vector<world_vertex> vertices;
glm::dvec3 const lift{0.0, line_lift, 0.0};
glm::vec3 const upwards{0.f, 1.f, 0.f};
for (auto const &corner : corners)
{
for (std::size_t idx = 0; idx + 1 < corner.size(); ++idx)
{
// the line keeps its distance from the edge, on the side of the centre
auto const inwards = [&](glm::dvec3 const &Point) {
auto direction{centre - Point};
direction.y = 0.0;
return glm::length2(direction) > 1e-12 ? glm::normalize(direction) : glm::dvec3{0.0};
};
auto const outer0{corner[idx] + inwards(corner[idx]) * edge_inset + lift};
auto const inner0{corner[idx] + inwards(corner[idx]) * (edge_inset + line_width) + lift};
auto const outer1{corner[idx + 1] + inwards(corner[idx + 1]) * edge_inset + lift};
auto const inner1{corner[idx + 1] + inwards(corner[idx + 1]) * (edge_inset + line_width) + lift};
// the corners are listed going around the junction the way which makes these face up
vertices.push_back({inner0, upwards, {0.f, 0.f}});
vertices.push_back({outer0, upwards, {1.f, 0.f}});
vertices.push_back({outer1, upwards, {1.f, 1.f}});
vertices.push_back({inner0, upwards, {0.f, 0.f}});
vertices.push_back({outer1, upwards, {1.f, 1.f}});
vertices.push_back({inner1, upwards, {0.f, 1.f}});
}
}
if (false == vertices.empty())
{
lighting_data paint;
paint.diffuse = (m_state.markings == road_node::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;
scene::shape_node shape;
shape.make_terrain(GfxRenderer->Fetch_Material("colored"), std::move(vertices), glm::dvec3{0.0});
shape.lighting(paint);
shapes.emplace_back(std::move(shape));
}
}
return shapes;
}
void junction_node::show()
{
if (m_merged || m_shapes.shown())
{
return;
}
m_shapes.show(create_shapes(), location());
}
void junction_node::hide()
{
m_shapes.hide();
}
// creates a path through the junction from provided definition and registers it with the simulation
TTrack *junction_node::create_track(std::string const &Definition, std::size_t const Index)
{
cParser parser(Definition, cParser::buffer_TEXT);
scene::node_data nodedata;
nodedata.range_max = -1.0;
nodedata.name = (m_name.empty() ? std::string{} : m_name + ":way" + std::to_string(Index + 1));
nodedata.type = "track";
auto *track{new TTrack(nodedata)};
track->Load(&parser, glm::dvec3{0.0});
track->m_road = this;
// NOTE: the table points the name at the newest path, which is what a junction made anew in the editor needs
simulation::Paths.insert(track);
simulation::Region->insert_and_register(track);
return track;
}
float junction_node::radius_()
{
float radius{0.f};
for (std::size_t arm = 0; arm < m_state.arms.size(); ++arm)
{
radius = std::max(radius, static_cast<float>(glm::distance(m_state.arms[arm].position, m_state.centre) + 0.5 * m_state.arm_width(arm)));
}
return radius;
}
void junction_node::serialize_(std::ostream &Output) const
{
// TODO: implement
}
void junction_node::deserialize_(std::istream &Input)
{
// TODO: implement
}
// export() subclass details, sends basic content of the class in legacy (text) format to provided stream
void junction_node::export_as_text_(std::ostream &Output) const
{
Output << "junction ";
auto const precision{Output.precision(std::numeric_limits<double>::digits10)};
Output << m_state.centre.x << ' ' << m_state.centre.y << ' ' << m_state.centre.z << ' ';
for (auto const &arm : m_state.arms)
{
Output << "arm " << arm.position.x << ' ' << arm.position.y << ' ' << arm.position.z << ' ' << arm.direction.x << ' ' << arm.direction.y << ' ' << arm.incoming << ' ' << arm.outgoing << ' ' << arm.width << ' ';
}
Output.precision(precision);
Output << "surface " << m_state.surface << ' ' << "texlength " << m_state.texturelength << ' ' << "markings "
<< (m_state.markings == road_node::marking_colour::white ? "white" : m_state.markings == road_node::marking_colour::orange ? "orange" : "none") << ' ' << "velocity " << m_state.velocity << ' ' << "friction " << m_state.friction << ' '
<< "environment " << m_state.environment << ' ';
Output << "endjunction"
<< "\n";
}
// creates a path for the vehicles and registers it with the simulation
TTrack *road_node::create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity)
{
@@ -1116,4 +1786,42 @@ void road_table::create_geometry(scene::scratch_data &Scratchpad)
}
}
// legacy style initialization, to be performed when the tracks are joined, ahead of the roads
void junction_table::InitJunctions()
{
for (auto *junction : m_items)
{
if (junction != nullptr)
{
junction->create_links();
}
}
}
// generates geometry of the junctions and puts it in the scene
void junction_table::create_geometry(scene::scratch_data &Scratchpad)
{
if (simulation::Region == nullptr)
{
return;
}
for (auto *junction : m_items)
{
if (junction == nullptr)
{
continue;
}
if (Global.editor_session && false == Global.NvRenderer)
{
junction->show();
continue;
}
for (auto &shape : junction->create_shapes())
{
simulation::Region->insert(shape, Scratchpad, false);
}
junction->merged(true);
}
}
//---------------------------------------------------------------------------

View File

@@ -26,6 +26,27 @@ struct scratch_data;
class basic_section;
}
// geometry a node puts in the scene as shapes of its own, apart from the shapes of the scenery, so it can take it back
class owned_shapes
{
public:
// puts provided shapes in the section of the scene holding specified point
void show(std::vector<scene::shape_node> Shapes, glm::dvec3 const &Location);
// takes the shapes back
void hide();
bool shown() const
{
return m_section != nullptr;
}
// true if specified geometry was put in the scene by any object of this class
static bool owns(gfx::geometry_handle const &Geometry);
private:
gfx::geometrybank_handle m_bank{0, 0};
std::vector<gfx::geometry_handle> m_geometry;
scene::basic_section *m_section{nullptr};
};
// a piece of a road for wheeled traffic: an axis, and a cross-section which stays the same along it.
// the node itself is neither driven on nor drawn. it produces the lanes, which are ordinary invisible one-way tracks,
// and the surface, the sides and the markings, which are ordinary shapes. this way the vehicles, the ai and the renderers
@@ -176,10 +197,108 @@ class road_node : public scene::basic_node
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};
owned_shapes m_shapes; // geometry put in the scene by show()
};
// a place where roads meet. each arm of the junction is where a road ends: a point in the middle of that end, the way
// the road leaves, and the lanes it has there. the junction produces the surface between the arms, and paths leading
// the vehicles from each lane coming in to the lanes going out of the other arms. a lane with more than one way out
// gets a legacy crossroads path, which is what the vehicles and their drivers already know how to pick a way through.
// scenery entry:
// node <max> <min> <name> junction <centre> arm <position> <direction x z> <lanes in> <lanes out> <lane width> ... [<property> <values>]... endjunction
class junction_node : public scene::basic_node
{
public:
// types
struct arm_data
{
glm::dvec3 position{0.0}; // middle of the end of the road
glm::dvec2 direction{0.0, 1.0}; // the way out of the junction, seen from above (x, z)
int incoming{1}; // lanes leading into the junction
int outgoing{1}; // lanes leading out of it
float width{3.5f}; // width of a lane
};
struct state
{
glm::dvec3 centre{0.0};
std::vector<arm_data> arms;
std::string surface{"asphaltdark1"};
float texturelength{4.f};
road_node::marking_colour markings{road_node::marking_colour::white};
float velocity{30.f}; // speed limit on the way through
float friction{0.85f};
float sounddistance{25.f};
int quality{15};
std::string environment{"flat"};
// brings the content to a usable form
void normalize();
// width of the road at specified arm
double arm_width(std::size_t const Arm) const;
// where a lane of an arm meets the junction; the lanes of each kind are counted from the middle of the road, starting with 1
glm::dvec3 lane_point(std::size_t const Arm, bool const Incoming, int const Lane) const;
// arms in the order they're met going around the junction
std::vector<std::size_t> arm_order() const;
// edge of the surface: ends of the arms joined with rounded corners, going around the junction.
// Corners, if provided, receives the points of each corner apart
std::vector<glm::dvec3> outline(std::vector<std::vector<glm::dvec3>> *Corners = nullptr) const;
};
// constructors
explicit junction_node(scene::node_data const &Nodedata);
// methods
// restores content of the node from provided input stream; reads up to and including the closing 'endjunction'
void import(cParser &Input, glm::dvec3 const &Offset);
state const &definition() const
{
return m_state;
}
void define(state const &State);
// makes paths leading through the junction, between the lanes of the roads attached to it at the moment
void create_links();
// gives up the paths leading through the junction; the caller is expected to take them out of use
std::vector<TTrack *> release_links();
std::vector<TTrack *> const &links() const
{
return m_links;
}
// shapes of the ways through the junction, for display
std::vector<segment_data> const &movements() const
{
return m_movements;
}
// true if there's a vehicle on any path of the junction
bool occupied() const;
// generates geometry of the surface
std::vector<scene::shape_node> create_shapes() const;
// puts geometry of the junction in the scene as shapes of its own, or takes it back
void show();
void hide();
void merged(bool const Merged)
{
m_merged = Merged;
}
bool merged() const
{
return m_merged;
}
// members
bool m_editorremoved{false};
private:
// methods
float radius_() override;
void serialize_(std::ostream &Output) const override;
void deserialize_(std::istream &Input) override;
void export_as_text_(std::ostream &Output) const override;
// creates a path through the junction from provided definition and registers it with the simulation
TTrack *create_track(std::string const &Definition, std::size_t const Index);
// members
state m_state;
std::vector<TTrack *> m_links;
std::vector<segment_data> m_movements;
bool m_merged{false};
owned_shapes m_shapes;
};
// collection of roads present in the scene
@@ -193,4 +312,15 @@ class road_table : public basic_table<road_node>
void create_geometry(scene::scratch_data &Scratchpad);
};
// collection of road junctions present in the scene
class junction_table : public basic_table<junction_node>
{
public:
// legacy style initialization, to be performed when the tracks are joined, ahead of the roads
void InitJunctions();
// generates geometry of the junctions and puts it in the scene
void create_geometry(scene::scratch_data &Scratchpad);
};
//---------------------------------------------------------------------------

View File

@@ -186,7 +186,7 @@ public:
std::vector<segment_data> m_paths; // source data for owned paths
bool m_editorremoved { false };
road_node *m_road { nullptr }; // road which generated this path as one of its lanes. such path is one-way, and isn't saved nor edited on its own
scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own
int iterate_stamp = 0;
// sleepermodel optional parameter -------------------------------------------------