diff --git a/application/editormode.cpp b/application/editormode.cpp index 37a779dc..7053fe6a 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -231,11 +231,13 @@ namespace }; // appends triangles of a shape node which overlap specified XZ rectangle - void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) + void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out, bool const Skiproads = false) { auto const &data = Shape.data(); if (editor_orthophoto::owns(data.geometry)) return; // the orthophoto laid over the ground isn't ground itself + if (Skiproads && owned_shapes::owns(data.geometry)) + return; // neither are the roads, for the ones which ask where to put a road if (data.area.radius >= 0.0f) { double const r = data.area.radius; @@ -275,7 +277,7 @@ namespace // ground triangles overlapping an XZ rectangle (Min/Max are x,z). shapes hold the (legacy) terrain, // model instances at least ModelRadius large (if ModelsAsGround) the terrain tiles - void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector &Out) + void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector &Out, bool const Skiproads = false) { glm::dvec2 const center = (Min + Max) * 0.5; float const radius = static_cast(glm::length(Max - Min) * 0.5); @@ -283,7 +285,7 @@ namespace for (auto *section : sections) { for (auto const &shape : section->m_shapes) - gather_shape_triangles(shape, Min, Max, Out); + gather_shape_triangles(shape, Min, Max, Out, Skiproads); for (auto &cell : section->m_cells) { double const r = cell.m_area.radius; @@ -349,6 +351,71 @@ editor_mode::editor_mode() { }); } +std::vector editor_mode::ground_heights(std::vector const &Points, bool const Fresh) +{ + std::vector heights; + heights.reserve(Points.size()); + for (auto const &point : Points) + heights.push_back(point.y); + if (Points.empty() || simulation::Region == nullptr) + return heights; + + glm::dvec2 bmin{Points.front().x, Points.front().z}; + glm::dvec2 bmax{bmin}; + for (auto const &point : Points) + { + bmin = glm::min(bmin, glm::dvec2{point.x, point.z}); + bmax = glm::max(bmax, glm::dvec2{point.x, point.z}); + } + // the road tools ask about the same place frame after frame, so the ground is gathered with room to spare and kept for a moment + struct ground_cache + { + glm::dvec2 min{0.0}; + glm::dvec2 max{0.0}; + std::unique_ptr 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; + } + + // surfaces much higher than the point are something it's under, rather than the ground + double constexpr height_tolerance = 50.0; + auto const terrains = active_terrains(); + for (std::size_t i = 0; i < Points.size(); ++i) + { + auto const &p = Points[i]; + double const ceiling = p.y + height_tolerance; + double y = 0.0; + bool found = cache.grid->height_at(p.x, p.z, ceiling, y); + for (editor_terrain *terrain : terrains) + { + if (!terrain->contains(p.x, p.z)) + continue; + double const h = terrain->height_at(p.x, p.z); + if (h <= ceiling && (!found || h > y)) + { + y = h; + found = true; + } + } + if (found) + heights[i] = y; + } + return heights; +} + editor_ui *editor_mode::ui() const { return static_cast(m_userinterface.get()); diff --git a/application/editormode.h b/application/editormode.h index f7cfff48..12562559 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -110,6 +110,8 @@ class editor_mode : public application_mode std::vector> roads; // road pieces with what they were like before the change std::vector roads_created; std::vector roads_removed; + std::vector junctions_created; + std::vector junctions_removed; }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -491,8 +493,9 @@ class editor_mode : public application_mode { bool window{false}; // the road window is open, the left mouse button belongs to its tools bool lanes{true}; // lanes of the roads are drawn as trajectories - int tool{0}; // 0: select, 1: build + int tool{0}; // 0: select, 1: build, 2: junction road_node *selected{nullptr}; + junction_node *junction{nullptr}; // selected junction road_node::state settings; // layout shown in the window: of the selected piece, or of the pieces about to be built bool whole{true}; // layout changes go to every piece of the road the selected one belongs to bool chain{false}; // the point to build from is set @@ -500,6 +503,18 @@ class editor_mode : public application_mode glm::dvec2 direction{0.0, 1.0}; // the way it has to leave that point bool hasdirection{false}; bool reversed{false}; // the pieces are laid from their ends, to carry on a road from its start + double grade{0.0}; // slope the next piece has to start with, to carry on what it's attached to + bool hasgrade{false}; + float offset{0.1f}; // height above the ground the roads and junctions are built at + bool follow{true}; // built roads are led over the ground between the clicked points + int kind{0}; // kind of the junctions being placed, an editor_road::junction_kind + float crossingspeed{30.f}; // speed limit on the junctions being placed + // what a click would make at the moment, worked out once a frame for display + std::vector preview; + junction_node::state previewjunction; + bool hasjunction{false}; + editor_road::loose_end previewsnap; + std::string previewerror; glm::dvec3 mouse{0.0}; std::string status; char surface[128]{}; @@ -513,12 +528,20 @@ class editor_mode : public application_mode void update_road_tool(); void road_click(); void road_select(road_node *Road); + void junction_select(junction_node *Junction); + void junction_click(); + bool junction_preview(junction_node::state &State, editor_road::loose_end &Snap, std::string &Error) const; void road_apply(); bool road_delete(); bool road_split(); void road_cancel(); - std::vector road_preview(editor_road::loose_end &Snap, std::string &Error) const; - void push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed); + // Fresh: the ground is looked up anew instead of going by what was gathered for the previous frames + std::vector road_preview(editor_road::loose_end &Snap, std::string &Error, bool const Fresh); + void road_profile(std::vector &Pieces, editor_road::loose_end const &Snap, bool const Fresh); + // height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height + std::vector ground_heights(std::vector const &Points, bool const Fresh); + void push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed, std::vector Junctionscreated = {}, + std::vector Junctionsremoved = {}); void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); road_tool m_roadtool; }; diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 27210dc2..2dcba223 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "imgui/imgui.h" #include #include +#include #include namespace @@ -108,10 +109,16 @@ std::string lower_case(std::string Text) double const kSnapRadius{4.0}; // an end of a road this close to the cursor is what the click is taken to mean double const kReach{3000.0}; // clicks landing further than this are taken for misses double const kLaneRange{600.0}; // lanes are drawn for the roads this close to the camera +double const kLongestStretch{2000.0}; // a single click doesn't lead a road further than this +// lengths of the pieces a road following the ground is tried with, from the longest; the first one to fit the ground is taken +double const kProfileSteps[]{50.0, 25.0, 12.5}; +double const kSunkTolerance{0.05}; // how far under the height it's built at the road can get between the points it's led through +double const kRaisedTolerance{0.5}; // and how far over it ImU32 const kForwardColor{IM_COL32(70, 230, 110, 230)}; ImU32 const kBackwardColor{IM_COL32(255, 170, 50, 230)}; ImU32 const kTurnColor{IM_COL32(210, 210, 210, 150)}; +ImU32 const kWayColor{IM_COL32(120, 200, 255, 170)}; ImU32 const kSelectedColor{IM_COL32(40, 220, 255, 230)}; ImU32 const kPreviewColor{IM_COL32(255, 210, 60, 255)}; ImU32 const kRefusedColor{IM_COL32(240, 60, 60, 255)}; @@ -134,12 +141,29 @@ void editor_mode::update_road_tool() auto &tool{m_roadtool}; if (tool.selected != nullptr && tool.selected->m_editorremoved) tool.selected = nullptr; + if (tool.junction != nullptr && tool.junction->m_editorremoved) + tool.junction = nullptr; + tool.preview.clear(); + tool.hasjunction = false; + tool.previewsnap = {}; + tool.previewerror.clear(); if (false == tool.window) { tool.chain = false; return; } - tool.mouse = Global.pCamera.Pos + GfxRenderer->Mouse_Position(); + glm::dvec3 const camera{Global.pCamera.Pos}; + tool.mouse = camera + GfxRenderer->Mouse_Position(); + if (glm::distance(tool.mouse, camera) > kReach) + { + tool.previewerror = "The cursor isn't over anything near enough"; + return; + } + // what a click would make at the moment, to be drawn over the view + if (tool.tool == 1 && tool.chain) + tool.preview = road_preview(tool.previewsnap, tool.previewerror, false); + else if (tool.tool == 2) + tool.hasjunction = junction_preview(tool.previewjunction, tool.previewsnap, tool.previewerror); } void editor_mode::road_select(road_node *Road) @@ -148,6 +172,7 @@ void editor_mode::road_select(road_node *Road) tool.selected = Road; if (Road != nullptr) { + tool.junction = nullptr; tool.settings = Road->definition(); std::string reason; tool.status = "Selected: " + (Road->name().empty() ? std::string{"(unnamed road)"} : Road->name()); @@ -159,20 +184,33 @@ void editor_mode::road_select(road_node *Road) copy_text(tool.sides[side], sizeof(tool.sides[side]), tool.settings.sides[side].material); } +void editor_mode::junction_select(junction_node *Junction) +{ + auto &tool{m_roadtool}; + tool.junction = Junction; + if (Junction == nullptr) + return; + tool.selected = nullptr; + std::string reason; + tool.status = "Selected: " + Junction->name(); + if (false == editor_road::can_edit(*Junction, &reason)) + tool.status += ". It can't be removed: " + reason; +} + void editor_mode::road_cancel() { m_roadtool.chain = false; } -std::vector editor_mode::road_preview(editor_road::loose_end &Snap, std::string &Error) const +std::vector editor_mode::road_preview(editor_road::loose_end &Snap, std::string &Error, bool const Fresh) { auto const &tool{m_roadtool}; Snap = editor_road::find_end(tool.mouse, kSnapRadius); // the end the road is being drawn from isn't somewhere to lead it to - if (Snap.road != nullptr && glm::distance(Snap.position, tool.point) < 0.5) + if (Snap.valid() && glm::distance(Snap.position, tool.point) < 0.5) Snap = {}; glm::dvec2 enddirection{0.0, 1.0}; - if (Snap.road != nullptr) + if (Snap.valid()) { glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z}; if (glm::length(outwards) > 1e-6) @@ -180,7 +218,204 @@ std::vector editor_mode::road_preview(editor_road::loose_end &Snap else Snap = {}; } - return editor_road::plan(tool.point, tool.hasdirection ? &tool.direction : nullptr, Snap.road != nullptr ? Snap.position : tool.mouse, Snap.road != nullptr ? &enddirection : nullptr, Global.ctrlState, Error); + // in the open the road is put over what's under the cursor + auto const target{Snap.valid() ? Snap.position : tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}}; + if (glm::distance(tool.point, target) > kLongestStretch) + { + Error = "Too far from the last point"; + return {}; + } + if (Snap.junction != nullptr) + { + // the junction has its ways through made for a set of lanes at each arm + auto const arriving{tool.reversed ? tool.settings.backward : tool.settings.forward}; + auto const leaving{tool.reversed ? tool.settings.forward : tool.settings.backward}; + if (arriving != Snap.backward || leaving != Snap.forward || std::abs(tool.settings.lanewidth - Snap.width) > 0.01f) + { + char width[32]; + std::snprintf(width, sizeof(width), "%.2f", Snap.width); + Error = "This arm of the junction takes a road with " + std::to_string(Snap.backward) + " lane(s) leading into it and " + std::to_string(Snap.forward) + " out of it, " + width + " m wide each"; + return {}; + } + } + auto pieces{editor_road::plan(tool.point, tool.hasdirection ? &tool.direction : nullptr, target, Snap.valid() ? &enddirection : nullptr, Global.ctrlState, Error)}; + road_profile(pieces, Snap, Fresh); + return pieces; +} + +// gives the pieces laid out by the planner their heights: over the ground if the road is to follow it, with the slopes +// of the pieces matched where they meet, and with what the road is attached to +void editor_mode::road_profile(std::vector &Pieces, editor_road::loose_end const &Snap, bool const Fresh) +{ + auto const &tool{m_roadtool}; + if (Pieces.empty()) + return; + // an end of a road or an arm of a junction is reached with the slope it has + double endgrade{0.0}; + if (Snap.valid()) + { + auto const run{glm::length(glm::dvec2{Snap.outwards.x, Snap.outwards.z})}; + endgrade = (run > 1e-6 ? -Snap.outwards.y / run : 0.0); + } + auto const *firstgrade{tool.hasgrade ? &tool.grade : nullptr}; + auto const *lastgrade{Snap.valid() ? &endgrade : nullptr}; + if (false == tool.follow) + { + // straight from a point to the next one, the way a ramp or a viaduct goes + std::vector heights; + for (auto const &piece : Pieces) + heights.emplace_back(piece.points[segment_data::point::start].y); + heights.emplace_back(Pieces.back().points[segment_data::point::end].y); + editor_road::profile(Pieces, heights, firstgrade, lastgrade); + return; + } + auto const planned{Pieces}; + bool fresh{Fresh}; + for (auto const step : kProfileSteps) + { + Pieces = editor_road::refine(planned, step); + auto const count{Pieces.size()}; + // the ground is looked up where the pieces meet, and in the middle of each to see how the result fits it + std::vector points; + points.reserve(2 * count + 1); + for (auto const &piece : Pieces) + points.emplace_back(piece.points[segment_data::point::start]); + points.emplace_back(Pieces.back().points[segment_data::point::end]); + for (auto const &piece : Pieces) + points.emplace_back(path_point(piece, 0.5)); + // the planner leads the road between its ends, which are over the ground already + for (auto &point : points) + point.y -= tool.offset; + auto ground{ground_heights(points, fresh)}; + fresh = false; + for (auto &height : ground) + height += tool.offset; + std::vector heights(ground.begin(), ground.begin() + count + 1); + // the ends are where they were put + heights.front() = Pieces.front().points[segment_data::point::start].y; + heights.back() = Pieces.back().points[segment_data::point::end].y; + editor_road::profile(Pieces, heights, firstgrade, lastgrade); + // shorter pieces are called for if the ground shows through the road, or the road hangs over it. + // the pieces attached to something are let off, as they have to get to its height one way or another + bool fits{true}; + for (std::size_t i = 0; i < count; ++i) + { + if ((i == 0 && firstgrade != nullptr) || (i + 1 == count && lastgrade != nullptr)) + continue; + auto const over{path_point(Pieces[i], 0.5).y - ground[count + 1 + i]}; + if (over < -kSunkTolerance || over > kRaisedTolerance) + { + fits = false; + break; + } + } + if (fits) + break; + } +} + +// lays out the junction a click would make. returns: false if there's none to make, with the reason in Error unless +// it's a matter of the first click being still ahead +bool editor_mode::junction_preview(junction_node::state &State, editor_road::loose_end &Snap, std::string &Error) const +{ + auto const &tool{m_roadtool}; + auto const kind{static_cast(std::clamp(tool.kind, 0, 3))}; + Snap = {}; + junction_node::state state; + // the look goes after the road the junction is made for + auto const dress = [&state](road_node::state const &Layout) { + state.surface = Layout.surface; + state.texturelength = Layout.texturelength; + state.markings = Layout.markings; + state.friction = Layout.friction; + state.sounddistance = Layout.sounddistance; + state.quality = Layout.quality; + state.environment = Layout.environment; + }; + if (false == tool.chain) + { + // at a loose end of a road the junction is made for that road, and put right past its end + Snap = editor_road::find_end(tool.mouse, kSnapRadius, false); + if (Snap.road == nullptr) + return false; + auto const &layout{Snap.road->definition()}; + glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z}; + if (glm::length(outwards) < 1e-6 || layout.lanes.empty()) + { + Error = "There's no telling which way this road goes at its end"; + return false; + } + // the ways through the junction start and end where the lanes of the road are expected, which takes lanes of the same width + auto const width{layout.lanes.front().width}; + for (auto const &lane : layout.lanes) + { + if (std::abs(lane.width - width) > 0.01f) + { + Error = "Lanes of this road differ in width, and a junction takes roads with lanes of the same width"; + return false; + } + } + state = editor_road::junction(kind, Snap.position, glm::normalize(outwards), Snap.atend ? layout.forward : layout.backward, Snap.atend ? layout.backward : layout.forward, width); + dress(layout); + } + else + { + // in the open the centre is set already, and the cursor shows where the first road comes from + glm::dvec2 const away{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + if (glm::length(away) < 1.0) + { + Error = "Move the cursor away from the centre to turn the junction"; + return false; + } + auto const heading{-glm::normalize(away)}; + auto const &layout{tool.settings}; + auto const reach{editor_road::junction_reach(layout.forward + layout.backward, layout.lanewidth)}; + glm::dvec3 const entry{tool.point.x - heading.x * reach, tool.point.y, tool.point.z - heading.y * reach}; + state = editor_road::junction(kind, entry, heading, layout.forward, layout.backward, layout.lanewidth); + dress(layout); + } + state.velocity = tool.crossingspeed; + state.normalize(); + State = state; + return true; +} + +void editor_mode::junction_click() +{ + auto &tool{m_roadtool}; + junction_node::state state; + editor_road::loose_end snap; + std::string error; + if (false == junction_preview(state, snap, error)) + { + if (false == error.empty()) + { + tool.status = "Can't make the junction: " + error; + } + else if (false == tool.chain) + { + // away from the roads the first click sets the centre, the second one turns the junction + tool.point = tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}; + tool.chain = true; + tool.status = "Centre set"; + } + return; + } + std::string reason; + if (snap.valid() && false == editor_road::can_join(snap, &reason)) + { + tool.status = "Can't make the junction here: " + reason; + return; + } + auto const created{editor_road::create(std::vector{state})}; + if (created.empty()) + { + tool.status = "The junction wasn't created"; + return; + } + push_road_snapshot({}, {}, {}, created, {}); + tool.chain = false; + tool.status = "\"" + created.front()->name() + "\" made" + (snap.road != nullptr ? " at the end of \"" + snap.road->name() + "\"" : std::string{}) + ". Lead roads out of its arms with the Build tool"; } void editor_mode::road_click() @@ -197,9 +432,11 @@ void editor_mode::road_click() if (tool.tool == 0) { auto *road{editor_road::nearest(ground, 1.5)}; - if (road == nullptr) + auto *junction{road == nullptr ? editor_road::nearest_junction(ground) : nullptr}; + if (road == nullptr && junction == nullptr) tool.status = "No road here"; road_select(road); + junction_select(junction); return; } @@ -209,30 +446,58 @@ void editor_mode::road_click() tool.status = reason; return; } + if (tool.tool == 2) + { + junction_click(); + return; + } if (false == tool.chain) { - // first click sets where the road starts. started from a loose end of a road it carries that road on + // first click sets where the road starts. started from a loose end of a road it carries that road on, + // started from an arm of a junction it's the road the junction is made for there auto const end{editor_road::find_end(ground, kSnapRadius)}; glm::dvec2 const outwards{end.outwards.x, end.outwards.z}; - if (end.road != nullptr && glm::length(outwards) > 1e-6) + if (end.valid() && glm::length(outwards) > 1e-6) { + if (false == editor_road::can_join(end, &reason)) + { + tool.status = "Can't build from here: " + reason; + return; + } tool.point = end.position; tool.direction = glm::normalize(outwards); tool.hasdirection = true; - // carried on from its start the road keeps the direction of its axis, so the lanes stay what they were - tool.reversed = (false == end.atend); - auto const layout{end.road->definition()}; - tool.selected = nullptr; - tool.settings = layout; + tool.grade = end.outwards.y / glm::length(outwards); + tool.hasgrade = true; + tool.junction = nullptr; + if (end.road != nullptr) + { + // carried on from its start the road keeps the direction of its axis, so the lanes stay what they were + tool.reversed = (false == end.atend); + tool.settings = end.road->definition(); + tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; + } + else + { + tool.reversed = false; + tool.settings.forward = end.forward; + tool.settings.backward = end.backward; + tool.settings.lanewidth = end.width; + tool.settings.lanes.clear(); + tool.settings.changes.clear(); + tool.settings.normalize(); + tool.status = "Leading a road out of \"" + end.junction->name() + "\", with the lanes the junction has there"; + } road_select(nullptr); - tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; } else { - tool.point = ground; + tool.point = ground + glm::dvec3{0.0, tool.offset, 0.0}; tool.hasdirection = false; + tool.hasgrade = false; tool.reversed = false; tool.selected = nullptr; + tool.junction = nullptr; tool.status = "Start set"; } tool.chain = true; @@ -241,16 +506,22 @@ void editor_mode::road_click() editor_road::loose_end snap; std::string error; - auto pieces{road_preview(snap, error)}; + auto pieces{road_preview(snap, error, true)}; if (pieces.empty()) { tool.status = "Can't build this: " + error; return; } + if (snap.valid() && false == editor_road::can_join(snap, &reason)) + { + tool.status = "Can't join here: " + reason; + return; + } // where the next piece is going to carry on from auto const &last{pieces.back()}; auto const endpoint{last.points[segment_data::point::end]}; auto const tangent{last.points[segment_data::point::control2] != glm::dvec3{0.0} ? -last.points[segment_data::point::control2] : last.points[segment_data::point::end] - last.points[segment_data::point::start]}; + auto const grade{editor_road::end_grade(last)}; if (tool.reversed) { for (auto &piece : pieces) @@ -274,10 +545,10 @@ void editor_mode::road_click() return; } push_road_snapshot({}, created, {}); - if (snap.road != nullptr) + if (snap.valid()) { tool.chain = false; - tool.status = "Joined with \"" + snap.road->name() + "\""; + tool.status = "Joined with \"" + (snap.road != nullptr ? snap.road->name() : snap.junction->name()) + "\""; return; } glm::dvec2 const planar{tangent.x, tangent.z}; @@ -287,6 +558,8 @@ void editor_mode::road_click() tool.direction = glm::normalize(planar); tool.hasdirection = true; } + tool.grade = grade; + tool.hasgrade = true; tool.status = std::to_string(created.size()) + (created.size() == 1 ? " piece added" : " pieces added"); } @@ -329,16 +602,30 @@ void editor_mode::road_apply() bool editor_mode::road_delete() { auto &tool{m_roadtool}; - if (false == tool.window || tool.selected == nullptr) + if (false == tool.window || (tool.selected == nullptr && tool.junction == nullptr)) return false; - auto *road{tool.selected}; std::string reason; + if (tool.junction != nullptr) + { + auto *junction{tool.junction}; + if (false == editor_road::can_edit(*junction, &reason)) + { + tool.status = "Can't delete: " + reason; + return true; + } + editor_road::remove(std::vector{junction}); + push_road_snapshot({}, {}, {}, {}, {junction}); + tool.junction = nullptr; + tool.status = "\"" + junction->name() + "\" deleted, Ctrl+Z brings it back"; + return true; + } + auto *road{tool.selected}; if (false == editor_road::can_edit(*road, &reason)) { tool.status = "Can't delete: " + reason; return true; } - editor_road::remove({road}); + editor_road::remove(std::vector{road}); push_road_snapshot({}, {}, {road}); tool.selected = nullptr; tool.status = "\"" + road->name() + "\" deleted, Ctrl+Z brings it back"; @@ -370,29 +657,43 @@ bool editor_mode::road_split() return true; } -void editor_mode::push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed) +void editor_mode::push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed, std::vector Junctionscreated, + std::vector Junctionsremoved) { - if (States.empty() && Created.empty() && Removed.empty()) + if (States.empty() && Created.empty() && Removed.empty() && Junctionscreated.empty() && Junctionsremoved.empty()) return; if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); - auto const *road = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front(); + // the entry is listed under the name of the first thing it's about + scene::basic_node const *node{nullptr}; + if (false == States.empty()) + node = States.front().first; + else if (false == Created.empty()) + node = Created.front(); + else if (false == Removed.empty()) + node = Removed.front(); + else if (false == Junctionscreated.empty()) + node = Junctionscreated.front(); + else + node = Junctionsremoved.front(); EditorSnapshot snap; snap.action = EditorSnapshot::Action::RoadEdit; - snap.node_name = road->name(); - snap.position = road->location(); + snap.node_name = node->name(); + snap.position = node->location(); snap.roads = std::move(States); snap.roads_created = std::move(Created); snap.roads_removed = std::move(Removed); + snap.junctions_created = std::move(Junctionscreated); + snap.junctions_removed = std::move(Junctionsremoved); m_history.push_back(std::move(snap)); g_redo.clear(); } void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo) { - if (Snapshot.roads.empty() && Snapshot.roads_created.empty() && Snapshot.roads_removed.empty()) + if (Snapshot.roads.empty() && Snapshot.roads_created.empty() && Snapshot.roads_removed.empty() && Snapshot.junctions_created.empty() && Snapshot.junctions_removed.empty()) return; // what the roads are like now is what the opposite operation brings back @@ -405,11 +706,15 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec if (Undo) { editor_road::remove(Snapshot.roads_created); + editor_road::remove(Snapshot.junctions_created); + editor_road::revive(Snapshot.junctions_removed); editor_road::revive(Snapshot.roads_removed); } else { editor_road::revive(Snapshot.roads_created); + editor_road::revive(Snapshot.junctions_created); + editor_road::remove(Snapshot.junctions_removed); editor_road::remove(Snapshot.roads_removed); } editor_road::apply(Snapshot.roads); @@ -418,6 +723,8 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec tool.chain = false; if (tool.selected != nullptr) road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); + if (tool.junction != nullptr && tool.junction->m_editorremoved) + tool.junction = nullptr; } // draws controls for the layout of a road. returns: true if the layout was changed in a way which calls for the road to be made anew @@ -642,28 +949,71 @@ void editor_mode::render_road_window() ImGui::RadioButton("Select", &tool.tool, 0); ImGui::SameLine(); ImGui::RadioButton("Build", &tool.tool, 1); + ImGui::SameLine(); + ImGui::RadioButton("Junction", &tool.tool, 2); if (tool.tool != previoustool) { tool.chain = false; - if (tool.tool == 1) + if (tool.tool != 0) + { tool.selected = nullptr; + tool.junction = nullptr; + } } ImGui::SameLine(); ImGui::Checkbox("Show lanes", &tool.lanes); + // how the things being built are put on the ground + auto const placement = [&tool](bool const Road) { + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Height above the ground [m]", &tool.offset, 0.05f, 0.25f, "%.2f")) + tool.offset = std::clamp(tool.offset, -10.0f, 100.0f); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "How far over the ground under the cursor the points are put.\nA few centimetres keep the surface from flickering where it lies on the ground."); + if (false == Road) + return; + ImGui::Checkbox("Follow the ground", &tool.follow); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "On: the road is led over the ground between the clicked points, in pieces as short as the ground calls for,\nwith the profile rounded so the slope doesn't change abruptly.\nOff: the road goes straight from a point to the next one, like a ramp or a viaduct."); + }; if (tool.tool == 1) { ImGui::TextDisabled("LMB: start, then each next point. Ctrl: straight ahead. Esc: finish"); - ImGui::TextDisabled("Start or end on a loose end of a road to join it"); + ImGui::TextDisabled("Start or end on a loose end of a road or an arm of a junction to join it"); if (tool.chain) { if (ImGui::Button("Finish (Esc)")) tool.chain = false; } + placement(true); ImGui::Separator(); // what's set here is what the next pieces get render_road_layout(tool.settings); } + else if (tool.tool == 2) + { + ImGui::TextDisabled("LMB on a loose end of a road: junction for that road, right past its end"); + ImGui::TextDisabled("LMB elsewhere: the centre, then the side the first road comes from. Esc: start over"); + char const *kinds[]{"Crossroads", "T: the road ends, roads to both sides", "T: the road goes on, a road to the left", "T: the road goes on, a road to the right"}; + ImGui::SetNextItemWidth(280.0f); + ImGui::Combo("Kind", &tool.kind, kinds, 4); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Speed limit on the junction [km/h]", &tool.crossingspeed, 5.0f, 10.0f, "%.0f")) + tool.crossingspeed = std::clamp(tool.crossingspeed, 5.0f, 200.0f); + placement(false); + ImGui::Separator(); + ImGui::TextWrapped("Away from the roads the junction is made for roads with the lanes set in the Build tool: %d + %d, %.2f m wide. Its surface and markings go after the road it's made for.", tool.settings.forward, tool.settings.backward, + tool.settings.lanewidth); + ImGui::TextWrapped("Roads are led out of the arms with the Build tool, and get the lanes the junction has there."); + } + else if (tool.junction != nullptr) + { + auto const &junction{*tool.junction}; + ImGui::Text("%s, %d arms, %d ways through", junction.name().c_str(), static_cast(junction.definition().arms.size()), static_cast(junction.movements().size())); + ImGui::TextDisabled("Speed limit %.0f km/h. The ways through are made for the roads attached at the moment", junction.definition().velocity); + if (ImGui::Button("Delete (Del)")) + road_delete(); + } else if (tool.selected != nullptr) { auto const &road{*tool.selected}; @@ -681,7 +1031,7 @@ void editor_mode::render_road_window() } else { - ImGui::TextDisabled("LMB: select a road piece"); + ImGui::TextDisabled("LMB: select a road piece or a junction"); } if (false == tool.status.empty()) { @@ -748,6 +1098,14 @@ void editor_mode::draw_road_overlay() const drawpath(turn->m_paths.front(), kTurnColor, 1.5f, 8); } } + // ways through the junctions + for (auto const *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved || glm::distance(junction->location(), camera) > kLaneRange) + continue; + for (auto const &way : junction->movements()) + drawpath(way, kWayColor, 1.5f, 8); + } } if (false == tool.window) return; @@ -786,6 +1144,15 @@ void editor_mode::draw_road_overlay() const drawlist->AddCircle(screen, Radius, Color, 20, 3.0f); }; + // outline of a junction: the edge of its surface, with the ends of the roads it's made for + auto const drawjunction = [&](junction_node::state const &State, ImU32 const Color) { + auto const outline{State.outline()}; + for (std::size_t i = 0; i < outline.size(); ++i) + projection.line(drawlist, outline[i], outline[(i + 1) % outline.size()], Color, 2.0f); + for (auto const &arm : State.arms) + drawpoint(arm.position, Color, 5.0f, true); + }; + if (tool.selected != nullptr && false == tool.selected->m_editorremoved) { auto const &state{tool.selected->definition()}; @@ -793,42 +1160,57 @@ void editor_mode::draw_road_overlay() const drawpoint(state.axis.points[segment_data::point::start], kSelectedColor, 6.0f, true); drawpoint(state.axis.points[segment_data::point::end], kSelectedColor, 6.0f, true); } + if (tool.junction != nullptr && false == tool.junction->m_editorremoved) + drawjunction(tool.junction->definition(), kSelectedColor); std::string hint; if (tool.tool == 1) { if (tool.chain) { - editor_road::loose_end snap; - std::string error; - auto const pieces{road_preview(snap, error)}; drawpoint(tool.point, kPreviewColor, 6.0f, true); - if (pieces.empty()) + if (tool.preview.empty()) { projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f); - hint = error + " "; + hint = tool.previewerror + " "; } - for (auto const &piece : pieces) + for (auto const &piece : tool.preview) { auto state{tool.settings}; state.axis = piece; drawroad(state, kPreviewColor); } - if (snap.road != nullptr) - drawpoint(snap.position, kSnapColor, 14.0f, false); + if (tool.previewsnap.valid()) + drawpoint(tool.previewsnap.position, kSnapColor, 14.0f, false); hint += "LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece"; } else { auto const end{editor_road::find_end(tool.mouse, kSnapRadius)}; - if (end.road != nullptr) + if (end.valid()) drawpoint(end.position, kSnapColor, 14.0f, false); - hint = "LMB: set where the road starts; on a loose end of a road (ring) to carry it on"; + hint = "LMB: set where the road starts; on a loose end of a road or an arm of a junction (ring) to carry on from there"; } } + else if (tool.tool == 2) + { + if (tool.chain) + drawpoint(tool.point, kPreviewColor, 6.0f, true); + if (tool.hasjunction) + drawjunction(tool.previewjunction, kPreviewColor); + else if (false == tool.previewerror.empty()) + hint = tool.previewerror + " "; + if (tool.previewsnap.valid()) + drawpoint(tool.previewsnap.position, kSnapColor, 14.0f, false); + hint += (tool.chain ? "LMB: make the junction, with the first road coming from the side of the cursor Esc: start over" : + tool.previewsnap.valid() ? "LMB: make the junction at the end of this road" : + "LMB: on a loose end of a road (ring) to make its junction there; elsewhere to set the centre of one"); + } else { - hint = tool.selected != nullptr ? "LMB: select a road piece K: split at the cursor Del: delete Ctrl+Z: undo" : "LMB: select a road piece"; + hint = tool.selected != nullptr ? "LMB: select a road piece or a junction K: split at the cursor Del: delete Ctrl+Z: undo" : + tool.junction != nullptr ? "LMB: select a road piece or a junction Del: delete Ctrl+Z: undo" : + "LMB: select a road piece or a junction"; } // shown above the place the track tools put their hints at ImGuiIO const &io = ImGui::GetIO(); diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index bb2039b2..4cbd4051 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -63,6 +63,72 @@ std::vector roads_at(glm::dvec3 const &Point, road_node const *Excl return result; } +// junctions with an arm at specified point +std::vector junctions_at(glm::dvec3 const &Point) +{ + std::vector result; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved) + continue; + for (auto const &arm : junction->definition().arms) + { + if (glm::distance(arm.position, Point) < kSamePoint) + { + result.emplace_back(junction); + break; + } + } + } + return result; +} + +void add_unique(std::vector &Junctions, junction_node *Junction) +{ + if (Junction != nullptr && std::find(Junctions.begin(), Junctions.end(), Junction) == Junctions.end()) + Junctions.emplace_back(Junction); +} + +// part of a path between two values of its curve parameter +segment_data subpath(segment_data const &Path, double const From, double const To) +{ + segment_data part{Path}; + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + part.rolls = {static_cast(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * From), static_cast(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * To)}; + if (Path.points[segment_data::point::control1] == glm::dvec3{0.0} && Path.points[segment_data::point::control2] == glm::dvec3{0.0}) + { + part.points[segment_data::point::start] = glm::mix(p0, p3, From); + part.points[segment_data::point::end] = glm::mix(p0, p3, To); + return part; + } + auto const p1{p0 + Path.points[segment_data::point::control1]}; + auto const p2{p3 + Path.points[segment_data::point::control2]}; + auto const point = [&](double const T) { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + }; + auto const derivative = [&](double const T) { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + }; + // a part of a cubic curve is a cubic curve, with the tangents scaled to the part of the parameter range it takes + part.points[segment_data::point::start] = point(From); + part.points[segment_data::point::end] = point(To); + part.points[segment_data::point::control1] = derivative(From) * ((To - From) / 3.0); + part.points[segment_data::point::control2] = derivative(To) * (-(To - From) / 3.0); + return part; +} + +// true if there's a vehicle on any of the paths turning the traffic back at the loose ends of the road +bool turning(road_node const &Road) +{ + for (auto const *turn : Road.turns()) + if (turn != nullptr && false == turn->Dynamics.empty()) + return true; + return false; +} + void retire(std::vector const &Tracks) { for (auto *track : Tracks) @@ -138,6 +204,66 @@ bool editor_road::can_edit(road_node const &Road, std::string *Reason) if (turn != nullptr && false == turn->Dynamics.empty()) reason = "there are vehicles turning back next to it"; } + if (reason.empty()) + { + // so do the ways through the junctions it leads to + for (auto const atend : {false, true}) + for (auto const *junction : junctions_at(axis_end(Road.definition(), atend))) + if (junction->occupied()) + reason = "there are vehicles on the junction it leads to"; + } + if (false == reason.empty() && Reason != nullptr) + *Reason = reason; + return reason.empty(); +} + +bool editor_road::can_edit(junction_node const &Junction, std::string *Reason) +{ + std::string reason; + if (false == available(&reason)) + { + // reason was filled by the call + } + else if (Junction.merged()) + { + reason = "the scenery wasn't opened for editing (-edit), geometry of the junctions it came with can't be taken apart"; + } + else if (false == scene::Layers.editable(&Junction, &reason)) + { + // reason was filled by the call + } + else if (Junction.occupied()) + { + reason = "there are vehicles on it"; + } + else + { + // the roads attached to it get their loose ends closed anew + for (auto const &arm : Junction.definition().arms) + for (auto const *road : roads_at(arm.position, nullptr)) + if (turning(*road)) + reason = "there are vehicles turning back on the roads it leads to"; + } + if (false == reason.empty() && Reason != nullptr) + *Reason = reason; + return reason.empty(); +} + +bool editor_road::can_join(loose_end const &End, std::string *Reason) +{ + // what gets attached takes the place of the paths made for the end as it was + std::string reason; + if (End.road != nullptr && turning(*End.road)) + reason = "there are vehicles turning back on the road"; + if (End.junction != nullptr) + { + if (End.junction->occupied()) + reason = "there are vehicles on the junction"; + for (auto const &arm : End.junction->definition().arms) + for (auto const *road : roads_at(arm.position, nullptr)) + if (turning(*road)) + reason = "there are vehicles turning back on the roads the junction leads to"; + } if (false == reason.empty() && Reason != nullptr) *Reason = reason; return reason.empty(); @@ -155,38 +281,64 @@ std::vector editor_road::neighbours(road_node const &Road) } template -void editor_road::refresh(std::vector const &Roads, Change_ Change) +void editor_road::refresh(std::vector const &Roads, std::vector const &Junctions, Change_ Change) { - // the pieces next to the changed ones keep their shape, but what they're joined with changes + // the pieces and junctions next to the changed ones keep their shape, but what they're joined with changes. + // this takes down the paths made for the joints, so they can be made again once the change is done std::vector affected; - for (auto *road : Roads) - { - add_unique(affected, road); - for (auto *neighbour : neighbours(*road)) - add_unique(affected, neighbour); - } - for (auto *road : affected) - retire(road->release_turns()); + std::vector crossings; + auto const gather = [&]() { + std::vector roads{affected}; + std::vector junctions{crossings}; + for (auto *road : Roads) + { + add_unique(roads, road); + if (road->m_editorremoved) + continue; + for (auto *neighbour : neighbours(*road)) + add_unique(roads, neighbour); + for (auto const atend : {false, true}) + for (auto *junction : junctions_at(axis_end(road->definition(), atend))) + add_unique(junctions, junction); + } + for (auto *junction : Junctions) + add_unique(junctions, junction); + // a junction changes what the roads attached to it do at their ends + for (auto *junction : junctions) + if (false == junction->m_editorremoved) + for (auto const &arm : junction->definition().arms) + for (auto *road : roads_at(arm.position, nullptr)) + add_unique(roads, road); + for (auto *road : roads) + { + if (std::find(affected.begin(), affected.end(), road) == affected.end()) + { + retire(road->release_turns()); + affected.emplace_back(road); + } + } + for (auto *junction : junctions) + { + if (std::find(crossings.begin(), crossings.end(), junction) == crossings.end()) + { + retire(junction->release_links()); + crossings.emplace_back(junction); + } + } + }; + gather(); for (auto *road : Roads) { road->hide(); retire(road->release_lanes()); } + for (auto *junction : Junctions) + junction->hide(); Change(); - // the change could have moved the pieces next to other ones - for (auto *road : Roads) - { - for (auto *neighbour : neighbours(*road)) - { - if (std::find(affected.begin(), affected.end(), neighbour) == affected.end()) - { - retire(neighbour->release_turns()); - affected.emplace_back(neighbour); - } - } - } + // the change could have moved things next to other ones + gather(); std::vector lanes; for (auto *road : Roads) { @@ -198,6 +350,13 @@ void editor_road::refresh(std::vector const &Roads, Change_ Change) } // joins the lanes with the lanes of the neighbours, by the ends they share editor_track::commit(lanes); + for (auto *junction : Junctions) + if (false == junction->m_editorremoved) + junction->show(); + // the junctions take the lanes which end at them, what's left loose after that is closed by the roads + for (auto *junction : crossings) + if (false == junction->m_editorremoved) + junction->create_links(); for (auto *road : affected) if (false == road->m_editorremoved) road->close_ends(); @@ -227,7 +386,7 @@ std::vector editor_road::create(std::vector const scene::Layers.count(road->layer(), scene::layer_item::track); created.emplace_back(road); } - refresh(created, []() {}); + refresh(created, {}, []() {}); return created; } @@ -238,7 +397,7 @@ void editor_road::apply(std::vector> co add_unique(roads, change.first); if (roads.empty()) return; - refresh(roads, [&]() { + refresh(roads, {}, [&]() { for (auto const &change : Changes) { change.first->define(change.second); @@ -255,7 +414,7 @@ void editor_road::remove(std::vector const &Roads) add_unique(roads, road); if (roads.empty()) return; - refresh(roads, [&]() { + refresh(roads, {}, [&]() { for (auto *road : roads) { road->m_editorremoved = true; @@ -272,7 +431,7 @@ void editor_road::revive(std::vector const &Roads) add_unique(roads, road); if (roads.empty()) return; - refresh(roads, [&]() { + refresh(roads, {}, [&]() { for (auto *road : roads) { road->m_editorremoved = false; @@ -282,6 +441,68 @@ void editor_road::revive(std::vector const &Roads) }); } +std::vector editor_road::create(std::vector const &States) +{ + static int counter{0}; + std::vector created; + if (false == available()) + return created; + for (auto const &state : States) + { + scene::node_data data; + data.type = "junction"; + data.range_max = -1.0; + data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing + do + { + data.name = "junction_" + std::to_string(++counter); + } while (simulation::Junctions.find(data.name) != nullptr); + auto *junction{new junction_node(data)}; + junction->define(state); + junction->mark_dirty(); + simulation::Junctions.insert(junction); + scene::Layers.count(junction->layer(), scene::layer_item::track); + created.emplace_back(junction); + } + refresh({}, created, []() {}); + return created; +} + +void editor_road::remove(std::vector const &Junctions) +{ + std::vector junctions; + for (auto *junction : Junctions) + if (junction != nullptr && false == junction->m_editorremoved) + add_unique(junctions, junction); + if (junctions.empty()) + return; + refresh({}, junctions, [&]() { + for (auto *junction : junctions) + { + junction->m_editorremoved = true; + scene::Layers.count(junction->layer(), scene::layer_item::track, -1); + } + }); +} + +void editor_road::revive(std::vector const &Junctions) +{ + std::vector junctions; + for (auto *junction : Junctions) + if (junction != nullptr && junction->m_editorremoved) + add_unique(junctions, junction); + if (junctions.empty()) + return; + refresh({}, junctions, [&]() { + for (auto *junction : junctions) + { + junction->m_editorremoved = false; + junction->mark_dirty(); + scene::Layers.count(junction->layer(), scene::layer_item::track); + } + }); +} + road_node *editor_road::split(road_node &Road, double const T) { if (T <= 0.001 || T >= 0.999) @@ -386,7 +607,29 @@ road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin) return result; } -editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double const Radius) +junction_node *editor_road::nearest_junction(glm::dvec3 const &Point) +{ + junction_node *result{nullptr}; + double best{std::numeric_limits::max()}; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved) + continue; + auto const &state{junction->definition()}; + // the junction takes the ground up to the ends of the roads it's made for + double reach{0.0}; + for (auto const &arm : state.arms) + reach = std::max(reach, glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{state.centre.x, state.centre.z})); + auto const distance{glm::distance(glm::dvec2{state.centre.x, state.centre.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > reach || std::abs(state.centre.y - Point.y) > 3.0 || distance > best) + continue; + best = distance; + result = junction; + } + return result; +} + +editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double const Radius, bool const Junctions) { loose_end result; double best{Radius}; @@ -399,15 +642,38 @@ editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double con { auto const position{axis_end(state, atend)}; auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; - if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty()) + if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty() || false == junctions_at(position).empty()) continue; best = distance; + result = loose_end{}; result.road = road; result.atend = atend; result.position = position; result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0)); } } + if (false == Junctions) + return result; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved) + continue; + for (auto const &arm : junction->definition().arms) + { + auto const distance{glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > best || std::abs(arm.position.y - Point.y) > 3.0 || false == roads_at(arm.position, nullptr).empty()) + continue; + best = distance; + result = loose_end{}; + result.junction = junction; + result.position = arm.position; + result.outwards = glm::dvec3{arm.direction.x, 0.0, arm.direction.y}; + // a road leading away from the junction has the lanes going out of it as the ones going along its axis + result.forward = arm.outgoing; + result.backward = arm.incoming; + result.width = arm.width; + } + } return result; } @@ -535,6 +801,147 @@ std::vector editor_road::plan(glm::dvec3 const &Start, glm::dvec2 return pieces; } +double editor_road::planar_length(segment_data const &Path) +{ + auto const whole{subpath(Path, 0.0, 1.0)}; + auto const &p0{whole.points[segment_data::point::start]}; + auto const &p3{whole.points[segment_data::point::end]}; + if (whole.points[segment_data::point::control1] == glm::dvec3{0.0} && whole.points[segment_data::point::control2] == glm::dvec3{0.0}) + return glm::distance(glm::dvec2{p0.x, p0.z}, glm::dvec2{p3.x, p3.z}); + auto const p1{p0 + whole.points[segment_data::point::control1]}; + auto const p2{p3 + whole.points[segment_data::point::control2]}; + double length{0.0}; + glm::dvec2 previous{p0.x, p0.z}; + for (int i = 1; i <= 32; ++i) + { + auto const t{i / 32.0}; + auto const u{1.0 - t}; + auto const point{u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3}; + glm::dvec2 const current{point.x, point.z}; + length += glm::distance(previous, current); + previous = current; + } + return length; +} + +double editor_road::end_grade(segment_data const &Path) +{ + auto const &control{Path.points[segment_data::point::control2]}; + if (control != glm::dvec3{0.0}) + { + auto const run{glm::length(glm::dvec2{control.x, control.z})}; + return run > 1e-6 ? -control.y / run : 0.0; + } + auto const length{planar_length(Path)}; + return length > 1e-6 ? (Path.points[segment_data::point::end].y - Path.points[segment_data::point::start].y) / length : 0.0; +} + +std::vector editor_road::refine(std::vector const &Pieces, double const Length) +{ + std::vector result; + for (auto const &piece : Pieces) + { + auto const count{std::max(1, static_cast(std::ceil(planar_length(piece) / std::max(1.0, Length) - 0.01)))}; + for (int i = 0; i < count; ++i) + result.emplace_back(count == 1 ? piece : subpath(piece, static_cast(i) / count, static_cast(i + 1) / count)); + } + return result; +} + +void editor_road::profile(std::vector &Pieces, std::vector Heights, double const *Startgrade, double const *Endgrade) +{ + auto const count{Pieces.size()}; + if (count == 0 || Heights.size() != count + 1) + return; + std::vector lengths(count); + for (std::size_t i = 0; i < count; ++i) + lengths[i] = std::max(0.01, planar_length(Pieces[i])); + // the heights in between are evened out, so the road doesn't ride every dip. it's never lowered though, + // as that would let the ground the heights were taken from come through the surface + { + auto const sampled{Heights}; + for (std::size_t i = 1; i < count; ++i) + Heights[i] = std::max(sampled[i], 0.25 * sampled[i - 1] + 0.5 * sampled[i] + 0.25 * sampled[i + 1]); + } + std::vector slopes(count); + for (std::size_t i = 0; i < count; ++i) + slopes[i] = (Heights[i + 1] - Heights[i]) / lengths[i]; + // slope at each joint is shared by the pieces on both its sides, which is what rounds the profile + std::vector grades(count + 1); + grades.front() = (Startgrade != nullptr ? *Startgrade : slopes.front()); + grades.back() = (Endgrade != nullptr ? *Endgrade : slopes.back()); + for (std::size_t i = 1; i < count; ++i) + { + auto const before{slopes[i - 1]}; + auto const after{slopes[i]}; + if (before * after <= 0.0) + { + // a crest or a bottom + grades[i] = 0.0; + continue; + } + auto grade{(before * lengths[i] + after * lengths[i - 1]) / (lengths[i - 1] + lengths[i])}; + // kept from overshooting the heights the road is led through + auto const limit{3.0 * std::min(std::abs(before), std::abs(after))}; + grades[i] = std::clamp(grade, -limit, limit); + } + for (std::size_t i = 0; i < count; ++i) + { + auto &piece{Pieces[i]}; + auto &start{piece.points[segment_data::point::start]}; + auto &end{piece.points[segment_data::point::end]}; + auto &control1{piece.points[segment_data::point::control1]}; + auto &control2{piece.points[segment_data::point::control2]}; + start.y = Heights[i]; + end.y = Heights[i + 1]; + if (control1 == glm::dvec3{0.0} && control2 == glm::dvec3{0.0}) + { + if (std::abs(grades[i] - slopes[i]) < 1e-4 && std::abs(grades[i + 1] - slopes[i]) < 1e-4) + continue; // even slope, the piece stays a plain straight + // a straight needs its control points spelled out to bend up or down + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + control1.y = grades[i] * glm::length(glm::dvec2{control1.x, control1.z}); + control2.y = -grades[i + 1] * glm::length(glm::dvec2{control2.x, control2.z}); + } +} + +double editor_road::junction_reach(int const Lanes, float const Width) +{ + // half of the road, and room for the corners + return 0.5 * std::max(1, Lanes) * Width + 6.0; +} + +junction_node::state editor_road::junction(junction_kind const Kind, glm::dvec3 const &Entry, glm::dvec2 const &Heading, int const Incoming, int const Outgoing, float const Width) +{ + junction_node::state state; + auto const heading{glm::length(Heading) > 1e-6 ? glm::normalize(Heading) : glm::dvec2{0.0, 1.0}}; + glm::dvec2 const left{heading.y, -heading.x}; + auto const reach{junction_reach(Incoming + Outgoing, Width)}; + auto const spot = [&](glm::dvec2 const &Direction, double const Distance) { return glm::dvec3{Entry.x + Direction.x * Distance, Entry.y, Entry.z + Direction.y * Distance}; }; + state.centre = spot(heading, reach); + auto const arm = [&](glm::dvec2 const &Direction, int const In, int const Out) { + junction_node::arm_data data; + data.position = glm::dvec3{state.centre.x + Direction.x * reach, Entry.y, state.centre.z + Direction.y * reach}; + data.direction = Direction; + data.incoming = In; + data.outgoing = Out; + data.width = Width; + state.arms.emplace_back(data); + }; + // the road the junction is entered by, then the others: what comes in by one leaves by them + arm(-heading, Incoming, Outgoing); + if (Kind == junction_kind::cross || Kind == junction_kind::tee_left || Kind == junction_kind::tee_right) + arm(heading, Outgoing, Incoming); + if (Kind == junction_kind::cross || Kind == junction_kind::tee_sides || Kind == junction_kind::tee_left) + arm(left, Outgoing, Incoming); + if (Kind == junction_kind::cross || Kind == junction_kind::tee_sides || Kind == junction_kind::tee_right) + arm(-left, Outgoing, Incoming); + state.normalize(); + return state; +} + std::vector const &editor_road::materials() { static std::vector names; diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp index 40f21cb4..e7f7fff3 100644 --- a/editor/editorRoad.hpp +++ b/editor/editorRoad.hpp @@ -17,22 +17,42 @@ http://mozilla.org/MPL/2.0/. #include "world/Road.h" // editor operations on the roads. a road in the scenery is a string of pieces joined end to end, -// each piece being a road node with lanes and geometry of its own +// each piece being a road node with lanes and geometry of its own; roads meet at junctions class editor_road { public: - // an end of a road piece nothing is attached to + // an end of a road piece, or an arm of a junction, nothing is attached to struct loose_end { road_node *road{nullptr}; - bool atend{false}; + junction_node *junction{nullptr}; + bool atend{false}; // of a road: the end of its axis rather than the start glm::dvec3 position{0.0}; - glm::dvec3 outwards{0.0}; // direction the road would continue in + glm::dvec3 outwards{0.0}; // direction a road would continue in + // of a junction arm: lanes of a road leading away from it + int forward{0}; + int backward{0}; + float width{0.f}; + + bool valid() const + { + return road != nullptr || junction != nullptr; + } + }; + enum class junction_kind + { + cross, // four roads + tee_sides, // the road ends, with roads to both sides + tee_left, // the road goes on, with a road to the left + tee_right // the road goes on, with a road to the right }; // true if the roads can be built and changed at all. optionally explains why they can't static bool available(std::string *Reason = nullptr); - // true if specified road piece can be changed or removed. optionally explains why it can't + // true if specified road piece or junction can be changed or removed. optionally explains why it can't static bool can_edit(road_node const &Road, std::string *Reason = nullptr); + static bool can_edit(junction_node const &Junction, std::string *Reason = nullptr); + // true if a road or a junction can be attached to specified loose end. optionally explains why it can't + static bool can_join(loose_end const &End, std::string *Reason = nullptr); // makes road pieces out of provided definitions, with their lanes joined to the neighbours and geometry in the scene static std::vector create(std::vector const &States); // replaces definitions of the pieces and makes their lanes and geometry anew @@ -40,26 +60,48 @@ class editor_road // takes the pieces out of the scenery. they're kept around, so they can be brought back static void remove(std::vector const &Roads); static void revive(std::vector const &Roads); + // the same for the junctions + static std::vector create(std::vector const &States); + static void remove(std::vector const &Junctions); + static void revive(std::vector const &Junctions); // cuts the piece in two at specified point of its axis. returns: the new piece, holding the part past the cut static road_node *split(road_node &Road, double const T); // the pieces which make a single road together with specified one, in no particular order static std::vector chain(road_node &Road); // the piece under specified point, or within specified distance from it static road_node *nearest(glm::dvec3 const &Point, double const Margin); + // the junction specified point lies on + static junction_node *nearest_junction(glm::dvec3 const &Point); // value of the curve parameter for the point of the axis nearest to specified point static double nearest_parameter(road_node::state const &State, glm::dvec3 const &Point); - // end of a piece with nothing attached, within specified distance from specified point - static loose_end find_end(glm::dvec3 const &Point, double const Radius); + // end of a road with nothing attached, within specified distance from specified point; optionally arms of junctions too + static loose_end find_end(glm::dvec3 const &Point, double const Radius, bool const Junctions = true); // lays out axes of the pieces for a road leading from a point to another. the directions are optional requirements // for the road at its ends; without them it starts straight at the target, with them it bends to meet them. // returns: the axes in order, or nothing if there's no reasonable road to make, with the reason in Error static std::vector plan(glm::dvec3 const &Start, glm::dvec2 const *Direction, glm::dvec3 const &Target, glm::dvec2 const *Enddirection, bool const Straight, std::string &Error); + // cuts the pieces so none is longer than specified + static std::vector refine(std::vector const &Pieces, double const Length); + // gives a string of pieces a smooth profile running through provided heights. + // Heights: one for the start of each piece and one for the end of the last. these of the ends are kept, the others are evened out: + // the dips are filled while the tops stay, so the ground the heights were taken from doesn't come through the road. + // Startgrade, Endgrade: slopes to keep at the ends, to carry on what the road joins; nullptr for none + static void profile(std::vector &Pieces, std::vector Heights, double const *Startgrade, double const *Endgrade); + // length of the path seen from above + static double planar_length(segment_data const &Path); + // slope of the path at its end, in the direction it's laid out + static double end_grade(segment_data const &Path); + // distance from the centre of a junction to the ends of the roads it's made for + static double junction_reach(int const Lanes, float const Width); + // lays out a junction entered by a road. Entry: middle of the end of that road, Heading: the way the road goes there, + // Incoming, Outgoing: lanes of that road leading into the junction and out of it + static junction_node::state junction(junction_kind const Kind, glm::dvec3 const &Entry, glm::dvec2 const &Heading, int const Incoming, int const Outgoing, float const Width); // names of materials and images present in the texture folder, to pick from static std::vector const &materials(); private: - // takes down what depends on shape of the listed pieces, lets the caller change them, then builds it all again + // takes down what depends on the listed pieces and junctions, lets the caller change them, then builds it all again template - static void refresh(std::vector const &Roads, Change_ Change); + static void refresh(std::vector const &Roads, std::vector const &Junctions, Change_ Change); static std::vector neighbours(road_node const &Road); }; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 71bc26b3..9fe5baf4 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -857,6 +857,72 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[source.layer].edits.emplace_back(std::move(edit)); rewritten.emplace(road); } + // road junctions, likewise + std::vector savedjunctions; + std::vector droppedjunctions; + auto const junction_text = [](junction_node &Junction, glm::dvec3 const &Offset) { + auto const definition{Junction.definition()}; + auto local{definition}; + local.centre -= Offset; + for (auto &arm : local.arms) + { + arm.position -= Offset; + } + Junction.define(local); + std::string text; + Junction.export_as_text(text); + Junction.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->from_template()) + { + continue; + } + auto const nodelayer{resolve(junction->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(junction)}; + if (lookup == m_sources.end()) + { + if (junction->m_editorremoved || false == junction->dirty()) + { + continue; + } + created[nodelayer].emplace_back(junction, junction_text(*junction, layer(nodelayer).context_insert().offset)); + savedjunctions.push_back(junction); + continue; + } + auto const &source{lookup->second}; + if ((false == junction->m_editorremoved && false == junction->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (junction->m_editorremoved) + { + droppedjunctions.push_back(junction); + } + else + { + edit.text = junction_text(*junction, source.context.offset); + edit.length = edit.text.size(); + edit.node = junction; + savedjunctions.push_back(junction); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(junction); + } for (auto const &erased : m_erased) { file_patch::edit edit; @@ -1308,6 +1374,14 @@ save_result node_layers::save(std::vector const &Rootstatements) { m_sources.erase(road); } + for (auto *junction : savedjunctions) + { + junction->m_dirty = false; + } + for (auto const *junction : droppedjunctions) + { + m_sources.erase(junction); + } if (removedterrain) { // binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index e8e4542f..b24ffb70 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -38,6 +38,7 @@ event_manager Events; memory_table Memory; path_table Paths; road_table Roads; +junction_table Junctions; traction_table Traction; powergridsource_table Powergrid; instance_table Instances; diff --git a/simulation/simulation.h b/simulation/simulation.h index 03448007..67bfae0e 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -83,6 +83,7 @@ extern event_manager Events; extern memory_table Memory; extern path_table Paths; extern road_table Roads; +extern junction_table Junctions; extern traction_table Traction; extern powergridsource_table Powergrid; extern instance_table Instances; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 82843960..e2598fdc 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -181,6 +181,7 @@ state_serializer::deserialize_continue(std::shared_ptr state // geometry of the roads is generated on each load instead of being kept in the binary terrain file, // so it's inserted in the region only after that file had its chance to be written simulation::Roads.create_geometry( Scratchpad ); + simulation::Junctions.create_geometry( Scratchpad ); return false; } @@ -398,6 +399,8 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc } simulation::Paths.InitTracks(); + // the junctions tie the lanes of the roads together, what's left loose after that gets closed by the roads + simulation::Junctions.InitJunctions(); simulation::Roads.InitRoads(); simulation::Traction.InitTraction(); simulation::Events.InitEvents(); @@ -521,6 +524,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch road->create_lanes(); scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } ); } + else if( nodedata.type == "junction" ) { + + auto *junction { new junction_node( nodedata ) }; + junction->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Junctions.insert( junction ) ) { + ErrorLog( "Bad scenario: duplicate junction name \"" + junction->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), junction ); + scene::Layers.track( junction, { sourcebegin, Input.TokenEnd() } ); + } else if( nodedata.type == "traction" ) { auto *traction { deserialize_traction( Input, Scratchpad, nodedata ) }; @@ -695,6 +712,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch { "dynamic", scene::layer_item::vehicle }, { "track", scene::layer_item::track }, { "road", scene::layer_item::track }, + { "junction", scene::layer_item::track }, { "traction", scene::layer_item::traction }, { "tractionpowersource", scene::layer_item::powersource }, { "model", scene::layer_item::model }, @@ -1404,6 +1422,11 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons road->export_as_text( scmfile ); } } + for( auto const *junction : Junctions.sequence() ) { + if( junction != nullptr && false == junction->m_editorremoved && junction->dirty() == Dirty && junction->group() == null_handle ) { + junction->export_as_text( scmfile ); + } + } // traction scmfile << "// traction\n"; for( auto const *traction : Traction.sequence() ) { @@ -1459,7 +1482,7 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st std::pair state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { // statements which take more than a single token, with the tokens ending them static std::unordered_map const nodeends { - { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, + { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "junction", "endjunction" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, { "triangles", "endtri" }, { "triangle_strip", "endtri" }, { "triangle_fan", "endtri" }, { "lines", "endline" }, { "line_strip", "endline" }, { "line_loop", "endline" }, { "memcell", "endmemcell" }, { "eventlauncher", "end" }, { "sound", "endsound" } }; static std::unordered_map const statementends { diff --git a/utilities/Classes.h b/utilities/Classes.h index 7622a57c..74aface6 100644 --- a/utilities/Classes.h +++ b/utilities/Classes.h @@ -45,6 +45,8 @@ struct light_array; class particle_manager; class road_node; // kawałek drogi, z którego powstają pasy ruchu class road_table; +class junction_node; +class junction_table; struct dictionary_source; class trainset_desc; class scenery_desc; diff --git a/world/Road.cpp b/world/Road.cpp index 7553b5eb..a0da0b62 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -907,26 +907,52 @@ std::vector road_node::create_shapes() const return shapes; } -// puts geometry of the road in the scene as shapes of its own, which can be taken back -void road_node::show() +namespace { - if (m_section != nullptr || m_merged || simulation::Region == nullptr || false == simulation::Region->point_inside(location())) + +// banks holding geometry put in the scene by the owned shapes +std::unordered_set ownedbanks; + +double const junction_reach{0.25}; // an end of a road this close to an arm is attached to it +double const junction_fit{0.05}; // a lane has to end this close to where the junction expects it +int const corner_steps{6}; // pieces a rounded corner is made of + +// direction on the ground as a vector in space +glm::dvec3 flat(glm::dvec2 const &Direction) +{ + return glm::dvec3{Direction.x, 0.0, Direction.y}; +} + +// direction to the left of the provided one, on the ground +glm::dvec2 left_of(glm::dvec2 const &Direction) +{ + return glm::dvec2{Direction.y, -Direction.x}; +} + +// angle of the turn from one direction to another, positive to the left +double turn_angle(glm::dvec2 const &From, glm::dvec2 const &To) +{ + return std::atan2(glm::dot(To, left_of(From)), glm::dot(To, From)); +} + +} // namespace + +// puts provided shapes in the section of the scene holding specified point +void owned_shapes::show(std::vector Shapes, glm::dvec3 const &Location) +{ + if (m_section != nullptr || Shapes.empty() || simulation::Region == nullptr || false == simulation::Region->point_inside(Location)) { return; } - auto shapes{create_shapes()}; - if (shapes.empty()) - { - return; - } - auto §ion{simulation::Region->section(location())}; + auto §ion{simulation::Region->section(Location)}; // the rest of the section has to be built by now, or it'd count our shapes as waiting for it too section.create_geometry(); if (m_bank.bank == 0 && m_bank.chunk == 0) { m_bank = GfxRenderer->Create_Bank(); + ownedbanks.emplace(m_bank.bank); } - for (auto &shape : shapes) + for (auto &shape : Shapes) { // shapes held by a section are drawn relative to its centre shape.origin(section.m_area.center); @@ -935,14 +961,14 @@ void road_node::show() shape.create_geometry(m_bank); m_geometry.emplace_back(shape.data().geometry); section.m_shapes.emplace_back(std::move(shape)); - // the section may need to reach further to keep the road from being culled at its edges + // the section may need to reach further to keep the shape from being culled at its edges section.m_area.radius = std::max(section.m_area.radius, static_cast(glm::length(section.m_area.center - centre) + radius)); } m_section = §ion; } -// takes back geometry put in the scene by show() -void road_node::hide() +// takes the shapes back +void owned_shapes::hide() { if (m_section == nullptr) { @@ -960,6 +986,650 @@ void road_node::hide() m_section = nullptr; } +// true if specified geometry was put in the scene by any object of this class +bool owned_shapes::owns(gfx::geometry_handle const &Geometry) +{ + return Geometry.bank != 0 && ownedbanks.count(Geometry.bank) != 0; +} + +// puts geometry of the road in the scene as shapes of its own, which can be taken back +void road_node::show() +{ + if (m_merged || m_shapes.shown()) + { + return; + } + m_shapes.show(create_shapes(), location()); +} + +// takes back geometry put in the scene by show() +void road_node::hide() +{ + m_shapes.hide(); +} + +// brings the content to a usable form +void junction_node::state::normalize() +{ + for (auto &arm : arms) + { + arm.incoming = std::clamp(arm.incoming, 0, 8); + arm.outgoing = std::clamp(arm.outgoing, 0, 8); + arm.width = std::max(1.f, arm.width); + arm.direction = (glm::length(arm.direction) > 1e-6 ? glm::normalize(arm.direction) : glm::dvec2{0.0, 1.0}); + } + if (texturelength < 0.01f) + { + texturelength = 4.f; + } + if (surface.empty()) + { + surface = "none"; + } + static std::array const environments{"flat", "mountains", "mountain", "canyon", "tunnel", "bridge", "bank"}; + if (std::find(environments.begin(), environments.end(), environment) == environments.end()) + { + environment = "flat"; + } +} + +// width of the road at specified arm +double junction_node::state::arm_width(std::size_t const Arm) const +{ + return Arm < arms.size() ? static_cast(arms[Arm].incoming + arms[Arm].outgoing) * arms[Arm].width : 0.0; +} + +// where a lane of an arm meets the junction; the lanes of each kind are counted from the middle of the road, starting with 1 +glm::dvec3 junction_node::state::lane_point(std::size_t const Arm, bool const Incoming, int const Lane) const +{ + auto const &arm{arms[Arm]}; + // seen from the junction the traffic coming in is on the left side of the road, and the one going out on the right + auto const divide{0.5 * arm_width(Arm) - static_cast(arm.incoming) * arm.width}; + auto const offset{Incoming ? divide + (Lane - 0.5) * arm.width : divide - (Lane - 0.5) * arm.width}; + return arm.position + flat(left_of(arm.direction)) * offset; +} + +// arms in the order they're met going around the junction +std::vector junction_node::state::arm_order() const +{ + std::vector order(arms.size()); + for (std::size_t idx = 0; idx < order.size(); ++idx) + { + order[idx] = idx; + } + // the order goes from the z axis towards the x axis, which makes triangles spanned from the centre face up + std::sort(order.begin(), order.end(), [this](std::size_t const Left, std::size_t const Right) { return std::atan2(arms[Left].direction.x, arms[Left].direction.y) < std::atan2(arms[Right].direction.x, arms[Right].direction.y); }); + return order; +} + +// edge of the surface: ends of the arms joined with rounded corners, going around the junction +std::vector junction_node::state::outline(std::vector> *Corners) const +{ + std::vector points; + auto const order{arm_order()}; + for (std::size_t idx = 0; idx < order.size(); ++idx) + { + auto const &arm{arms[order[idx]]}; + auto const &next{arms[order[(idx + 1) % order.size()]]}; + auto const halfwidth{0.5 * arm_width(order[idx])}; + auto const nexthalfwidth{0.5 * arm_width(order[(idx + 1) % order.size()])}; + // the end of the arm, then the corner leading to the next one + auto const right{arm.position - flat(left_of(arm.direction)) * halfwidth}; + auto const left{arm.position + flat(left_of(arm.direction)) * halfwidth}; + auto const nextright{next.position - flat(left_of(next.direction)) * nexthalfwidth}; + points.emplace_back(right); + points.emplace_back(left); + // the corner is bent towards the point where the edges of both roads would meet, if there's a reasonable one + auto control{0.5 * (left + nextright)}; + auto const cross{arm.direction.x * next.direction.y - arm.direction.y * next.direction.x}; + if (std::abs(cross) > 0.05) + { + // left - arm.direction * t == nextright - next.direction * s + glm::dvec2 const delta{nextright.x - left.x, nextright.z - left.z}; + auto const t{-(delta.x * next.direction.y - delta.y * next.direction.x) / cross}; + auto const s{-(delta.x * arm.direction.y - delta.y * arm.direction.x) / cross}; + if (t > 0.0 && s > 0.0 && t < 100.0 && s < 100.0) + { + control = left - flat(arm.direction) * t; + control.y = 0.5 * (left.y + nextright.y); + } + } + std::vector corner{left}; + for (int step = 1; step < corner_steps; ++step) + { + auto const t{static_cast(step) / corner_steps}; + auto const point{(1.0 - t) * (1.0 - t) * left + 2.0 * (1.0 - t) * t * control + t * t * nextright}; + points.emplace_back(point); + corner.emplace_back(point); + } + corner.emplace_back(nextright); + if (Corners != nullptr) + { + Corners->emplace_back(std::move(corner)); + } + } + return points; +} + +junction_node::junction_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} + +// restores content of the node from provided input stream; reads up to and including the closing 'endjunction' +void junction_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + auto const label{m_name.empty() ? std::string{"unnamed junction"} : "junction \"" + m_name + "\""}; + + m_state = state{}; + Input.getTokens(3); + Input >> m_state.centre.x >> m_state.centre.y >> m_state.centre.z; + m_state.centre += Offset; + + auto token{Input.getToken()}; + while (false == token.empty() && token != "endjunction") + { + if (token == "arm") + { + // arm + arm_data arm; + Input.getTokens(3); + Input >> arm.position.x >> arm.position.y >> arm.position.z; + arm.position += Offset; + Input.getTokens(2); + Input >> arm.direction.x >> arm.direction.y; + Input.getTokens(3); + Input >> arm.incoming >> arm.outgoing >> arm.width; + m_state.arms.emplace_back(arm); + } + else if (token == "surface") + { + m_state.surface = Input.getToken(); + replace_slashes(m_state.surface); + } + else if (token == "texlength") + { + Input.getTokens(); + Input >> m_state.texturelength; + } + else if (token == "markings") + { + auto const colour{Input.getToken()}; + m_state.markings = (colour == "white" ? road_node::marking_colour::white : colour == "orange" ? road_node::marking_colour::orange : road_node::marking_colour::none); + } + else if (token == "velocity") + { + Input.getTokens(); + Input >> m_state.velocity; + } + else if (token == "friction") + { + Input.getTokens(); + Input >> m_state.friction; + } + else if (token == "environment") + { + m_state.environment = Input.getToken(); + } + else + { + ErrorLog("Bad junction: unknown property: \"" + token + "\" defined for " + label); + } + token = Input.getToken(); + } + if (m_state.arms.size() < 2 || m_state.arms.size() > 4) + { + ErrorLog("Bad junction: " + label + " has " + std::to_string(m_state.arms.size()) + " arms, it takes 2 to 4"); + if (m_state.arms.size() > 4) + { + m_state.arms.resize(4); + } + } + m_state.normalize(); + location(m_state.centre); +} + +void junction_node::define(state const &State) +{ + m_state = State; + m_state.normalize(); + location(m_state.centre); + m_area.radius = -1.f; +} + +// makes paths leading through the junction, between the lanes of the roads attached to it at the moment +void junction_node::create_links() +{ + m_movements.clear(); + auto const &arms{m_state.arms}; + // lanes of the roads attached to the arms; a road is attached if one of its ends is where the arm is + struct arm_lanes + { + std::vector incoming; + std::vector outgoing; + }; + std::vector lanes(arms.size()); + for (std::size_t arm = 0; arm < arms.size(); ++arm) + { + lanes[arm].incoming.assign(static_cast(arms[arm].incoming), nullptr); + lanes[arm].outgoing.assign(static_cast(arms[arm].outgoing), nullptr); + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved || road->tracks().size() != road->definition().lanes.size()) + { + continue; + } + auto const &layout{road->definition()}; + for (auto const atend : {false, true}) + { + if (glm::distance(layout.axis.points[atend ? segment_data::point::end : segment_data::point::start], arms[arm].position) > junction_reach) + { + continue; + } + // the lanes heading for this end of the road lead into the junction, the others lead out of it + for (int lane = 1; lane <= arms[arm].incoming; ++lane) + { + auto const index{layout.lane_index((atend ? "f" : "b") + std::to_string(lane))}; + if (index >= 0 && road->tracks()[index] != nullptr && glm::distance(glm::dvec3{road->tracks()[index]->CurrentSegment()->FastGetPoint_1()}, m_state.lane_point(arm, true, lane)) < junction_fit) + { + lanes[arm].incoming[lane - 1] = road->tracks()[index]; + } + } + for (int lane = 1; lane <= arms[arm].outgoing; ++lane) + { + auto const index{layout.lane_index((atend ? "b" : "f") + std::to_string(lane))}; + if (index >= 0 && road->tracks()[index] != nullptr && glm::distance(glm::dvec3{road->tracks()[index]->CurrentSegment()->FastGetPoint_0()}, m_state.lane_point(arm, false, lane)) < junction_fit) + { + lanes[arm].outgoing[lane - 1] = road->tracks()[index]; + } + } + } + } + } + + enum class way + { + left, + straight, + right + }; + struct exit_data + { + std::size_t arm; + double turn; + way kind; + TTrack *track; + }; + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + auto const write_point = [&text](glm::dvec3 const &Point) { text << Point.x << ' ' << Point.y << ' ' << Point.z << ' '; }; + + for (std::size_t arm = 0; arm < arms.size(); ++arm) + { + auto const heading{-arms[arm].direction}; + // the other arms with a road to leave by, from the leftmost to the rightmost + std::vector exits; + for (std::size_t other = 0; other < arms.size(); ++other) + { + if (other == arm || std::all_of(lanes[other].outgoing.begin(), lanes[other].outgoing.end(), [](TTrack const *Track) { return Track == nullptr; })) + { + continue; + } + exits.push_back({other, turn_angle(heading, arms[other].direction), way::straight, nullptr}); + } + std::sort(exits.begin(), exits.end(), [](exit_data const &Left, exit_data const &Right) { return Left.turn > Right.turn; }); + if (exits.size() == 3) + { + exits[0].kind = way::left; + exits[2].kind = way::right; + } + else + { + auto const sideways{glm::radians(50.0)}; + for (auto &exit : exits) + { + exit.kind = (exit.turn > sideways ? way::left : exit.turn < -sideways ? way::right : way::straight); + } + if (exits.size() == 2 && exits[0].kind == exits[1].kind) + { + // two ways of the same kind are told apart by which one is more to the left + if (exits[0].kind == way::right) + { + exits[0].kind = way::straight; + } + else if (exits[0].kind == way::left) + { + exits[1].kind = way::straight; + } + else + { + exits[0].kind = way::left; + exits[1].kind = way::right; + } + } + } + auto const lanecount{static_cast(lanes[arm].incoming.size())}; + for (int lane = 1; lane <= lanecount; ++lane) + { + auto *entry{lanes[arm].incoming[lane - 1]}; + if (entry == nullptr) + { + continue; + } + // with more lanes than one the inner lane takes the left turns, the outer one the right turns, and all go straight + std::vector ways; + for (auto const &exit : exits) + { + auto const allowed{lanecount == 1 || exit.kind == way::straight || (exit.kind == way::left && lane == 1) || (exit.kind == way::right && lane == lanecount)}; + if (allowed) + { + ways.emplace_back(exit); + } + } + if (ways.empty()) + { + ways = exits; + } + for (auto &exit : ways) + { + // the lane of the same number on the road to leave by, or the nearest one there is + auto const &candidates{lanes[exit.arm].outgoing}; + for (int shift = 0; shift < static_cast(candidates.size()) && exit.track == nullptr; ++shift) + { + for (auto const index : {lane - 1 - shift, lane - 1 + shift}) + { + if (index >= 0 && index < static_cast(candidates.size()) && candidates[index] != nullptr) + { + exit.track = candidates[index]; + break; + } + } + } + if (exit.track == nullptr) + { + exit.track = candidates.back(); + } + } + ways.erase(std::remove_if(ways.begin(), ways.end(), [](exit_data const &Exit) { return Exit.track == nullptr; }), ways.end()); + if (ways.empty()) + { + continue; + } + if (ways.size() == 3) + { + // the legacy crossroads has the way straight ahead at its second point, the left one at the third and the right one at the fourth + std::swap(ways[0], ways[1]); + } + + // shape of the ways: each leaves the lane coming in the way that lane goes, and joins the lane going out the way that one goes + glm::dvec3 const start{entry->CurrentSegment()->FastGetPoint_1()}; + std::vector ends; + std::vector endcontrols; + double reach{0.0}; + for (auto const &exit : ways) + { + glm::dvec3 const end{exit.track->CurrentSegment()->FastGetPoint_0()}; + auto const span{glm::distance(start, end)}; + ends.emplace_back(end); + endcontrols.emplace_back(-flat(arms[exit.arm].direction) * (0.39 * span)); + reach += 0.39 * span / ways.size(); + } + auto const startcontrol{flat(heading) * reach}; + for (std::size_t idx = 0; idx < ways.size(); ++idx) + { + segment_data movement; + movement.points[segment_data::point::start] = start; + movement.points[segment_data::point::control1] = startcontrol; + movement.points[segment_data::point::control2] = endcontrols[idx]; + movement.points[segment_data::point::end] = ends[idx]; + m_movements.emplace_back(movement); + } + + text.str(""); + text << (ways.size() == 1 ? "road " : "cross ") << glm::distance(start, ends[0]) << ' ' << arms[arm].width << ' ' << m_state.friction << ' ' << m_state.sounddistance << ' ' << m_state.quality << " 0 " << m_state.environment << " unvis "; + // first path: from the lane coming in to the first way out + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[0]); + write_point(ends[0]); + text << "0 0 "; + if (ways.size() == 2) + { + // second path of a crossroads of three roads starts where the first one does + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[1]); + write_point(ends[1]); + text << "0 0 "; + } + else if (ways.size() == 3) + { + // second path of a crossroads of four roads runs between its third and fourth point + write_point(ends[1]); + text << "0 "; + write_point(endcontrols[1]); + write_point(endcontrols[2]); + write_point(ends[2]); + text << "0 0 "; + } + if (m_state.velocity > 0.f) + { + text << "velocity " << m_state.velocity << ' '; + } + text << "endtrack"; + auto *link{create_track(text.str(), m_links.size())}; + + // the path is tied to the lanes by hand: the lanes going out are shared by the paths from all the other arms, + // which is more than the automatic joining of path ends can express + if (ways.size() == 1) + { + entry->ConnectNextPrev(link, 0); + link->trNext = ways[0].track; + link->iNextDirection = 0; + if (ways[0].track->trPrev == nullptr) + { + ways[0].track->trPrev = link; + ways[0].track->iPrevDirection = 1; + } + } + else if (link->SwitchExtension != nullptr) + { + auto &extension{*link->SwitchExtension}; + // the points of the crossroads are stored as: first, second, fourth, third + extension.pPrevs[0] = entry; + extension.iPrevDirection[0] = 1; + extension.pNexts[0] = ways[0].track; + extension.iNextDirection[0] = 0; + extension.pPrevs[1] = (ways.size() == 3 ? ways[2].track : ways[1].track); + extension.iPrevDirection[1] = 0; + extension.pNexts[1] = (ways.size() == 3 ? ways[1].track : nullptr); + extension.iNextDirection[1] = 0; + link->Switch(0); + entry->trNext = link; + entry->iNextDirection = 0; + // the lanes going out get to know one of the paths leading to them, for whatever goes backwards + int const ends2[]{1, 2}; + int const ends3[]{1, 3, 2}; + for (std::size_t idx = 0; idx < ways.size(); ++idx) + { + if (ways[idx].track->trPrev == nullptr) + { + ways[idx].track->trPrev = link; + ways[idx].track->iPrevDirection = (ways.size() == 3 ? ends3[idx] : ends2[idx]); + } + } + } + m_links.emplace_back(link); + } + } +} + +// gives up the paths leading through the junction +std::vector junction_node::release_links() +{ + std::vector released; + released.swap(m_links); + m_movements.clear(); + return released; +} + +// true if there's a vehicle on any path of the junction +bool junction_node::occupied() const +{ + return std::any_of(m_links.begin(), m_links.end(), [](TTrack const *Track) { return Track != nullptr && false == Track->Dynamics.empty(); }); +} + +// generates geometry of the surface +std::vector junction_node::create_shapes() const +{ + std::vector shapes; + std::vector> corners; + auto const points{m_state.outline(&corners)}; + if (points.size() < 3) + { + return shapes; + } + auto const ¢re{m_state.centre}; + if (m_state.surface != "none") + { + // a fan of triangles spanned from the centre, with the image laid out flat over the ground + auto const tile{static_cast(texture_ratio(m_state.surface)) * m_state.texturelength}; + auto const vertex = [&](glm::dvec3 const &Point, glm::vec3 const &Normal) { + return world_vertex{Point, Normal, {static_cast(0.5 + (Point.x - centre.x) / tile), static_cast(0.5 + (Point.z - centre.z) / m_state.texturelength)}}; + }; + std::vector vertices; + for (std::size_t idx = 0; idx < points.size(); ++idx) + { + auto const ¤t{points[idx]}; + auto const &next{points[(idx + 1) % points.size()]}; + auto normal{glm::cross(current - centre, next - centre)}; + if (glm::length2(normal) < 1e-12) + { + continue; + } + glm::vec3 const facing{glm::normalize(normal)}; + vertices.emplace_back(vertex(centre, facing)); + vertices.emplace_back(vertex(current, facing)); + vertices.emplace_back(vertex(next, facing)); + } + if (false == vertices.empty()) + { + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material(m_state.surface), std::move(vertices), glm::dvec3{0.0}); + shapes.emplace_back(std::move(shape)); + } + } + if (m_state.markings != road_node::marking_colour::none) + { + // edge lines carried around the corners + std::vector vertices; + glm::dvec3 const lift{0.0, line_lift, 0.0}; + glm::vec3 const upwards{0.f, 1.f, 0.f}; + for (auto const &corner : corners) + { + for (std::size_t idx = 0; idx + 1 < corner.size(); ++idx) + { + // the line keeps its distance from the edge, on the side of the centre + auto const inwards = [&](glm::dvec3 const &Point) { + auto direction{centre - Point}; + direction.y = 0.0; + return glm::length2(direction) > 1e-12 ? glm::normalize(direction) : glm::dvec3{0.0}; + }; + auto const outer0{corner[idx] + inwards(corner[idx]) * edge_inset + lift}; + auto const inner0{corner[idx] + inwards(corner[idx]) * (edge_inset + line_width) + lift}; + auto const outer1{corner[idx + 1] + inwards(corner[idx + 1]) * edge_inset + lift}; + auto const inner1{corner[idx + 1] + inwards(corner[idx + 1]) * (edge_inset + line_width) + lift}; + // the corners are listed going around the junction the way which makes these face up + vertices.push_back({inner0, upwards, {0.f, 0.f}}); + vertices.push_back({outer0, upwards, {1.f, 0.f}}); + vertices.push_back({outer1, upwards, {1.f, 1.f}}); + vertices.push_back({inner0, upwards, {0.f, 0.f}}); + vertices.push_back({outer1, upwards, {1.f, 1.f}}); + vertices.push_back({inner1, upwards, {0.f, 1.f}}); + } + } + if (false == vertices.empty()) + { + lighting_data paint; + paint.diffuse = (m_state.markings == road_node::marking_colour::white ? glm::vec4{0.92f, 0.92f, 0.92f, 1.f} : glm::vec4{0.95f, 0.5f, 0.08f, 1.f}); + paint.ambient = paint.diffuse; + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material("colored"), std::move(vertices), glm::dvec3{0.0}); + shape.lighting(paint); + shapes.emplace_back(std::move(shape)); + } + } + return shapes; +} + +void junction_node::show() +{ + if (m_merged || m_shapes.shown()) + { + return; + } + m_shapes.show(create_shapes(), location()); +} + +void junction_node::hide() +{ + m_shapes.hide(); +} + +// creates a path through the junction from provided definition and registers it with the simulation +TTrack *junction_node::create_track(std::string const &Definition, std::size_t const Index) +{ + cParser parser(Definition, cParser::buffer_TEXT); + scene::node_data nodedata; + nodedata.range_max = -1.0; + nodedata.name = (m_name.empty() ? std::string{} : m_name + ":way" + std::to_string(Index + 1)); + nodedata.type = "track"; + auto *track{new TTrack(nodedata)}; + track->Load(&parser, glm::dvec3{0.0}); + track->m_road = this; + // NOTE: the table points the name at the newest path, which is what a junction made anew in the editor needs + simulation::Paths.insert(track); + simulation::Region->insert_and_register(track); + return track; +} + +float junction_node::radius_() +{ + float radius{0.f}; + for (std::size_t arm = 0; arm < m_state.arms.size(); ++arm) + { + radius = std::max(radius, static_cast(glm::distance(m_state.arms[arm].position, m_state.centre) + 0.5 * m_state.arm_width(arm))); + } + return radius; +} + +void junction_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +void junction_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +// export() subclass details, sends basic content of the class in legacy (text) format to provided stream +void junction_node::export_as_text_(std::ostream &Output) const +{ + Output << "junction "; + auto const precision{Output.precision(std::numeric_limits::digits10)}; + Output << m_state.centre.x << ' ' << m_state.centre.y << ' ' << m_state.centre.z << ' '; + for (auto const &arm : m_state.arms) + { + Output << "arm " << arm.position.x << ' ' << arm.position.y << ' ' << arm.position.z << ' ' << arm.direction.x << ' ' << arm.direction.y << ' ' << arm.incoming << ' ' << arm.outgoing << ' ' << arm.width << ' '; + } + Output.precision(precision); + Output << "surface " << m_state.surface << ' ' << "texlength " << m_state.texturelength << ' ' << "markings " + << (m_state.markings == road_node::marking_colour::white ? "white" : m_state.markings == road_node::marking_colour::orange ? "orange" : "none") << ' ' << "velocity " << m_state.velocity << ' ' << "friction " << m_state.friction << ' ' + << "environment " << m_state.environment << ' '; + Output << "endjunction" + << "\n"; +} + // creates a path for the vehicles and registers it with the simulation TTrack *road_node::create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity) { @@ -1116,4 +1786,42 @@ void road_table::create_geometry(scene::scratch_data &Scratchpad) } } +// legacy style initialization, to be performed when the tracks are joined, ahead of the roads +void junction_table::InitJunctions() +{ + for (auto *junction : m_items) + { + if (junction != nullptr) + { + junction->create_links(); + } + } +} + +// generates geometry of the junctions and puts it in the scene +void junction_table::create_geometry(scene::scratch_data &Scratchpad) +{ + if (simulation::Region == nullptr) + { + return; + } + for (auto *junction : m_items) + { + if (junction == nullptr) + { + continue; + } + if (Global.editor_session && false == Global.NvRenderer) + { + junction->show(); + continue; + } + for (auto &shape : junction->create_shapes()) + { + simulation::Region->insert(shape, Scratchpad, false); + } + junction->merged(true); + } +} + //--------------------------------------------------------------------------- diff --git a/world/Road.h b/world/Road.h index c444ab8a..de77977f 100644 --- a/world/Road.h +++ b/world/Road.h @@ -26,6 +26,27 @@ struct scratch_data; class basic_section; } +// geometry a node puts in the scene as shapes of its own, apart from the shapes of the scenery, so it can take it back +class owned_shapes +{ + public: + // puts provided shapes in the section of the scene holding specified point + void show(std::vector Shapes, glm::dvec3 const &Location); + // takes the shapes back + void hide(); + bool shown() const + { + return m_section != nullptr; + } + // true if specified geometry was put in the scene by any object of this class + static bool owns(gfx::geometry_handle const &Geometry); + + private: + gfx::geometrybank_handle m_bank{0, 0}; + std::vector m_geometry; + scene::basic_section *m_section{nullptr}; +}; + // a piece of a road for wheeled traffic: an axis, and a cross-section which stays the same along it. // the node itself is neither driven on nor drawn. it produces the lanes, which are ordinary invisible one-way tracks, // and the surface, the sides and the markings, which are ordinary shapes. this way the vehicles, the ai and the renderers @@ -176,10 +197,108 @@ class road_node : public scene::basic_node std::vector m_tracks; std::vector m_turns; bool m_merged{false}; - // geometry put in the scene by show() - gfx::geometrybank_handle m_bank{0, 0}; - std::vector m_geometry; - scene::basic_section *m_section{nullptr}; + owned_shapes m_shapes; // geometry put in the scene by show() +}; + +// a place where roads meet. each arm of the junction is where a road ends: a point in the middle of that end, the way +// the road leaves, and the lanes it has there. the junction produces the surface between the arms, and paths leading +// the vehicles from each lane coming in to the lanes going out of the other arms. a lane with more than one way out +// gets a legacy crossroads path, which is what the vehicles and their drivers already know how to pick a way through. +// scenery entry: +// node junction arm ... [ ]... endjunction +class junction_node : public scene::basic_node +{ + + public: + // types + struct arm_data + { + glm::dvec3 position{0.0}; // middle of the end of the road + glm::dvec2 direction{0.0, 1.0}; // the way out of the junction, seen from above (x, z) + int incoming{1}; // lanes leading into the junction + int outgoing{1}; // lanes leading out of it + float width{3.5f}; // width of a lane + }; + struct state + { + glm::dvec3 centre{0.0}; + std::vector arms; + std::string surface{"asphaltdark1"}; + float texturelength{4.f}; + road_node::marking_colour markings{road_node::marking_colour::white}; + float velocity{30.f}; // speed limit on the way through + float friction{0.85f}; + float sounddistance{25.f}; + int quality{15}; + std::string environment{"flat"}; + + // brings the content to a usable form + void normalize(); + // width of the road at specified arm + double arm_width(std::size_t const Arm) const; + // where a lane of an arm meets the junction; the lanes of each kind are counted from the middle of the road, starting with 1 + glm::dvec3 lane_point(std::size_t const Arm, bool const Incoming, int const Lane) const; + // arms in the order they're met going around the junction + std::vector arm_order() const; + // edge of the surface: ends of the arms joined with rounded corners, going around the junction. + // Corners, if provided, receives the points of each corner apart + std::vector outline(std::vector> *Corners = nullptr) const; + }; + // constructors + explicit junction_node(scene::node_data const &Nodedata); + // methods + // restores content of the node from provided input stream; reads up to and including the closing 'endjunction' + void import(cParser &Input, glm::dvec3 const &Offset); + state const &definition() const + { + return m_state; + } + void define(state const &State); + // makes paths leading through the junction, between the lanes of the roads attached to it at the moment + void create_links(); + // gives up the paths leading through the junction; the caller is expected to take them out of use + std::vector release_links(); + std::vector const &links() const + { + return m_links; + } + // shapes of the ways through the junction, for display + std::vector const &movements() const + { + return m_movements; + } + // true if there's a vehicle on any path of the junction + bool occupied() const; + // generates geometry of the surface + std::vector create_shapes() const; + // puts geometry of the junction in the scene as shapes of its own, or takes it back + void show(); + void hide(); + void merged(bool const Merged) + { + m_merged = Merged; + } + bool merged() const + { + return m_merged; + } + // members + bool m_editorremoved{false}; + + private: + // methods + float radius_() override; + void serialize_(std::ostream &Output) const override; + void deserialize_(std::istream &Input) override; + void export_as_text_(std::ostream &Output) const override; + // creates a path through the junction from provided definition and registers it with the simulation + TTrack *create_track(std::string const &Definition, std::size_t const Index); + // members + state m_state; + std::vector m_links; + std::vector m_movements; + bool m_merged{false}; + owned_shapes m_shapes; }; // collection of roads present in the scene @@ -193,4 +312,15 @@ class road_table : public basic_table void create_geometry(scene::scratch_data &Scratchpad); }; +// collection of road junctions present in the scene +class junction_table : public basic_table +{ + + public: + // legacy style initialization, to be performed when the tracks are joined, ahead of the roads + void InitJunctions(); + // generates geometry of the junctions and puts it in the scene + void create_geometry(scene::scratch_data &Scratchpad); +}; + //--------------------------------------------------------------------------- diff --git a/world/Track.h b/world/Track.h index 2fb9b0ce..b894655d 100644 --- a/world/Track.h +++ b/world/Track.h @@ -186,7 +186,7 @@ public: std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; - road_node *m_road { nullptr }; // road which generated this path as one of its lanes. such path is one-way, and isn't saved nor edited on its own + scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own int iterate_stamp = 0; // sleepermodel optional parameter -------------------------------------------------