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

feat(roads): single piece widening with generated transitions, point height tools, fix for the frame rate drop in the road tools

This commit is contained in:
2026-10-03 22:18:53 +02:00
parent 1d4c448e30
commit 502c609b13
6 changed files with 528 additions and 108 deletions

View File

@@ -299,6 +299,10 @@ namespace
{
if (instance == nullptr || instance->Model() == nullptr || instance->radius() < ModelRadius)
continue;
// going through the triangles of a model which doesn't reach the rectangle is all cost and no gain
double const reach = instance->radius();
if (instance->location().x + reach < Min.x || instance->location().x - reach > Max.x || instance->location().z + reach < Min.y || instance->location().z - reach > Max.y)
continue;
std::vector<world_triangle> modeltriangles;
gather_submodel_triangles(instance->Model()->Root, instance_matrix(*instance), modeltriangles);
for (auto const &t : modeltriangles)
@@ -310,6 +314,20 @@ namespace
}
}
// ground kept for the road tools, which ask about the same places frame after frame. it's gathered a tile at a time:
// gathering is what takes time, so a place asked about for the first time costs a single tile a frame instead of a stall
struct ground_tile
{
std::unique_ptr<triangle_grid> grid;
double gathered{0.0}; // when, on the clock of the user interface
int used{0}; // frame it was last asked for
};
double constexpr ground_tile_size = 256.0;
double constexpr ground_tile_lifetime = 30.0; // older tiles are gathered again when asked for, to catch up with the scenery
std::size_t constexpr ground_tile_limit = 96;
std::map<std::pair<int, int>, ground_tile> ground_tiles;
int ground_tile_frame{-1}; // frame the last tile was gathered in without being told to
// scenery file name without path and extension
std::string scenery_stem()
{
@@ -360,45 +378,36 @@ std::vector<double> editor_mode::ground_heights(std::vector<glm::dvec3> const &P
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;
}
int const frame = ImGui::GetFrameCount();
// 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];
std::pair<int, int> const key{static_cast<int>(std::floor(p.x / ground_tile_size)), static_cast<int>(std::floor(p.z / ground_tile_size))};
auto &tile = ground_tiles[key];
bool const missing = (tile.grid == nullptr);
bool const stale = missing || now < tile.gathered || now - tile.gathered > ground_tile_lifetime;
// told to wait the caller gets every tile it needs, otherwise what's there, with one tile gathered each frame
if (stale && (Fresh ? missing : ground_tile_frame != frame))
{
glm::dvec2 const tilemin{key.first * ground_tile_size, key.second * ground_tile_size};
glm::dvec2 const tilemax{tilemin + glm::dvec2{ground_tile_size}};
std::vector<world_triangle> triangles;
// the same ground as the area fill uses, terrain tile models included
gather_ground_triangles(tilemin, tilemax, true, 50.0f, triangles, true);
tile.grid = std::make_unique<triangle_grid>(std::move(triangles), tilemin, tilemax);
tile.gathered = now;
if (false == Fresh)
ground_tile_frame = frame;
}
tile.used = frame;
double const ceiling = p.y + height_tolerance;
double y = 0.0;
bool found = cache.grid->height_at(p.x, p.z, ceiling, y);
bool found = (tile.grid != nullptr) && tile.grid->height_at(p.x, p.z, ceiling, y);
for (editor_terrain *terrain : terrains)
{
if (!terrain->contains(p.x, p.z))
@@ -413,6 +422,17 @@ std::vector<double> editor_mode::ground_heights(std::vector<glm::dvec3> const &P
if (found)
heights[i] = y;
}
// the tiles left alone the longest go once there are too many
while (ground_tiles.size() > ground_tile_limit)
{
auto oldest = ground_tiles.begin();
for (auto tile = ground_tiles.begin(); tile != ground_tiles.end(); ++tile)
if (tile->second.used < oldest->second.used)
oldest = tile;
if (oldest->second.used == frame)
break;
ground_tiles.erase(oldest);
}
return heights;
}
@@ -1925,6 +1945,8 @@ void editor_mode::handle_terrain_sculpt(double Deltatime)
// crossing a chunk boundary edits both and shared-edge vertices stay in sync
for (editor_terrain *terrain : active_terrains())
terrain->sculpt(world.x, world.z, m_terrain_brush_radius, signedrate);
// what the road tools know about the ground is no longer true
ground_tiles.clear();
}
void editor_mode::capture_terrain()

View File

@@ -521,6 +521,7 @@ class editor_mode : public application_mode
float offset{0.1f}; // height above the ground the roads are built at
bool follow{true}; // built roads are led over the ground between the clicked points
float crossingspeed{30.f}; // speed limit on the junctions being made
float heightstep{0.1f}; // how much the selected points are raised or lowered at a time
road_plan plan; // what a click would make at the moment, worked out once a frame for display
glm::dvec3 hover{0.0}; // point of a road or a junction under the cursor a road can be led out of
bool hashover{false};
@@ -542,11 +543,11 @@ class editor_mode : public application_mode
bool road_delete();
bool road_split();
void road_cancel();
// sets the width of the road, or its height, at the selected points
// sets the width of the road at the selected points, or the height of each of them
void road_point_width(float const Width);
void road_point_height(double const Height);
bool road_points_apply(std::vector<std::pair<road_node *, road_node::state>> const &Changes, std::size_t const Skipped);
// Fresh: the ground is looked up anew instead of going by what was gathered for the previous frames
void road_point_heights(std::vector<double> const &Heights);
bool road_points_apply(std::vector<std::pair<road_node *, road_node::state>> const &Changes, std::vector<std::pair<junction_node *, junction_node::state>> const &Junctions);
// Fresh: the ground which wasn't looked up yet is gathered on the spot, instead of a bit of it each frame
road_plan road_preview(bool const Fresh);
// Firstgrade, Lastgrade: slopes the road has to start and end with, nullptr for none
void road_profile(std::vector<segment_data> &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh);

