diff --git a/application/editormode.cpp b/application/editormode.cpp index 7053fe6a..1a947be5 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -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 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 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, 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 editor_mode::ground_heights(std::vector 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 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 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(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 const key{static_cast(std::floor(p.x / ground_tile_size)), static_cast(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 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(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 editor_mode::ground_heights(std::vector 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() diff --git a/application/editormode.h b/application/editormode.h index b2352179..e34c6e2f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -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> 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 const &Heights); + bool road_points_apply(std::vector> const &Changes, std::vector> 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 &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh); diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 09427d7f..300899f4 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -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 -std::vector> point_changes(std::vector const &Points, std::size_t &Skipped, Change_ Change) +std::vector> point_changes(std::vector const &Points, Change_ Change) { std::vector> 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 const &Listed) { return Listed.first == end.road; })}; @@ -152,12 +169,27 @@ std::vector> 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 &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 &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 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> const &Changes, std::size_t const Skipped) +bool editor_mode::road_points_apply(std::vector> const &Changes, std::vector> 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::vectordefinition()); } + 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(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 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 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> 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 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(tool.points.front().y)}; if (number("Height [m]", height, 0.1f, "%.2f")) - road_point_height(height); + road_point_heights(std::vector(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 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(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>{{&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(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(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(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); diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index aa37f692..65358eb0 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -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 &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 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 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 stations(road_node::state const &State) { std::vector 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(i) / kStations)}; + auto const current{curve_point(points, static_cast(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 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 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 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 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::ends_at(glm::dvec3 const &Point return result; } -bool editor_road::at_junction(glm::dvec3 const &Point) +std::vector> editor_road::arms_at(glm::dvec3 const &Point) { - return false == junctions_at(Point).empty(); + std::vector> 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>{{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(width / otherlayout.width()), 0.25f, 4.f); + remember(Record, other); + apply(std::vector>{{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 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 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::max(), otherlayout.velocity > 0.f ? otherlayout.velocity : std::numeric_limits::max()); + if (junction.velocity == std::numeric_limits::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>{{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})}; + 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>{{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()); diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp index 751d04c6..fd41bb0f 100644 --- a/editor/editorRoad.hpp +++ b/editor/editorRoad.hpp @@ -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 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> 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. diff --git a/world/Road.cpp b/world/Road.cpp index b9e4a537..c43a7516 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -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(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 junction_node::create_shapes() const std::vector 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(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(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)