View File

@@ -55,8 +55,22 @@ class screen_projection
Screen = screen(c);
return true;
}
// true while there's room in the draw list for more. the user interface is drawn with 16 bit indices, so a list
// can't hold more than 65536 vertices; past that the lines come out as triangles strewn all over the screen,
// which takes the frame rate down with it. the limit doesn't apply if the renderer can deal with larger lists
bool room(ImDrawList const *Drawlist) const
{
return (Drawlist->Flags & ImDrawListFlags_AllowVtxOffset) != 0 || Drawlist->VtxBuffer.Size < limit;
}
// true if a line between two points on the screen can't be seen
bool outside(ImVec2 const &A, ImVec2 const &B) const
{
return (A.x < 0.0f && B.x < 0.0f) || (A.y < 0.0f && B.y < 0.0f) || (A.x > m_size.x && B.x > m_size.x) || (A.y > m_size.y && B.y > m_size.y);
}
void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const
{
if (false == room(Drawlist))
return;
glm::vec4 a = clip(A);
glm::vec4 b = clip(B);
if (a.w < kNear && b.w < kNear)
@@ -65,8 +79,17 @@ class screen_projection
a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w));
else if (b.w < kNear)
b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w));
Drawlist->AddLine(screen(a), screen(b), Color, Thickness);
auto const from{screen(a)};
auto const to{screen(b)};
if (outside(from, to))
return;
Drawlist->AddLine(from, to, Color, Thickness);
}
// members
int limit{kVertexLimit}; // vertices the draw list can hold before the drawing stops
static constexpr int kVertexLimit{56000}; // leaves room for what's drawn after the overlay
static constexpr int kLaneVertexLimit{36000}; // the lanes give way to the tools, which are drawn after them
private:
static constexpr float kNear{0.1f};
@@ -129,21 +152,15 @@ ImU32 const kPreviewColor{IM_COL32(255, 210, 60, 255)};
ImU32 const kRefusedColor{IM_COL32(240, 60, 60, 255)};
ImU32 const kSnapColor{IM_COL32(255, 60, 255, 255)};
// gathers new definitions for the pieces which meet or end at provided points. Change: receives a definition, and which end of it is at the point.
// Skipped: receives the number of points left out for being at a junction, which is made for the road the way it is
// gathers new definitions for the pieces which meet or end at provided points.
// Change: receives a definition, which end of it is at the point, and the number of the point
template <class Change_>
std::vector<std::pair<road_node *, road_node::state>> point_changes(std::vector<glm::dvec3> const &Points, std::size_t &Skipped, Change_ Change)
std::vector<std::pair<road_node *, road_node::state>> point_changes(std::vector<glm::dvec3> const &Points, Change_ Change)
{
std::vector<std::pair<road_node *, road_node::state>> changes;
Skipped = 0;
for (auto const &point : Points)
for (std::size_t index = 0; index < Points.size(); ++index)
{
if (editor_road::at_junction(point))
{
++Skipped;
continue;
}
for (auto const &end : editor_road::ends_at(point))
for (auto const &end : editor_road::ends_at(Points[index]))
{
// a piece with both its ends among the points gets both changed
auto entry{std::find_if(changes.begin(), changes.end(), [&end](std::pair<road_node *, road_node::state> const &Listed) { return Listed.first == end.road; })};
@@ -152,12 +169,27 @@ std::vector<std::pair<road_node *, road_node::state>> point_changes(std::vector<
changes.emplace_back(end.road, end.road->definition());
entry = std::prev(changes.end());
}
Change(entry->second, end.atend);
Change(entry->second, end.atend, index);
}
}
return changes;
}
// direction of a path for specified value of its curve parameter. like path_point() it's cheap enough to be asked for a lot each frame
glm::dvec3 path_direction(segment_data const &Path, double const T)
{
auto const &p0 = Path.points[segment_data::point::start];
auto const &p3 = Path.points[segment_data::point::end];
auto const &cp1 = Path.points[segment_data::point::control1];
auto const &cp2 = Path.points[segment_data::point::control2];
if (cp1 == glm::dvec3{0.0} && cp2 == glm::dvec3{0.0})
return p3 - p0;
auto const p1 = p0 + cp1;
auto const p2 = p3 + cp2;
double const u = 1.0 - T;
return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2);
}
}
void editor_mode::render_road_menu()
@@ -418,7 +450,6 @@ void editor_mode::road_profile(std::vector<segment_data> &Pieces, double const *
return;
}
auto const planned{Pieces};
bool fresh{Fresh};
for (auto const step : kProfileSteps)
{
Pieces = editor_road::refine(planned, step);
@@ -434,8 +465,7 @@ void editor_mode::road_profile(std::vector<segment_data> &Pieces, double const *
// 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;
auto ground{ground_heights(points, Fresh)};
for (auto &height : ground)
height += tool.offset;
std::vector<double> heights(ground.begin(), ground.begin() + count + 1);
@@ -702,9 +732,19 @@ void editor_mode::road_apply()
changes.emplace_back(road, state);
}
editor_road::apply(changes);
// what's next to the changed pieces is fitted to them: junctions take the lanes they have now, the pieces of the road
// which stay as they were get wider or narrower towards them, or give up their ends for the lanes to change
std::string notes;
for (auto *road : pieces)
editor_road::settle(*road, record, notes);
auto const fitted{record.roads.size() + record.roads_removed.size() + record.junctions.size() + record.junctions_created.size() - pieces.size()};
push_road_snapshot(std::move(record));
tool.settings = tool.selected->definition();
tool.status = (pieces.size() == 1 ? std::string{"Changed"} : "Changed " + std::to_string(pieces.size()) + " pieces");
tool.status = (pieces.size() == 1 ? std::string{"Changed."} : "Changed " + std::to_string(pieces.size()) + " pieces.");
if (fitted > 0)
tool.status += " What's next to it was fitted to it.";
if (false == notes.empty())
tool.status += " " + notes;
}
bool editor_mode::road_delete()
@@ -774,19 +814,18 @@ bool editor_mode::road_split()
}
// makes the changes gathered for the selected points. returns: true if they were made
bool editor_mode::road_points_apply(std::vector<std::pair<road_node *, road_node::state>> const &Changes, std::size_t const Skipped)
bool editor_mode::road_points_apply(std::vector<std::pair<road_node *, road_node::state>> const &Changes, std::vector<std::pair<junction_node *, junction_node::state>> const &Junctions)
{
auto &tool{m_roadtool};
std::string const leftalone{Skipped > 0 ? " The points at junctions were left alone, as a junction is made for the road the way it is." : ""};
if (Changes.empty())
if (Changes.empty() && Junctions.empty())
{
tool.status = "Nothing was changed." + leftalone;
tool.status = "There's nothing to change at the selected points";
return false;
}
editor_road::record record;
std::string reason;
for (auto const &change : Changes)
{
std::string reason;
if (false == editor_road::can_edit(*change.first, &reason))
{
tool.status = "\"" + change.first->name() + "\" can't be changed: " + reason;
@@ -794,39 +833,67 @@ bool editor_mode::road_points_apply(std::vector<std::pair<road_node *, road_node
}
record.roads.emplace_back(change.first, change.first->definition());
}
for (auto const &change : Junctions)
{
if (false == editor_road::can_edit(*change.first, &reason))
{
tool.status = "\"" + change.first->name() + "\" can't be changed: " + reason;
return false;
}
record.junctions.emplace_back(change.first, change.first->definition());
}
editor_road::apply(Changes);
editor_road::apply(Junctions);
// junctions the changed pieces lead to are made for the roads as they are now
std::string notes;
for (auto const &change : Changes)
editor_road::settle(*change.first, record, notes);
push_road_snapshot(std::move(record));
tool.status = (Changes.size() == 1 ? std::string{"Changed 1 piece."} : "Changed " + std::to_string(Changes.size()) + " pieces.") + leftalone;
tool.status = (Changes.size() == 1 ? std::string{"Changed 1 piece."} : "Changed " + std::to_string(Changes.size()) + " pieces.");
if (false == notes.empty())
tool.status += " " + notes;
return true;
}
void editor_mode::road_point_width(float const Width)
{
std::size_t skipped{0};
// the pieces on both sides of a point get to that width there, and from there go back to their own along their length
auto const changes{point_changes(m_roadtool.points, skipped, [Width](road_node::state &State, bool const Atend) {
auto const changes{point_changes(m_roadtool.points, [Width](road_node::state &State, bool const Atend, std::size_t const) {
auto const width{State.width()};
if (width > 0.0)
State.taper[Atend ? 1 : 0] = std::clamp(static_cast<float>(Width / width), 0.25f, 4.f);
})};
road_points_apply(changes, skipped);
road_points_apply(changes, {});
}
void editor_mode::road_point_height(double const Height)
// Heights: one for each of the selected points
void editor_mode::road_point_heights(std::vector<double> const &Heights)
{
auto &tool{m_roadtool};
std::size_t skipped{0};
// the control points are kept relative to the ends, so the slopes the pieces have at the point stay
auto const changes{point_changes(tool.points, skipped, [Height](road_node::state &State, bool const Atend) { State.axis.points[Atend ? segment_data::point::end : segment_data::point::start].y = Height; })};
// the selection follows what it's made of
std::vector<bool> moved;
for (auto const &point : tool.points)
moved.emplace_back(false == editor_road::at_junction(point));
if (false == road_points_apply(changes, skipped))
if (Heights.size() != tool.points.size())
return;
for (std::size_t i = 0; i < tool.points.size(); ++i)
if (moved[i])
tool.points[i].y = Height;
// the control points are kept relative to the ends, so the slopes the pieces have at a point stay what they are
auto const changes{point_changes(tool.points, [&Heights](road_node::state &State, bool const Atend, std::size_t const Point) { State.axis.points[Atend ? segment_data::point::end : segment_data::point::start].y = Heights[Point]; })};
// an arm of a junction goes up or down with the end of the road it's made for
std::vector<std::pair<junction_node *, junction_node::state>> junctions;
for (std::size_t index = 0; index < tool.points.size(); ++index)
{
for (auto const &arm : editor_road::arms_at(tool.points[index]))
{
auto entry{std::find_if(junctions.begin(), junctions.end(), [&arm](std::pair<junction_node *, junction_node::state> const &Listed) { return Listed.first == arm.first; })};
if (entry == junctions.end())
{
junctions.emplace_back(arm.first, arm.first->definition());
entry = std::prev(junctions.end());
}
entry->second.arms[arm.second].position.y = Heights[index];
}
}
if (false == road_points_apply(changes, junctions))
return;
// the selection follows what it's made of
for (std::size_t index = 0; index < tool.points.size(); ++index)
tool.points[index].y = Heights[index];
}
void editor_mode::push_road_snapshot(editor_road::record Record)
@@ -1187,13 +1254,40 @@ void editor_mode::render_road_window()
road_point_width(std::clamp(width, 1.0f, 100.0f));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Combined width of the lanes at this point; each lane takes its share of it.\nThe pieces on both sides get wider or narrower along their length to meet it.\nSelect several points to keep the width over the pieces between them.");
// heights: set outright, moved by a step, or taken from the ground
auto height{static_cast<float>(tool.points.front().y)};
if (number("Height [m]", height, 0.1f, "%.2f"))
road_point_height(height);
road_point_heights(std::vector<double>(tool.points.size(), height));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Height of the axis of the road at this point. The slopes of the pieces at the point are kept.");
if (editor_road::at_junction(tool.points.front()))
ImGui::TextDisabled("This point is at a junction, which is made for the road the way it is");
ImGui::SetTooltip("%s", "Height of the axis of the road at the point; with several points selected all are put at this height.\nThe slopes of the pieces at the point are kept, so the road stays smooth.");
std::vector<double> heights;
for (auto const &point : tool.points)
heights.emplace_back(point.y);
auto const moved{ImGui::Button("Lower") ? -1.0 : 0.0};
ImGui::SameLine();
auto const raised{ImGui::Button("Raise") ? 1.0 : 0.0};
ImGui::SameLine();
ImGui::SetNextItemWidth(80.0f);
if (ImGui::InputFloat("by [m]", &tool.heightstep, 0.0f, 0.0f, "%.2f"))
tool.heightstep = std::clamp(tool.heightstep, 0.01f, 50.0f);
if (moved + raised != 0.0)
{
// each point moves from where it is, so the shape of the road between them is kept
for (auto &entry : heights)
entry += (moved + raised) * tool.heightstep;
road_point_heights(heights);
}
if (ImGui::Button("Put on the ground"))
{
// a point with nothing under it comes back with the height it has, and is left where it is
auto const ground{ground_heights(tool.points, true)};
for (std::size_t index = 0; index < heights.size(); ++index)
if (ground[index] != tool.points[index].y)
heights[index] = ground[index] + tool.offset;
road_point_heights(heights);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Puts each point %.2f m over the ground under it; the height above the ground is set in the Build tool.", tool.offset);
}
}
else if (tool.junction != nullptr)
@@ -1220,6 +1314,26 @@ void editor_mode::render_road_window()
tool.status = "\"" + junction.name() + "\" can't be changed: " + reason;
}
}
auto centre{static_cast<float>(junction.definition().centre.y)};
if (number("Height of the middle [m]", centre, 0.1f, "%.2f"))
{
if (editor_road::can_edit(junction, &reason))
{
editor_road::record record;
record.junctions.emplace_back(&junction, junction.definition());
auto state{junction.definition()};
state.centre.y = centre;
editor_road::apply(std::vector<std::pair<junction_node *, junction_node::state>>{{&junction, state}});
push_road_snapshot(std::move(record));
tool.status = "Changed";
}
else
{
tool.status = "\"" + junction.name() + "\" can't be changed: " + reason;
}
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "The surface is spanned between the ends of the roads and this point.\nThe ends of the roads are raised or lowered as points: select the white dot at the end of a road.");
if (ImGui::Button("Delete (Del)"))
road_delete();
}
@@ -1233,7 +1347,7 @@ void editor_mode::render_road_window()
ImGui::TextDisabled("K: split at the cursor");
ImGui::Checkbox("Changes go to the whole road", &tool.whole);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nSplit a piece (K) to get a zone with rules of its own.");
ImGui::SetTooltip("%s", "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nWith this off the selected piece alone is changed, and the road next to it is fitted to it:\nmade wider or narrower towards it, or given a stretch where the lanes change if their number differs.\nSplit a piece (K) to get a piece of the length you need.");
ImGui::Separator();
if (tool.selected != nullptr && render_road_layout(tool.settings))
road_apply();
@@ -1256,9 +1370,15 @@ void editor_mode::draw_road_overlay() const
if (false == tool.lanes && false == tool.window)
return;
screen_projection const projection;
screen_projection projection;
ImDrawList *drawlist = ImGui::GetBackgroundDrawList();
glm::dvec3 const camera{Global.pCamera.Pos};
// pieces of a line needed to draw a path: one for a straight, more the longer a bend is and the nearer it is
auto const pieces = [&camera](segment_data const &Path, glm::dvec3 const &Location) {
auto const range{glm::distance(Location, camera)};
auto const length{glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end])};
return std::clamp(static_cast<int>(length / (range < 100.0 ? 3.0 : range < 300.0 ? 8.0 : 20.0)), 2, 12);
};
auto const drawpath = [&](segment_data const &Path, ImU32 const Color, float const Thickness, int const Samples) {
auto previous{path_point(Path, 0.0)};
for (int i = 1; i <= Samples; ++i)
@@ -1272,14 +1392,16 @@ void editor_mode::draw_road_overlay() const
// lanes, as the vehicles see them: a line for each, with an arrow pointing where the traffic goes
if (tool.lanes)
{
projection.limit = screen_projection::kLaneVertexLimit;
for (auto const *road : simulation::Roads.sequence())
{
if (road == nullptr || road->m_editorremoved)
continue;
auto const range{glm::distance(road->location(), camera)};
if (range > kLaneRange + 0.5 * road->definition().length())
if (range > kLaneRange + 0.5 * glm::distance(road->definition().axis.points[segment_data::point::start], road->definition().axis.points[segment_data::point::end]))
continue;
auto const samples{is_curved(road->definition().axis) ? (range < 150.0 ? 12 : 6) : 1};
if (false == projection.room(drawlist))
break;
auto const against{static_cast<std::size_t>(road->definition().backward)};
auto const &tracks{road->tracks()};
for (std::size_t lane = 0; lane < tracks.size(); ++lane)
@@ -1288,7 +1410,7 @@ void editor_mode::draw_road_overlay() const
continue;
auto const &path{tracks[lane]->m_paths.front()};
auto const color{lane >= against ? kForwardColor : kBackwardColor};
drawpath(path, color, 2.0f, samples);
drawpath(path, color, 2.0f, is_curved(path) ? pieces(path, road->location()) : 1);
if (range > 300.0)
continue;
auto const middle{path_point(path, 0.5)};
@@ -1303,7 +1425,7 @@ void editor_mode::draw_road_overlay() const
}
for (auto const *turn : road->turns())
{
if (turn != nullptr && false == turn->m_paths.empty())
if (turn != nullptr && false == turn->m_paths.empty() && range < 300.0)
drawpath(turn->m_paths.front(), kTurnColor, 1.5f, 8);
}
}
@@ -1313,22 +1435,23 @@ void editor_mode::draw_road_overlay() const
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);
drawpath(way, kWayColor, 1.5f, pieces(way, junction->location()));
}
projection.limit = screen_projection::kVertexLimit;
}
if (false == tool.window)
return;
// outline of a road piece: its axis and the edges of its surface
auto const drawroad = [&](road_node::state const &State, ImU32 const Color) {
auto const samples{is_curved(State.axis) || State.taper[0] != State.taper[1] ? 12 : 1};
auto const samples{is_curved(State.axis) || State.taper[0] != State.taper[1] ? pieces(State.axis, State.axis.points[segment_data::point::start]) : 1};
drawpath(State.axis, Color, 3.0f, samples);
glm::dvec3 previous[2];
for (int i = 0; i <= samples; ++i)
{
auto const t{static_cast<double>(i) / samples};
auto const position{path_point(State.axis, t)};
auto const tangent{State.tangent(t)};
auto const tangent{path_direction(State.axis, t)};
glm::dvec3 across{tangent.z, 0.0, -tangent.x};
if (glm::length(across) > 1e-6)
across = glm::normalize(across);
@@ -1345,7 +1468,7 @@ void editor_mode::draw_road_overlay() const
};
auto const drawpoint = [&](glm::dvec3 const &Point, ImU32 const Color, float const Radius, bool const Filled) {
ImVec2 screen;
if (false == projection.project(Point, screen))
if (false == projection.room(drawlist) || false == projection.project(Point, screen) || projection.outside(screen, screen))
return;
if (Filled)
drawlist->AddCircleFilled(screen, Radius, Color);

View File

@@ -34,6 +34,7 @@ double const kTightestRadius{6.0};
double const kCornerRoom{4.0}; // length of an arm of a junction past the point where its edge meets the edge of the next one, taken by the rounded corner
double const kSharpestBranch{glm::radians(30.0)}; // smallest angle between two roads leaving a junction
double const kWidestCorner{glm::radians(120.0)}; // arms further apart than this don't have to make room for each other
double const kTransitionLength{30.0}; // length of the road taken by a change of its lanes
void add_unique(std::vector<road_node *> &Roads, road_node *Road)
{
@@ -141,22 +142,53 @@ road_node::state part(road_node::state const &State, double const From, double c
return result;
}
// points of a path as a bezier curve, and a point of that curve. unlike the methods of a road definition
// these don't measure the whole axis for each call, which matters for what's done every frame
std::array<glm::dvec3, 4> curve_points(segment_data const &Path)
{
auto const &p0{Path.points[segment_data::point::start]};
auto const &p3{Path.points[segment_data::point::end]};
if (Path.points[segment_data::point::control1] == glm::dvec3{0.0} && Path.points[segment_data::point::control2] == glm::dvec3{0.0})
return {p0, p0 + (p3 - p0) / 3.0, p3 - (p3 - p0) / 3.0, p3};
return {p0, p0 + Path.points[segment_data::point::control1], p3 + Path.points[segment_data::point::control2], p3};
}
glm::dvec3 curve_point(std::array<glm::dvec3, 4> const &Points, double const T)
{
auto const u{1.0 - T};
return u * u * u * Points[0] + 3.0 * u * u * T * Points[1] + 3.0 * u * T * T * Points[2] + T * T * T * Points[3];
}
int const kStations{64};
// distances along a path for evenly spaced values of its curve parameter
std::vector<double> stations(road_node::state const &State)
{
std::vector<double> result(kStations + 1, 0.0);
auto previous{State.point(0.0)};
auto const points{curve_points(State.axis)};
auto previous{points[0]};
for (int i = 1; i <= kStations; ++i)
{
auto const current{State.point(static_cast<double>(i) / kStations)};
auto const current{curve_point(points, static_cast<double>(i) / kStations)};
result[i] = result[i - 1] + glm::distance(previous, current);
previous = current;
}
return result;
}
// adds to a record what a piece or a junction is like, ahead of changing it. the first entry stays, as it's what taking the lot back has to end with
void remember(editor_road::record &Record, road_node *Road)
{
if (std::none_of(Record.roads.begin(), Record.roads.end(), [Road](std::pair<road_node *, road_node::state> const &Entry) { return Entry.first == Road; }))
Record.roads.emplace_back(Road, Road->definition());
}
void remember(editor_road::record &Record, junction_node *Junction)
{
if (std::none_of(Record.junctions.begin(), Record.junctions.end(), [Junction](std::pair<junction_node *, junction_node::state> const &Entry) { return Entry.first == Junction; }))
Record.junctions.emplace_back(Junction, Junction->definition());
}
// distance along a path for specified value of its curve parameter, and the other way around
double station_at(std::vector<double> const &Stations, double const T)
{
@@ -242,6 +274,19 @@ bool follow(road_node &Road, double const T, bool const Forward, double const Di
return false;
}
// arm of a junction for the part of a road left on one side of it. returns: false if the lanes there differ in width
bool cut_arm(road_cut const &Cut, junction_node::arm_data &Arm)
{
auto const &state{Cut.road->definition()};
Arm.position = state.point(Cut.t);
// the way out of the junction is the way the kept part of the piece goes from the cut
Arm.direction = planar(state.tangent(Cut.t)) * (Cut.keepstart ? -1.0 : 1.0);
// lanes heading for the cut lead into the junction
Arm.incoming = (Cut.keepstart ? state.forward : state.backward);
Arm.outgoing = (Cut.keepstart ? state.backward : state.forward);
return editor_road::lane_width(state, Cut.t, Arm.width);
}
// how far from the centre of a junction the edges of two of its roads meet, measured along the first one.
// Halfwidth, Otherhalfwidth: of the two roads, Angle: between the ways they leave
double edge_reach(double const Halfwidth, double const Otherhalfwidth, double const Angle)
@@ -715,8 +760,9 @@ std::vector<road_node *> editor_road::chain(road_node &Road)
double editor_road::nearest_parameter(road_node::state const &State, glm::dvec3 const &Point)
{
auto const points{curve_points(State.axis)};
auto const distance = [&](double const T) {
auto const position{State.point(T)};
auto const position{curve_point(points, T)};
return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z});
};
double best{0.0};
@@ -744,10 +790,12 @@ road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin)
if (road == nullptr || road->m_editorremoved)
continue;
auto const &state{road->definition()};
auto const reach{0.5 * state.width() + std::max(state.sides[0].width, state.sides[1].width) + Margin};
if (glm::distance(road->location(), Point) > 0.5 * state.length() + reach + 10.0)
auto const reach{0.5 * state.width() * std::max(state.taper[0], state.taper[1]) + std::max(state.sides[0].width, state.sides[1].width) + Margin};
// the axis is no longer than the lines joining its points, which is good enough to tell the road is nowhere near
auto const points{curve_points(state.axis)};
if (glm::distance(road->location(), Point) > 0.5 * (glm::distance(points[0], points[1]) + glm::distance(points[1], points[2]) + glm::distance(points[2], points[3])) + reach + 10.0)
continue;
auto const position{state.point(nearest_parameter(state, Point))};
auto const position{curve_point(points, nearest_parameter(state, Point))};
auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})};
// roads passing over or under the point don't count
if (distance > reach || std::abs(position.y - Point.y) > 3.0)
@@ -1097,9 +1145,168 @@ std::vector<editor_road::piece_end> editor_road::ends_at(glm::dvec3 const &Point
return result;
}
bool editor_road::at_junction(glm::dvec3 const &Point)
std::vector<std::pair<junction_node *, std::size_t>> editor_road::arms_at(glm::dvec3 const &Point)
{
return false == junctions_at(Point).empty();
std::vector<std::pair<junction_node *, std::size_t>> result;
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
auto const &arms{junction->definition().arms};
for (std::size_t arm = 0; arm < arms.size(); ++arm)
if (glm::distance(arms[arm].position, Point) < kSamePoint)
result.emplace_back(junction, arm);
}
return result;
}
void editor_road::settle(road_node &Road, record &Record, std::string &Notes)
{
for (auto const atend : {false, true})
{
if (Road.m_editorremoved)
return;
auto const &layout{Road.definition()};
auto const position{axis_end(layout, atend)};
// lanes heading for this end of the piece, and the ones leading away from it
auto const arriving{atend ? layout.forward : layout.backward};
auto const leaving{atend ? layout.backward : layout.forward};
float lanewidth{0.f};
auto const evenlanes{lane_width(layout, atend ? 1.0 : 0.0, lanewidth)};
char const *unevenlanes{" differ in width, and a junction takes roads with lanes of the same width. "};
std::string reason;
auto const junctions{arms_at(position)};
if (false == junctions.empty())
{
// a junction is made for the lanes of the road at each arm
for (auto const &entry : junctions)
{
auto *junction{entry.first};
auto state{junction->definition()};
auto &arm{state.arms[entry.second]};
if (arm.incoming == arriving && arm.outgoing == leaving && std::abs(arm.width - lanewidth) < 0.001f)
continue;
if (false == evenlanes)
{
Notes += "Lanes of \"" + Road.name() + "\"" + unevenlanes;
continue;
}
if (false == can_edit(*junction, &reason))
{
Notes += "\"" + junction->name() + "\" wasn't fitted: " + reason + ". ";
continue;
}
arm.incoming = arriving;
arm.outgoing = leaving;
arm.width = lanewidth;
remember(Record, junction);
apply(std::vector<std::pair<junction_node *, junction_node::state>>{{junction, state}});
}
continue;
}
auto const neighbours{roads_at(position, &Road)};
if (neighbours.size() != 1)
continue;
auto *other{neighbours.front()};
auto const otherlayout{other->definition()};
auto const otheratend{glm::distance(axis_end(otherlayout, true), position) < kSamePoint};
// lanes of the neighbour heading for the place they meet, and the ones leading away from it
auto const otherarriving{otheratend ? otherlayout.forward : otherlayout.backward};
auto const otherleaving{otheratend ? otherlayout.backward : otherlayout.forward};
if (otherarriving == leaving && otherleaving == arriving)
{
// the same lanes: the neighbour is made as wide as the piece where they meet, and gets to that width along its length
auto const width{layout.width() * layout.taper[atend ? 1 : 0]};
if (std::abs(width - otherlayout.width() * otherlayout.taper[otheratend ? 1 : 0]) < 0.001)
continue;
if (false == can_edit(*other, &reason))
{
Notes += "\"" + other->name() + "\" wasn't fitted: " + reason + ". ";
continue;
}
auto state{otherlayout};
state.taper[otheratend ? 1 : 0] = std::clamp(static_cast<float>(width / otherlayout.width()), 0.25f, 4.f);
remember(Record, other);
apply(std::vector<std::pair<road_node *, road_node::state>>{{other, state}});
continue;
}
// other lanes: the end of the neighbour is given up for a junction of the two, where the lanes of one are led to the lanes of the other
if (false == evenlanes)
{
Notes += "Lanes of \"" + Road.name() + "\"" + unevenlanes;
continue;
}
road_cut cut;
std::vector<road_node *> consumed;
std::string error;
bool room{false};
for (auto const length : {kTransitionLength, 0.5 * kTransitionLength})
{
consumed.clear();
if (follow(*other, otheratend ? 1.0 : 0.0, false == otheratend, length, cut, consumed, error))
{
room = true;
break;
}
}
if (false == room)
{
Notes += "There's no room next to \"" + Road.name() + "\" for the lanes to change (" + error + "), so they don't lead on there. ";
continue;
}
if (cut.road == &Road)
{
Notes += "The road is too short for its lanes to change. ";
continue;
}
std::vector<road_node *> removed{consumed};
if (cut.road != other)
removed.emplace_back(other);
// a cut at the very end the piece is kept from leaves it whole
auto const trimmed{cut.keepstart ? cut.t < 0.999 : cut.t > 0.001};
bool allowed{false == trimmed || can_edit(*cut.road, &reason)};
for (auto const *piece : removed)
allowed = allowed && can_edit(*piece, &reason);
junction_node::state junction;
junction_node::arm_data arms[2];
if (false == allowed)
{
Notes += "The lanes next to \"" + Road.name() + "\" weren't fitted: " + reason + ". ";
continue;
}
if (false == cut_arm(cut, arms[0]))
{
Notes += "Lanes of \"" + cut.road->name() + "\"" + unevenlanes;
continue;
}
arms[1].position = position;
arms[1].direction = planar(layout.tangent(atend ? 1.0 : 0.0)) * (atend ? -1.0 : 1.0);
arms[1].incoming = arriving;
arms[1].outgoing = leaving;
arms[1].width = lanewidth;
junction.arms.assign(std::begin(arms), std::end(arms));
junction.centre = 0.5 * (arms[0].position + arms[1].position);
dress(junction, layout);
// it's a stretch of the road, with the speed limit the road has
junction.velocity = std::min(layout.velocity > 0.f ? layout.velocity : std::numeric_limits<float>::max(), otherlayout.velocity > 0.f ? otherlayout.velocity : std::numeric_limits<float>::max());
if (junction.velocity == std::numeric_limits<float>::max())
junction.velocity = -1.f;
junction.normalize();
// room is made first, so nothing gets joined with what's about to go
remove(removed);
Record.roads_removed.insert(Record.roads_removed.end(), removed.begin(), removed.end());
if (trimmed)
{
remember(Record, cut.road);
apply(std::vector<std::pair<road_node *, road_node::state>>{{cut.road, cut.keepstart ? part(cut.road->definition(), 0.0, cut.t) : part(cut.road->definition(), cut.t, 1.0)}});
}
auto const created{create(std::vector<junction_node::state>{junction})};
Record.junctions_created.insert(Record.junctions_created.end(), created.begin(), created.end());
}
}
bool editor_road::lane_width(road_node::state const &State, double const T, float &Width)
@@ -1191,15 +1398,8 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const
junction.centre = centre;
for (auto const &cut : cuts)
{
auto const &state{cut.road->definition()};
junction_node::arm_data arm;
arm.position = state.point(cut.t);
// the way out of the junction is the way the kept part of the piece goes from the cut
arm.direction = planar(state.tangent(cut.t)) * (cut.keepstart ? -1.0 : 1.0);
// lanes heading for the cut lead into the junction
arm.incoming = (cut.keepstart ? state.forward : state.backward);
arm.outgoing = (cut.keepstart ? state.backward : state.forward);
if (false == lane_width(state, cut.t, arm.width))
if (false == cut_arm(cut, arm))
{
Error = "Lanes of \"" + cut.road->name() + "\" differ in width, and a junction takes roads with lanes of the same width";
return false;
@@ -1276,7 +1476,7 @@ junction_node *editor_road::carry_out(branch const &Branch, record &Record)
{
if (Branch.junction != nullptr)
{
Record.junctions.emplace_back(Branch.junction, Branch.junction->definition());
remember(Record, Branch.junction);
apply(std::vector<std::pair<junction_node *, junction_node::state>>{{Branch.junction, Branch.layout}});
return Branch.junction;
}
@@ -1284,7 +1484,7 @@ junction_node *editor_road::carry_out(branch const &Branch, record &Record)
remove(Branch.removed);
Record.roads_removed.insert(Record.roads_removed.end(), Branch.removed.begin(), Branch.removed.end());
for (auto const &change : Branch.changes)
Record.roads.emplace_back(change.first, change.first->definition());
remember(Record, change.first);
apply(Branch.changes);
auto const added{create(Branch.added)};
Record.roads_created.insert(Record.roads_created.end(), added.begin(), added.end());

View File

@@ -103,8 +103,13 @@ class editor_road
static bool nearest_joint(glm::dvec3 const &Point, double const Radius, glm::dvec3 &Joint);
// ends of the pieces which meet at specified point
static std::vector<piece_end> ends_at(glm::dvec3 const &Point);
// true if there's an arm of a junction at specified point
static bool at_junction(glm::dvec3 const &Point);
// junctions with an arm at specified point, each with the number of that arm
static std::vector<std::pair<junction_node *, std::size_t>> arms_at(glm::dvec3 const &Point);
// brings what's next to the ends of a piece in line with the piece, after its lanes or its width were changed:
// a junction gets its arm made for the lanes the piece has now, a neighbouring piece with the same lanes is made as wide
// as the piece where they meet, and one with other lanes gets a transition cut out of its end - a junction of two roads,
// which leads the lanes of one to the lanes of the other. what gets done is added to Record, what couldn't be done to Notes
static void settle(road_node &Road, record &Record, std::string &Notes);
// width shared by all lanes of the road at specified value of the curve parameter of its axis. returns: false if the lanes differ in width
static bool lane_width(road_node::state const &State, double const T, float &Width);
// lays out a junction for a road leaving the side of specified piece at specified value of the curve parameter of its axis.

View File

@@ -967,6 +967,20 @@ double turn_angle(glm::dvec2 const &From, glm::dvec2 const &To)
return std::atan2(glm::dot(To, left_of(From)), glm::dot(To, From));
}
// slope a lane has at one of its ends, in the direction of travel
double lane_grade(TTrack const &Lane, bool const Atend)
{
if (Lane.m_paths.empty())
{
return 0.0;
}
auto const &path{Lane.m_paths.front()};
auto const &control{path.points[Atend ? segment_data::point::control2 : segment_data::point::control1]};
auto const direction{control != glm::dvec3{0.0} ? (Atend ? -control : control) : path.points[segment_data::point::end] - path.points[segment_data::point::start]};
auto const run{glm::length(glm::dvec2{direction.x, direction.z})};
return run > 1e-6 ? direction.y / run : 0.0;
}
} // namespace
// puts provided shapes in the section of the scene holding specified point
@@ -1389,6 +1403,21 @@ void junction_node::create_links()
// 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]);
}
if (exits.size() == 1 && lane == lanecount)
{
// where the only road to leave by has more lanes than the one coming in, the road gets wider: the lanes it gains
// are entered from the outermost lane. a path has up to three ways out, so that's two lanes gained at most
auto const &candidates{lanes[exits[0].arm].outgoing};
for (auto extra = static_cast<std::size_t>(lanecount); extra < candidates.size() && ways.size() < 3; ++extra)
{
if (candidates[extra] != nullptr && candidates[extra] != ways.front().track)
{
auto way{ways.front()};
way.track = candidates[extra];
ways.emplace_back(way);
}
}
}
// 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()};
@@ -1400,10 +1429,11 @@ void junction_node::create_links()
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));
// the slopes of the lanes are carried on, so a junction on a hill doesn't make a step
endcontrols.emplace_back(-(flat(arms[exit.arm].direction) + up * lane_grade(*exit.track, false)) * (0.39 * span));
reach += 0.39 * span / ways.size();
}
auto const startcontrol{flat(heading) * reach};
auto const startcontrol{(flat(heading) + up * lane_grade(*entry, true)) * reach};
for (std::size_t idx = 0; idx < ways.size(); ++idx)
{
segment_data movement;
@@ -1556,6 +1586,45 @@ std::vector<scene::shape_node> junction_node::create_shapes() const
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};
auto const &arms{m_state.arms};
if (arms.size() == 2 && arms[0].incoming > 0 && arms[0].outgoing > 0 && arms[1].incoming > 0 && arms[1].outgoing > 0)
{
// a junction of two roads is where a road changes its lanes. the double line keeping the directions apart is led through it
auto const divide = [&](std::size_t const Arm) { return arms[Arm].position + flat(left_of(arms[Arm].direction)) * (0.5 * m_state.arm_width(Arm) - static_cast<double>(arms[Arm].incoming) * arms[Arm].width); };
auto const p0{divide(0)};
auto const p3{divide(1)};
auto const reach{0.39 * glm::distance(p0, p3)};
auto const p1{p0 - flat(arms[0].direction) * reach};
auto const p2{p3 - flat(arms[1].direction) * reach};
auto const pair{0.5 * (line_width + line_spacing)};
int const steps{12};
glm::dvec3 previous{p0};
glm::dvec3 previousleft{flat(left_of(-arms[0].direction))};
for (int step = 1; step <= steps; ++step)
{
auto const t{static_cast<double>(step) / steps};
auto const u{1.0 - t};
auto const current{u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3};
auto const direction{3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2)};
glm::dvec2 const heading{direction.x, direction.z};
auto const left{glm::length(heading) > 1e-9 ? flat(left_of(glm::normalize(heading))) : previousleft};
for (auto const side : {-1.0, 1.0})
{
auto const leftstart{previous + previousleft * (side * pair + 0.5 * line_width) + lift};
auto const rightstart{previous + previousleft * (side * pair - 0.5 * line_width) + lift};
auto const leftend{current + left * (side * pair + 0.5 * line_width) + lift};
auto const rightend{current + left * (side * pair - 0.5 * line_width) + lift};
vertices.push_back({leftstart, upwards, {0.f, 0.f}});
vertices.push_back({rightstart, upwards, {1.f, 0.f}});
vertices.push_back({leftend, upwards, {0.f, 1.f}});
vertices.push_back({rightstart, upwards, {1.f, 0.f}});
vertices.push_back({rightend, upwards, {1.f, 1.f}});
vertices.push_back({leftend, upwards, {0.f, 1.f}});
}
previous = current;
previousleft = left;
}
}
for (auto const &corner : corners)
{
for (std::size_t idx = 0; idx + 1 < corner.size(); ++idx)