From 27a94509c451a9b2126d0828e8a6710992899328 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sun, 4 Oct 2026 01:55:35 +0200 Subject: [PATCH] feat(roads): right of way and lane turns at junctions, road weights, kerbs, medians and forking roads, banks to the ground, roundabouts, pedestrian crossings, spawn sets by count; fix(editor): model skin export, placement on the ground under the orthophoto --- application/editormode.cpp | 127 ++- application/editormode.h | 21 +- application/editormoderoad.cpp | 456 +++++++++- editor/editorRoad.cpp | 209 ++++- editor/editorRoad.hpp | 9 +- model/AnimModel.cpp | 37 +- model/AnimModel.h | 1 + simulation/simulation.cpp | 3 +- simulation/simulationstateserializer.cpp | 3 +- vehicle/Driver.cpp | 2 +- world/Road.cpp | 1020 ++++++++++++++++++++-- world/Road.h | 121 ++- world/RoadOrder.h | 60 ++ world/RoadPoint.cpp | 318 ++++--- world/RoadPoint.h | 28 +- world/Track.cpp | 23 + world/Track.h | 4 + 17 files changed, 2198 insertions(+), 244 deletions(-) create mode 100644 world/RoadOrder.h diff --git a/application/editormode.cpp b/application/editormode.cpp index d690a199..32dd37cf 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -476,7 +476,10 @@ editor_mode::editor_mode() { ui()->set_gizmo_options([this]() { render_gizmo_options(); }); ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); ui()->set_track_options([this]() { render_track_ui(); }); - ui()->set_menu_options([this]() { render_road_menu(); }); + ui()->set_menu_options([this]() { + render_object_menu(); + render_road_menu(); + }); m_roadtool.settings.normalize(); road_select(nullptr); // the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included @@ -744,7 +747,7 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button) std::string name = "editor_"; - TAnimModel *cloned = simulation::State.create_model(*src, name, newPos); + TAnimModel *cloned = simulation::State.create_model(*src, name, placement_on_ground(newPos)); oldPos = newPos; m_brush_has_last = true; if (!cloned) @@ -2010,7 +2013,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, ui()->set_status("Template \"" + File + "\" can't be placed: the file of the active layer would receive it with a rotation or a scale in effect. Pick another layer.", true); return; } - glm::dvec3 const location = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); + glm::dvec3 const location = placement_on_ground(Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position())); std::optional yaw; if (RotationMode == functions_panel::RANDOM) yaw = static_cast(LocalRandom(0.0, 360.0)); @@ -2148,6 +2151,119 @@ void editor_mode::capture_terrain() simulation::State.delete_model(model); } +// the ground under a point the cursor landed on. the orthophoto laid over the ground catches the cursor like any +// other surface, so the point can hang over the ground by as much as the orthophoto is lifted, or lie on the flat sheet +// the orthophoto makes where it's not fitted to the ground. what's put in the scenery is to stand on the ground itself +glm::dvec3 editor_mode::placement_on_ground(glm::dvec3 Location) +{ + double constexpr reach = 2.0; // the ground is asked about this far around the point + double constexpr slack = 0.5; // a surface this far over the point still is what the cursor landed on + double constexpr height_tolerance = 50.0; // with nothing there or lower, how far up the ground is looked for + float constexpr terrain_model_radius = 50.0f; // the same models count as ground as for the area fill and the orthophoto + + glm::dvec2 const bmin{Location.x - reach, Location.z - reach}; + glm::dvec2 const bmax{Location.x + reach, Location.z + reach}; + std::vector triangles; + gather_ground_triangles(bmin, bmax, true, terrain_model_radius, triangles); + triangle_grid const ground(std::move(triangles), bmin, bmax); + auto const terrains = active_terrains(); + for (double const ceiling : {Location.y + slack, Location.y + height_tolerance}) + { + double y = -std::numeric_limits::max(); + bool found = ground.height_at(Location.x, Location.z, ceiling, y); + for (editor_terrain *terrain : terrains) + { + if (!terrain->contains(Location.x, Location.z)) + continue; + double const h = terrain->height_at(Location.x, Location.z); + if (h <= ceiling && (!found || h > y)) + { + y = h; + found = true; + } + } + if (found) + { + Location.y = y; + break; + } + } + // with no ground found the point stays where the cursor landed + return Location; +} + +// puts every instance of the model the selected node shows on the ground under it +void editor_mode::drop_model_instances() +{ + auto *selected = dynamic_cast(m_node); + if (selected == nullptr || selected->Model() == nullptr) + { + ui()->set_status("Select a model in the scene first", true); + return; + } + if (selected->radius() >= 50.0f) + { + // it'd be put on top of itself + ui()->set_status("\"" + selected->Model()->NameGet() + "\" is large enough to count as ground itself, so there's nothing to put it on", true); + return; + } + std::vector instances; + std::vector points; + int locked = 0; + for (auto *instance : simulation::Instances.sequence()) + { + if (instance == nullptr || instance->Model() != selected->Model()) + continue; + if (instance->from_template() || false == scene::Layers.editable(instance)) + { + // placed by a template, or on a layer which is hidden or locked + ++locked; + continue; + } + instances.push_back(instance); + points.push_back(instance->location()); + } + // the ground the road tools use: terrain shapes, large models and the terrain made in the editor, without the roads and the orthophoto. + // a point with nothing under it comes back with the height it has + auto const heights = ground_heights(points, true); + int moved = 0; + for (std::size_t idx = 0; idx < instances.size(); ++idx) + { + if (std::abs(heights[idx] - points[idx].y) < 0.001) + continue; + auto *instance = instances[idx]; + // one step of the history for each instance + push_snapshot(instance, EditorSnapshot::Action::Move); + auto location = points[idx]; + location.y = heights[idx]; + simulation::Region->erase(instance); + instance->location(location); + simulation::Region->insert(instance); + instance->mark_dirty(); + ++moved; + } + std::string status = std::to_string(moved) + " of " + std::to_string(instances.size()) + " instances of \"" + selected->Model()->NameGet() + "\" put on the ground"; + if (moved > 0) + status += "; Ctrl+Z takes them back one at a time"; + if (locked > 0) + status += ". " + std::to_string(locked) + " left alone: placed by a template, or on a layer which can't be edited"; + ui()->set_status(status); +} + +void editor_mode::render_object_menu() +{ + if (ImGui::BeginMenu("Objects")) + { + auto const *selected = dynamic_cast(m_node); + if (ImGui::MenuItem("Put all instances of the selected model on the ground", nullptr, false, selected != nullptr && selected->Model() != nullptr)) + drop_model_instances(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", "Select a model in the scene. Every instance of the same model file is moved straight up or down\n" + "to the ground under it: terrain, large models and terrain made in the editor."); + ImGui::EndMenu(); + } +} + void editor_mode::add_area_fill_point() { // like the other placement tools, the point comes from the depth buffer under the cursor @@ -2156,7 +2272,8 @@ void editor_mode::add_area_fill_point() if (glm::length(offset) < 1e-3) return; // no ground under the cursor (yet) - m_fill_points.push_back(Camera.Pos + offset); + // the outline gives the fill the height to look for the ground at, and the height of what it finds no ground for + m_fill_points.push_back(placement_on_ground(Camera.Pos + offset)); } void editor_mode::undo_last_area_fill() @@ -3227,7 +3344,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const std::string name = "editor_"; glm::dvec3 mouseOffset = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); - TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + mouseOffset); + TAnimModel *cloned = simulation::State.create_model(*src, name, placement_on_ground(Camera.Pos + mouseOffset)); if (!cloned) return; diff --git a/application/editormode.h b/application/editormode.h index 47f21892..104e260a 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -160,6 +160,11 @@ class editor_mode : public application_mode int m_max_history_size{200}; int m_selected_history_idx{-1}; glm::dvec3 clamp_mouse_offset_to_max(const glm::dvec3 &offset); + // the ground under a point the cursor landed on, for things which are to stand on the ground + glm::dvec3 placement_on_ground(glm::dvec3 Location); + // puts every instance of the model the selected node shows on the ground under it + void drop_model_instances(); + void render_object_menu(); // focus camera smoothly on specified node void start_focus(scene::basic_node *node, double duration = 0.6); @@ -523,9 +528,13 @@ class editor_mode : public application_mode bool follow{true}; // built roads are led over the ground between the clicked points float crossingspeed{30.f}; // speed limit on the junctions being made float heightstep{0.1f}; // how much the selected points are raised or lowered at a time + bool ring{false}; // the build tool makes roundabouts + float ringradius{15.f}; // of the middle of the road going around + std::string island{"none"}; // what the middle of a roundabout is covered with + float bankgrade{1.5f}; // metres a bank led to the ground runs for each metre it drops // level crossings and traffic points - int placekind{0}; // what the place tool puts down: 0: level crossing, 1: spawn point, 2: removal point - roadpoint_node::state placing[3]; // what the next point of each kind is going to be like + int placekind{0}; // what the place tool puts down: 0: level crossing, 1: spawn point, 2: removal point, 3: pedestrian crossing + roadpoint_node::state placing[4]; // what the next point of each kind is going to be like float stopmargin{5.f}; // distance from the outermost track of a new crossing to the places the vehicles stop at roadpoint_node::state target; // the point a click of the place tool would make at the moment bool hastarget{false}; @@ -542,6 +551,10 @@ class editor_mode : public application_mode char surface[128]{}; char sides[2][128]{}; char filter[64]{"asph"}; + char kerbtext[128]{}; + char mediantext[128]{}; + char banktext[128]{}; + char islandtext[128]{"none"}; }; void render_road_menu(); void render_road_window(); @@ -564,6 +577,10 @@ class editor_mode : public application_mode bool road_points_apply(std::vector> const &Changes, std::vector> const &Junctions); // takes the selected points out of their roads bool road_points_delete(); + // puts a roundabout around specified point + void road_roundabout(glm::dvec3 const &Ground); + // leads the banks of the selected piece, or of the whole road, to the ground beside it + void road_bank_to_ground(); // level crossings and traffic points void roadpoint_select(roadpoint_node *Point); // works out the point a click of the place tool would make. Fresh: even if the cursor is where it was the last time diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 4c38c798..e588afe6 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -159,15 +159,21 @@ ImU32 const kSnapColor{IM_COL32(255, 60, 255, 255)}; ImU32 const kCrossingColor{IM_COL32(255, 220, 60, 230)}; ImU32 const kSpawnColor{IM_COL32(90, 230, 120, 230)}; ImU32 const kDespawnColor{IM_COL32(255, 110, 110, 230)}; +ImU32 const kCrosswalkColor{IM_COL32(235, 235, 235, 230)}; +double const kBankReach{30.0}; // how far from the road the ground is looked for, for a bank to be led to it +double const kBankStep{0.5}; roadpoint_node::kind_type point_kind(int const Index) { - return Index == 1 ? roadpoint_node::kind_type::spawn : Index == 2 ? roadpoint_node::kind_type::despawn : roadpoint_node::kind_type::crossing; + return Index == 1 ? roadpoint_node::kind_type::spawn : Index == 2 ? roadpoint_node::kind_type::despawn : Index == 3 ? roadpoint_node::kind_type::crosswalk : roadpoint_node::kind_type::crossing; } char const *point_label(roadpoint_node::kind_type const Kind) { - return Kind == roadpoint_node::kind_type::spawn ? "spawn point" : Kind == roadpoint_node::kind_type::despawn ? "removal point" : "level crossing"; + return Kind == roadpoint_node::kind_type::spawn ? "spawn point" : + Kind == roadpoint_node::kind_type::despawn ? "removal point" : + Kind == roadpoint_node::kind_type::crosswalk ? "pedestrian crossing" : + "level crossing"; } // gathers new definitions for the pieces which meet or end at provided points. @@ -267,6 +273,12 @@ void editor_mode::update_road_tool() } if (tool.tool != 1) return; + if (tool.ring) + { + // a click puts a roundabout around the cursor, there's nothing to lead from a point to another + tool.chain = false; + return; + } if (tool.chain) { // what a click would make at the moment, to be drawn over the view @@ -304,6 +316,9 @@ void editor_mode::road_select(road_node *Road) copy_text(tool.surface, sizeof(tool.surface), tool.settings.surface); for (int side = 0; side < 2; ++side) copy_text(tool.sides[side], sizeof(tool.sides[side]), tool.settings.sides[side].material); + copy_text(tool.kerbtext, sizeof(tool.kerbtext), tool.settings.kerbmaterial); + copy_text(tool.mediantext, sizeof(tool.mediantext), tool.settings.median.material); + copy_text(tool.banktext, sizeof(tool.banktext), tool.settings.bankmaterial); } void editor_mode::junction_select(junction_node *Junction) @@ -320,6 +335,7 @@ void editor_mode::junction_select(junction_node *Junction) // the fields for the names of materials are shared with the roads copy_text(tool.surface, sizeof(tool.surface), Junction->definition().surface); copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material); + copy_text(tool.kerbtext, sizeof(tool.kerbtext), Junction->definition().kerbmaterial); } void editor_mode::roadpoint_select(roadpoint_node *Point) @@ -351,6 +367,21 @@ void editor_mode::roadpoint_aim(bool const Fresh) tool.hastarget = false; tool.targetnote.clear(); tool.targetdirection = glm::dvec3{0.0}; + if (kind == roadpoint_node::kind_type::crosswalk) + { + auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; + if (road == nullptr) + { + tool.targetnote = "Point at a road"; + return; + } + // the stripes go across the road, whichever part of it the cursor is over + auto const t{editor_road::nearest_parameter(road->definition(), tool.mouse)}; + tool.target.position = road->definition().point(t); + tool.targetdirection = road->definition().tangent(t); + tool.hastarget = true; + return; + } if (kind == roadpoint_node::kind_type::crossing) { auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; @@ -418,7 +449,7 @@ void editor_mode::roadpoint_place() case roadpoint_node::kind_type::spawn: { tool.status = (state.vehicles.empty() ? std::string{"Spawn point made, with no vehicles: nothing appears there until it's given some (Select, click the point)."} : - "Spawn point made, with " + std::to_string(state.vehicles.size() * state.copies) + " vehicles. They appear while the simulation runs."); + "Spawn point made, with up to " + std::to_string(state.count) + " vehicles on the roads at a time. They appear while the simulation runs."); break; } case roadpoint_node::kind_type::despawn: @@ -426,6 +457,11 @@ void editor_mode::roadpoint_place() tool.status = "Removal point made, it takes vehicles from " + std::to_string(point->lanes().size()) + (point->lanes().size() == 1 ? " lane." : " lanes."); break; } + case roadpoint_node::kind_type::crosswalk: + { + tool.status = "Pedestrian crossing painted."; + break; + } } if (false == tool.targetnote.empty()) tool.status += " " + tool.targetnote + "."; @@ -520,12 +556,13 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", "The speed the vehicles appear with. From there on the drivers go by the speed limits of the lanes."); ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt("Copies of each vehicle", &State.copies, 1, 1, flags)) + if (ImGui::InputInt("Vehicles on the roads at a time", &State.count, 1, 5, flags)) changed = true; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "How many of each vehicle of the set are made. A vehicle can't be in two places at once,\n" - "so this is how many of a kind can be on the road together. All of them are loaded along with the scenery,\n" - "and a vehicle taken by a removal point goes back to wait for its next turn."); + ImGui::SetTooltip("%s", "How many vehicles of this point can be on the scenery at once. That many are made along with the scenery,\n" + "drawn from the listed ones in turn; one of those which wait is put on the road each time its turn comes,\n" + "until all of them are out. A vehicle taken by a removal point goes back to wait for its next turn,\n" + "and so does one which stood still for 15 seconds, other than at a closed level crossing, or went backwards."); // the set ImGui::Separator(); ImGui::Text("Vehicles of the set: %d", static_cast(State.vehicles.size())); @@ -590,6 +627,14 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin "The distance it starts with takes in a single lane."); break; } + case roadpoint_node::kind_type::crosswalk: + { + if (number("Length along the road [m]", State.length, 0.5f, "%.1f")) + changed = true; + ImGui::TextDisabled("Stripes painted from one edge of the road to the other."); + ImGui::TextDisabled("The drivers don't stop for it: there's nobody to cross."); + break; + } } if (changed && Point != nullptr) State.normalize(); @@ -661,6 +706,25 @@ void editor_mode::render_junction_layout(junction_node &Junction) changed = true; } // what the corners between the roads are lined with + if (ImGui::Checkbox("Kerb around the corners", &state.kerbs)) + changed = true; + if (state.kerbs) + { + ImGui::Indent(); + if (number("Kerb width [m]", state.kerbwidth, 0.05f, "%.2f")) + { + state.kerbwidth = std::clamp(state.kerbwidth, 0.05f, 2.0f); + changed = true; + } + if (render_road_material("Kerb material", tool.kerbtext, sizeof(tool.kerbtext), state.kerbmaterial)) + changed = true; + if (state.side.type != road_node::side_type::sidewalk && number("Kerb height [m]", state.kerbheight, 0.01f, "%.2f")) + { + state.kerbheight = std::clamp(state.kerbheight, 0.0f, 1.0f); + changed = true; + } + ImGui::Unindent(); + } char const *sidetypes[]{"none", "shoulder", "sidewalk"}; int type{state.side.type == road_node::side_type::shoulder ? 1 : state.side.type == road_node::side_type::sidewalk ? 2 : 0}; ImGui::SetNextItemWidth(120.0f); @@ -710,7 +774,9 @@ void editor_mode::render_junction_layout(junction_node &Junction) // the roads if (ImGui::CollapsingHeader("Roads of the junction", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextDisabled("The lanes are taken from the roads: change them on the road, the junction follows"); + ImGui::TextDisabled("The lanes are taken from the roads: change them on the road, the junction follows."); + ImGui::TextDisabled("The roads are numbered in the view. Lanes are counted from the middle of the road;"); + ImGui::TextDisabled("automatic: the inner lane turns left, the outer one right, all go straight."); for (std::size_t arm = 0; arm < state.arms.size(); ++arm) { ImGui::PushID(static_cast(arm)); @@ -719,6 +785,36 @@ void editor_mode::render_junction_layout(junction_node &Junction) ImGui::SameLine(); if (ImGui::Checkbox("Stop line", &data.stopline)) changed = true; + if (data.incoming > 0 && state.arms.size() > 2) + { + ImGui::Indent(); + // the entries are in the order of the stored values + char const *priorities[]{"no sign: gives way to the right", "has the right of way", "gives way", "stops, then gives way"}; + int priority{static_cast(data.priority)}; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::Combo("##priority", &priority, priorities, 4)) + { + data.priority = static_cast(std::clamp(priority, 0, 3)); + changed = true; + } + // ways each lane can be left by + char const *ways[]{"automatic", "left", "straight", "left, straight", "right", "left, right", "straight, right", "any"}; + for (int lane = 0; lane < data.incoming; ++lane) + { + ImGui::PushID(100 + lane); + int turns{static_cast(lane) < data.turns.size() ? data.turns[lane] & 7 : 0}; + auto const label{"lane " + std::to_string(lane + 1) + " goes"}; + ImGui::SetNextItemWidth(140.0f); + if (ImGui::Combo(label.c_str(), &turns, ways, 8)) + { + data.turns.resize(std::max(data.turns.size(), static_cast(lane) + 1), 0); + data.turns[lane] = turns; + changed = true; + } + ImGui::PopID(); + } + ImGui::Unindent(); + } ImGui::PopID(); } } @@ -771,14 +867,15 @@ editor_mode::road_plan editor_mode::road_preview(bool const Fresh) plan.snap = {}; } } - if (plan.snap.junction != nullptr) + if (plan.snap.junction != nullptr || plan.snap.half != 0) { - // the junction has its ways through made for a set of lanes at each arm + // the junction has its ways through made for a set of lanes at each arm; a single roadway of a road takes the lanes it has if (false == evenlanes || arriving != plan.snap.backward || leaving != plan.snap.forward || std::abs(lanewidth - plan.snap.width) > 0.01f) { char width[32]; std::snprintf(width, sizeof(width), "%.2f", plan.snap.width); - plan.error = "This arm of the junction takes a road with " + std::to_string(plan.snap.backward) + " lane(s) leading into it and " + std::to_string(plan.snap.forward) + " out of it, " + width + " m wide each"; + plan.error = std::string{plan.snap.junction != nullptr ? "This arm of the junction" : "This roadway"} + " takes a road with "; + plan.error += std::to_string(plan.snap.backward) + " lane(s) leading to it and " + std::to_string(plan.snap.forward) + " away from it, " + width + " m wide each"; return plan; } } @@ -992,6 +1089,11 @@ void editor_mode::road_click() tool.status = reason; return; } + if (tool.ring) + { + road_roundabout(ground); + return; + } if (false == tool.chain) { // first click sets where the road starts @@ -1015,27 +1117,40 @@ void editor_mode::road_click() tool.hasdirection = true; tool.grade = end.outwards.y / glm::length(outwards); tool.hasgrade = true; - if (end.road != nullptr) + if (end.road != nullptr && end.half == 0) { // 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(); - // and it stays as wide as it is at that end + // and it stays as wide as it is at that end, with its directions as far apart auto const scale{tool.settings.taper[end.atend ? 1 : 0]}; tool.settings.taper = {scale, scale}; + auto const gap{tool.settings.median.width[end.atend ? 1 : 0]}; + tool.settings.median.width = {gap, gap}; + // the banks were made for the ground where they are + tool.settings.banks = {}; tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; } else { tool.reversed = false; + if (end.road != nullptr) + { + // one of the roadways of a road which keeps its directions apart is carried on as a road of its own, which looks like the road it leaves + tool.settings = end.road->definition(); + tool.settings.sides[end.half > 0 ? 0 : 1] = road_node::side_data{}; + tool.settings.banks = {}; + } tool.settings.forward = end.forward; tool.settings.backward = end.backward; tool.settings.lanewidth = end.width; tool.settings.taper = {1.f, 1.f}; + tool.settings.median.width = {0.f, 0.f}; 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"; + tool.status = (end.junction != nullptr ? "Leading a road out of \"" + end.junction->name() + "\", with the lanes the junction has there" : + "Carrying on one of the roadways of \"" + end.road->name() + "\" as a one-way road"); } road_select(nullptr); } @@ -1178,6 +1293,12 @@ void editor_mode::road_apply() state.axis = road->definition().axis; // how wide the road is at its points is set at these points, for each piece on its own state.taper = road->definition().taper; + if (road != tool.selected) + { + // and so are the banks, and how far apart the two directions are led + state.median.width = road->definition().median.width; + state.banks = road->definition().banks; + } if (road != tool.selected && road->definition().changes.size() == state.changes.size()) { // where the lanes can be changed is set for each piece on its own @@ -1257,6 +1378,122 @@ bool editor_mode::road_delete() return true; } +// puts a roundabout around specified point +void editor_mode::road_roundabout(glm::dvec3 const &Ground) +{ + auto &tool{m_roadtool}; + std::string reason; + if (false == editor_road::available(&reason)) + { + tool.status = reason; + return; + } + auto const states{editor_road::roundabout(Ground + glm::dvec3{0.0, tool.offset, 0.0}, tool.ringradius, tool.settings, tool.island)}; + if (states.empty()) + { + tool.status = "The roundabout is too small for its lanes: give it a larger radius"; + return; + } + for (auto const &state : states) + { + for (auto const t : {0.0, 0.5}) + { + if (editor_road::nearest(state.point(t), 0.0) != nullptr || editor_road::nearest_junction(state.point(t)) != nullptr) + { + tool.status = "There's a road in the way. Make the roundabout next to it, then lead the road into its side"; + return; + } + } + } + auto const created{editor_road::create(states)}; + if (created.empty()) + { + tool.status = "The roundabout wasn't created"; + return; + } + editor_road::record record; + record.roads_created = created; + push_road_snapshot(std::move(record)); + tool.status = "Roundabout made. Untick Roundabout and lead roads out of its side, or into it: the traffic joining it gives way to the traffic going around."; +} + +// leads the banks of the selected piece, or of the whole road, to the ground beside it +void editor_mode::road_bank_to_ground() +{ + auto &tool{m_roadtool}; + if (tool.selected == nullptr) + return; + auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; + std::string reason; + for (auto const *road : pieces) + { + if (false == editor_road::can_edit(*road, &reason)) + { + tool.status = "\"" + road->name() + "\" can't be changed: " + reason; + return; + } + } + auto const grade{std::clamp(static_cast(tool.bankgrade), 0.25, 10.0)}; + auto const steps{static_cast(kBankReach / kBankStep)}; + std::vector> changes; + for (auto *road : pieces) + { + auto state{road->definition()}; + for (int side = 0; side < 2; ++side) + { + auto const &data{state.sides[side]}; + auto const sided{data.type != road_node::side_type::none && data.width > 0.f}; + for (int end = 0; end < 2; ++end) + { + auto const t{static_cast(end)}; + // where the bank starts: past the roadway, the kerb and whatever lies beside it + auto const tangent{state.tangent(t)}; + glm::dvec3 across{tangent.z, 0.0, -tangent.x}; + if (glm::length(across) < 1e-9) + continue; + across = glm::normalize(across) * (side == 0 ? 1.0 : -1.0); + auto edge{state.point(t) + across * (0.5 * state.span(t) + (state.kerbs[side] ? state.kerbwidth : 0.f) + (sided ? data.width : 0.f))}; + if (sided && data.type == road_node::side_type::sidewalk) + edge.y += state.kerbheight; + // the ground further and further from the road, without the roads themselves + std::vector probes; + for (int step = 1; step <= steps; ++step) + probes.emplace_back(edge + across * (step * kBankStep)); + auto const ground{ground_heights(probes, true)}; + // the bank ends where a slope of the set grade, going down or up, gets to the ground + auto const rising{ground.front() > edge.y}; + auto width{kBankReach}; + auto drop{edge.y - ground.back()}; + for (int step = 1; step <= steps; ++step) + { + auto const reach{step * kBankStep}; + auto const height{ground[step - 1]}; + if (rising ? height <= edge.y + reach / grade : height >= edge.y - reach / grade) + { + width = reach; + drop = edge.y - height; + break; + } + } + state.banks[side].set = true; + state.banks[side].width[end] = static_cast(width); + state.banks[side].drop[end] = static_cast(drop); + } + } + changes.emplace_back(road, state); + } + editor_road::record record; + for (auto const &change : changes) + record.roads.emplace_back(change.first, change.first->definition()); + editor_road::apply(changes); + push_road_snapshot(std::move(record)); + road_select(tool.selected); + tool.status = "Banks of " + std::to_string(changes.size()) + (changes.size() == 1 ? " piece" : " pieces") + " led to the ground."; + auto const &layout{tool.selected->definition()}; + if ((layout.sides[0].type != road_node::side_type::shoulder || layout.sides[1].type != road_node::side_type::shoulder) && (layout.bankmaterial.empty() || layout.bankmaterial == "none")) + tool.status += " Pick a bank material to have them drawn where the road has no shoulder."; +} + // takes the selected points out of their roads bool editor_mode::road_points_delete() { @@ -1628,7 +1865,28 @@ bool editor_mode::render_road_layout(road_node::state &State) } ImGui::PopID(); } - if (State.sides[0].type == road_node::side_type::sidewalk || State.sides[1].type == road_node::side_type::sidewalk) + // kerbs + if (ImGui::Checkbox("Kerb on the left", &State.kerbs[0])) + changed = true; + ImGui::SameLine(); + if (ImGui::Checkbox("on the right", &State.kerbs[1])) + changed = true; + auto const island{State.divided() && State.median.type == road_node::median_type::island}; + if (State.kerbs[0] || State.kerbs[1] || island) + { + ImGui::Indent(); + if (number("Kerb width [m]", State.kerbwidth, 0.05f, "%.2f")) + { + State.kerbwidth = std::clamp(State.kerbwidth, 0.05f, 2.0f); + changed = true; + } + if (material("Kerb material", tool.kerbtext, sizeof(tool.kerbtext), State.kerbmaterial)) + changed = true; + if ((State.kerbs[0] || State.kerbs[1]) && (State.kerbmaterial.empty() || State.kerbmaterial == "none")) + ImGui::TextDisabled("Pick a material to have the kerbs drawn"); + ImGui::Unindent(); + } + if (State.kerbs[0] || State.kerbs[1] || island || State.sides[0].type == road_node::side_type::sidewalk || State.sides[1].type == road_node::side_type::sidewalk) { if (number("Kerb height [m]", State.kerbheight, 0.01f, "%.2f")) { @@ -1643,6 +1901,8 @@ bool editor_mode::render_road_layout(road_node::state &State) State.slope.x = std::clamp(State.slope.x, 0.0f, 20.0f); changed = true; } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The bank every shoulder of the road is closed with, unless the side has a bank of its own set below."); if (number("Bank drop [m]", State.slope.y, 0.1f, "%.2f")) { State.slope.y = std::clamp(State.slope.y, 0.0f, 20.0f); @@ -1650,6 +1910,105 @@ bool editor_mode::render_road_layout(road_node::state &State) } } + // banks leading to the ground + if (ImGui::CollapsingHeader("Banks to the ground")) + { + ImGui::TextDisabled("A slope from the edge of the road down, or up, to the ground."); + ImGui::TextDisabled("Set for each piece on its own, at its start and at its end."); + char const *banknames[]{"Bank on the left", "Bank on the right"}; + for (int side = 0; side < 2; ++side) + { + ImGui::PushID(100 + side); + auto &bank{State.banks[side]}; + if (ImGui::Checkbox(banknames[side], &bank.set)) + { + if (bank.set && bank.width[0] <= 0.f && bank.width[1] <= 0.f) + { + bank.width = {State.slope.x, State.slope.x}; + bank.drop = {State.slope.y, State.slope.y}; + } + changed = true; + } + if (bank.set) + { + ImGui::Indent(); + if (number("Width at the start [m]", bank.width[0], 0.5f, "%.2f")) + changed = true; + if (number("Drop at the start [m]", bank.drop[0], 0.25f, "%.2f")) + changed = true; + if (number("Width at the end [m]", bank.width[1], 0.5f, "%.2f")) + changed = true; + if (number("Drop at the end [m]", bank.drop[1], 0.25f, "%.2f")) + changed = true; + ImGui::Unindent(); + } + ImGui::PopID(); + } + if (material("Bank material", tool.banktext, sizeof(tool.banktext), State.bankmaterial)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "What a bank is covered with next to a sidewalk, a kerb or the bare edge of the road.\nA bank of a shoulder is covered with the material of the shoulder."); + if (tool.selected != nullptr) + { + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Run for a metre of drop [m]", &tool.bankgrade, 0.25f, 0.5f, "%.2f")) + tool.bankgrade = std::clamp(tool.bankgrade, 0.25f, 10.0f); + if (ImGui::Button("Lead the banks to the ground")) + { + road_bank_to_ground(); + // the layout shown was replaced with what the road is like now + return false; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Looks for the ground on both sides of the road, at the start and at the end of each piece,\n" + "and sets the banks to reach it with the slope given above: down where the road is over the ground, up where it's cut into it.\n" + "Done for the selected piece, or for every piece of the road if the changes go to the whole road."); + } + } + + // the two directions kept apart + if (State.forward > 0 && State.backward > 0 && ImGui::CollapsingHeader("Between the directions")) + { + char const *mediantypes[]{"nothing, the ground shows", "surface closed to the traffic", "island"}; + int type{State.median.type == road_node::median_type::gap ? 0 : State.median.type == road_node::median_type::island ? 2 : 1}; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::Combo("##median", &type, mediantypes, 3)) + { + State.median.type = (type == 0 ? road_node::median_type::gap : type == 2 ? road_node::median_type::island : road_node::median_type::painted); + changed = true; + } + if (number("Apart at the start [m]", State.median.width[0], 0.5f, "%.2f")) + changed = true; + if (number("Apart at the end [m]", State.median.width[1], 0.5f, "%.2f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "How far apart the two directions are led at each end of the piece; 0 at one end and more at the other makes them part along it.\n" + "Set for each piece on its own. The pieces next to it follow it where they meet, and so do the junctions.\n" + "A road with its directions apart can be carried on as two one-way roads: start the Build tool\n" + "on the middle of one of its roadways, at an end of the road with nothing attached."); + if (State.median.type == road_node::median_type::island) + { + if (material("Island material", tool.mediantext, sizeof(tool.mediantext), State.median.material)) + changed = true; + ImGui::TextDisabled("The island is as high as the kerbs; with a kerb material set it gets kerbs of its own."); + } + } + + // traffic + if (number("Traffic share, 100: like any other", State.weight, 10.0f, "%.0f")) + { + State.weight = std::clamp(State.weight, 0.0f, 10000.0f); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "How willing the drivers are to take this road when they leave a junction, next to the other roads they can take there.\n" + "With 100 for the main road and 20 for a dirt road leaving it, one vehicle in six turns into the dirt road.\n" + "0: nobody takes it, unless there's no other way."); + if (ImGui::Checkbox("Part of a roundabout", &State.roundabout)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "A junction made on this road gives the right of way to the traffic going along it,\nand tells the traffic of the road joining it to give way."); + // single lanes, and the rules for changing between them if (ImGui::CollapsingHeader("Lanes and lane changes", ImGuiTreeNodeFlags_DefaultOpen)) { @@ -1737,10 +2096,27 @@ void editor_mode::render_road_window() }; 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 the side of a road to make a junction there,"); - ImGui::TextDisabled("or on a junction to give it another road."); + if (ImGui::Checkbox("Roundabout", &tool.ring)) + road_cancel(); + if (tool.ring) + { + ImGui::TextDisabled("LMB: put a roundabout around the cursor. It's a one-way road going around,"); + ImGui::TextDisabled("with the lanes set below as the lanes along the axis."); + ImGui::TextDisabled("Then untick this and lead the roads out of its side, or into it."); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Radius of the middle of its road [m]", &tool.ringradius, 1.0f, 5.0f, "%.1f")) + tool.ringradius = std::clamp(tool.ringradius, 8.0f, 200.0f); + render_road_material("Island in the middle", tool.islandtext, sizeof(tool.islandtext), tool.island); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "What the middle is covered with; it's raised on a kerb as high as the kerbs of the road.\nnone: the middle is left as it is."); + } + else + { + 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 the side of a road to make a junction there,"); + ImGui::TextDisabled("or on a junction to give it another road."); + } if (tool.chain) { if (ImGui::Button("Finish (Esc)")) @@ -1769,7 +2145,8 @@ void editor_mode::render_road_window() ImGui::RadioButton("Spawn point", &tool.placekind, 1); ImGui::SameLine(); ImGui::RadioButton("Removal point", &tool.placekind, 2); - tool.placekind = std::clamp(tool.placekind, 0, 2); + ImGui::RadioButton("Pedestrian crossing", &tool.placekind, 3); + tool.placekind = std::clamp(tool.placekind, 0, 3); switch (tool.placekind) { case 0: @@ -1780,6 +2157,9 @@ void editor_mode::render_road_window() ImGui::TextDisabled("LMB on a lane. The vehicles of the set appear there one by one"); ImGui::TextDisabled("and drive on the way the lane goes."); break; + case 3: + ImGui::TextDisabled("LMB on a road: stripes are painted across it there."); + break; default: ImGui::TextDisabled("LMB on a lane. The vehicles which get there are taken off the road;"); ImGui::TextDisabled("put one where the road leaves the scenery."); @@ -1986,7 +2366,7 @@ void editor_mode::draw_road_overlay() const glm::dvec3 across{tangent.z, 0.0, -tangent.x}; if (glm::length(across) > 1e-6) across = glm::normalize(across); - auto const halfwidth{0.5 * State.width() * State.scale(t)}; + auto const halfwidth{0.5 * State.span(t)}; glm::dvec3 const edges[2]{position + across * halfwidth, position - across * halfwidth}; if (i > 0) { @@ -2074,6 +2454,13 @@ void editor_mode::draw_road_overlay() const drawlabel(State.position, Color, "removal"); break; } + case roadpoint_node::kind_type::crosswalk: + { + drawpoint(State.position, Color, 11.0f, false); + if (labelled) + drawlabel(State.position, Color, "pedestrians"); + break; + } } }; for (auto const *point : simulation::Roadpoints.sequence()) @@ -2082,7 +2469,11 @@ void editor_mode::draw_road_overlay() const continue; auto const kind{point->definition().kind}; drawmarker(point->definition(), point, - point == tool.marker ? kSelectedColor : kind == roadpoint_node::kind_type::spawn ? kSpawnColor : kind == roadpoint_node::kind_type::despawn ? kDespawnColor : kCrossingColor); + point == tool.marker ? kSelectedColor : + kind == roadpoint_node::kind_type::spawn ? kSpawnColor : + kind == roadpoint_node::kind_type::despawn ? kDespawnColor : + kind == roadpoint_node::kind_type::crosswalk ? kCrosswalkColor : + kCrossingColor); if (point == tool.marker) drawpoint(point->definition().position, kSelectedColor, 15.0f, false); } @@ -2095,10 +2486,29 @@ void editor_mode::draw_road_overlay() const 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); + // the roads are numbered the way the window lists them + auto const &arms{tool.junction->definition().arms}; + for (std::size_t arm = 0; arm < arms.size(); ++arm) + drawlabel(arms[arm].position, kSelectedColor, std::to_string(arm + 1).c_str()); + } std::string hint; - if (tool.tool == 1) + if (tool.tool == 1 && tool.ring) + { + // the road going around, as wide as its lanes + auto layout{tool.settings}; + layout.backward = 0; + layout.forward = std::max(1, layout.forward); + layout.lanes.clear(); + layout.normalize(); + auto const centre{tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}}; + drawring(centre, tool.ringradius + 0.5 * layout.width(), kPreviewColor); + drawring(centre, std::max(0.0, tool.ringradius - 0.5 * layout.width()), kPreviewColor); + hint = "LMB: put a roundabout here"; + } + else if (tool.tool == 1) { if (tool.chain) { diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index 339ff00a..1f483e55 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -139,6 +139,36 @@ road_node::state part(road_node::state const &State, double const From, double c auto result{State}; result.axis = subpath(State.axis, From, To); result.taper = {static_cast(State.scale(From)), static_cast(State.scale(To))}; + if (State.divided()) + result.median.width = {static_cast(State.median_width(From)), static_cast(State.median_width(To))}; + for (std::size_t side = 0; side < result.banks.size(); ++side) + { + auto const &bank{State.banks[side]}; + result.banks[side].width = {glm::mix(bank.width[0], bank.width[1], static_cast(From)), glm::mix(bank.width[0], bank.width[1], static_cast(To))}; + result.banks[side].drop = {glm::mix(bank.drop[0], bank.drop[1], static_cast(From)), glm::mix(bank.drop[0], bank.drop[1], static_cast(To))}; + } + return result; +} + +// points of an end of a road something can be joined to: the end of its axis, and where it keeps its two directions apart, +// the middle of each of its roadways +std::vector end_points(road_node::state const &State, bool const Atend) +{ + std::vector result{axis_end(State, Atend)}; + auto const t{Atend ? 1.0 : 0.0}; + auto const gap{State.median_width(t)}; + if (gap <= 0.0) + return result; + auto const tangent{State.tangent(t)}; + glm::dvec3 left{tangent.z, 0.0, -tangent.x}; + if (glm::length(left) < 1e-9) + return result; + left = glm::normalize(left); + auto const halfwidth{0.5 * State.width()}; + auto const divide{State.divide()}; + // lanes going against the axis are on its left + result.emplace_back(result.front() + left * (0.5 * (halfwidth + divide) * State.scale(t) + 0.5 * gap)); + result.emplace_back(result.front() + left * (0.5 * (divide - halfwidth) * State.scale(t) - 0.5 * gap)); return result; } @@ -284,6 +314,7 @@ bool cut_arm(road_cut const &Cut, junction_node::arm_data &Arm) // lanes heading for the cut lead into the junction Arm.incoming = (Cut.keepstart ? state.forward : state.backward); Arm.outgoing = (Cut.keepstart ? state.backward : state.forward); + Arm.median = static_cast(state.median_width(Cut.t)); return editor_road::lane_width(state, Cut.t, Arm.width); } @@ -303,6 +334,9 @@ void dress(junction_node::state &Junction, road_node::state const &Road) // one kind of side goes around the whole junction; the right side of the road is the one facing the corner a road leaves by Junction.side = (Road.sides[1].type != road_node::side_type::none ? Road.sides[1] : Road.sides[0]); Junction.kerbheight = Road.kerbheight; + Junction.kerbs = (Road.kerbs[0] || Road.kerbs[1]); + Junction.kerbwidth = Road.kerbwidth; + Junction.kerbmaterial = Road.kerbmaterial; Junction.slope = Road.slope; Junction.friction = Road.friction; Junction.sounddistance = Road.sounddistance; @@ -374,6 +408,14 @@ road_node::state flipped(road_node::state const &State) change = ((change & road_node::change_toright) != 0 ? road_node::change_toleft : 0) | ((change & road_node::change_toleft) != 0 ? road_node::change_toright : 0); std::swap(result.sides[0], result.sides[1]); std::swap(result.taper[0], result.taper[1]); + std::swap(result.median.width[0], result.median.width[1]); + std::swap(result.kerbs[0], result.kerbs[1]); + std::swap(result.banks[0], result.banks[1]); + for (auto &bank : result.banks) + { + std::swap(bank.width[0], bank.width[1]); + std::swap(bank.drop[0], bank.drop[1]); + } return result; } @@ -516,8 +558,27 @@ std::vector editor_road::neighbours(road_node const &Road) if (Road.m_editorremoved) return result; for (auto const atend : {false, true}) + { for (auto *road : roads_at(axis_end(Road.definition(), atend), &Road)) add_unique(result, road); + // a road which keeps its directions apart can have a road joined to each of its roadways, and be one for the road joined + auto const points{end_points(Road.definition(), atend)}; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road == &Road || road->m_editorremoved) + continue; + for (auto const otheratend : {false, true}) + { + auto const otherpoints{end_points(road->definition(), otheratend)}; + if (points.size() == 1 && otherpoints.size() == 1) + continue; + for (auto const &point : points) + for (auto const &otherpoint : otherpoints) + if (glm::distance(point, otherpoint) < kSamePoint) + add_unique(result, road); + } + } + } return result; } @@ -886,7 +947,7 @@ bool editor_road::nearest_lane(glm::dvec3 const &Point, double const Reach, glm: auto const axis{curve_points(state.axis)}; // the axis is no longer than the lines joining its points, which is good enough to tell the road is nowhere near if (glm::distance(road->location(), Point) > - 0.5 * (glm::distance(axis[0], axis[1]) + glm::distance(axis[1], axis[2]) + glm::distance(axis[2], axis[3])) + 0.5 * state.width() * std::max(state.taper[0], state.taper[1]) + Reach + 10.0) + 0.5 * (glm::distance(axis[0], axis[1]) + glm::distance(axis[1], axis[2]) + glm::distance(axis[2], axis[3])) + 0.5 * std::max(state.span(0.0), state.span(1.0)) + Reach + 10.0) continue; for (auto const *lane : road->tracks()) { @@ -1040,6 +1101,23 @@ bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string merged.axis.points[segment_data::point::control2] = control2; // the road is as wide at its far ends as it was merged.taper[1] = static_cast(after.taper[1] * after.width() / before.width()); + if (after.divided() || before.divided()) + { + if (false == before.divided()) + { + merged.median = after.median; + merged.median.width[0] = 0.f; + } + merged.median.width[1] = (after.divided() ? after.median.width[1] : 0.f); + } + for (std::size_t side = 0; side < merged.banks.size(); ++side) + { + if (merged.banks[side].set && after.banks[side].set) + { + merged.banks[side].width[1] = after.banks[side].width[1]; + merged.banks[side].drop[1] = after.banks[side].drop[1]; + } + } remember(Record, kept.road); remove(std::vector{dropped.road}); Record.roads_removed.emplace_back(dropped.road); @@ -1086,6 +1164,19 @@ road_node *editor_road::split(road_node &Road, double const T) auto const scale{static_cast(Road.definition().scale(T))}; first.taper[1] = scale; second.taper[0] = scale; + // and so is what's between its directions, and what leads from it to the ground + if (Road.definition().divided()) + { + auto const gap{static_cast(Road.definition().median_width(T))}; + first.median.width[1] = gap; + second.median.width[0] = gap; + } + for (std::size_t side = 0; side < first.banks.size(); ++side) + { + auto const &bank{Road.definition().banks[side]}; + first.banks[side].width[1] = second.banks[side].width[0] = glm::mix(bank.width[0], bank.width[1], static_cast(T)); + first.banks[side].drop[1] = second.banks[side].drop[0] = glm::mix(bank.drop[0], bank.drop[1], static_cast(T)); + } apply(std::vector>{{&Road, first}}); auto const created{create(std::vector{second})}; return created.empty() ? nullptr : created.front(); @@ -1121,7 +1212,7 @@ road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin) if (road == nullptr || road->m_editorremoved) continue; auto const &state{road->definition()}; - auto const reach{0.5 * state.width() * std::max(state.taper[0], state.taper[1]) + std::max(state.sides[0].width, state.sides[1].width) + Margin}; + auto const reach{0.5 * std::max(state.span(0.0), state.span(1.0)) + std::max(state.sides[0].width, state.sides[1].width) + Margin}; // the axis is no longer than the lines joining its points, which is good enough to tell the road is nowhere near auto const points{curve_points(state.axis)}; if (glm::distance(road->location(), Point) > 0.5 * (glm::distance(points[0], points[1]) + glm::distance(points[1], points[2]) + glm::distance(points[2], points[3])) + reach + 10.0) @@ -1184,6 +1275,38 @@ editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double con result.position = position; result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0)); } + // a road which keeps its two directions apart can be carried on as two roads, one for each of its roadways + for (auto const atend : {false, true}) + { + auto const points{end_points(state, atend)}; + float lanewidth{0.f}; + if (points.size() < 3 || state.median_width(atend ? 1.0 : 0.0) < 0.5 || false == lane_width(state, atend ? 1.0 : 0.0, lanewidth)) + continue; + // the whole end has to be loose + if (false == roads_at(points[0], road).empty() || false == junctions_at(points[0]).empty()) + continue; + for (int half = 1; half <= 2; ++half) + { + auto const &position{points[half]}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty()) + continue; + best = distance; + result = loose_end{}; + result.road = road; + result.atend = atend; + result.position = position; + result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0)); + // the first of the points is the roadway of the lanes going against the axis + result.half = (half == 1 ? -1 : 1); + auto const lanes{half == 1 ? state.backward : state.forward}; + // lanes which leave the road there go along the axis of the road carrying them on, the ones which come to it go against that axis + auto const leaving{(half == 2) == atend}; + result.forward = (leaving ? lanes : 0); + result.backward = (leaving ? 0 : lanes); + result.width = lanewidth; + } + } } if (false == Junctions) return result; @@ -1504,6 +1627,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) auto const leaving{atend ? layout.backward : layout.forward}; float lanewidth{0.f}; auto const evenlanes{lane_width(layout, atend ? 1.0 : 0.0, lanewidth)}; + auto const gap{static_cast(layout.median_width(atend ? 1.0 : 0.0))}; char const *unevenlanes{" differ in width, and a junction takes roads with lanes of the same width. "}; std::string reason; @@ -1516,7 +1640,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) auto *junction{entry.first}; auto state{junction->definition()}; auto &arm{state.arms[entry.second]}; - if (arm.incoming == arriving && arm.outgoing == leaving && std::abs(arm.width - lanewidth) < 0.001f) + if (arm.incoming == arriving && arm.outgoing == leaving && std::abs(arm.width - lanewidth) < 0.001f && std::abs(arm.median - gap) < 0.001f) continue; if (false == evenlanes) { @@ -1531,6 +1655,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) arm.incoming = arriving; arm.outgoing = leaving; arm.width = lanewidth; + arm.median = gap; remember(Record, junction); apply(std::vector>{{junction, state}}); } @@ -1550,7 +1675,8 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) { // the same lanes: the neighbour is made as wide as the piece where they meet, and gets to that width along its length auto const width{layout.width() * layout.taper[atend ? 1 : 0]}; - if (std::abs(width - otherlayout.width() * otherlayout.taper[otheratend ? 1 : 0]) < 0.001) + auto const othergap{static_cast(otherlayout.median_width(otheratend ? 1.0 : 0.0))}; + if (std::abs(width - otherlayout.width() * otherlayout.taper[otheratend ? 1 : 0]) < 0.001 && std::abs(gap - othergap) < 0.001f) continue; if (false == can_edit(*other, &reason)) { @@ -1559,6 +1685,17 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) } auto state{otherlayout}; state.taper[otheratend ? 1 : 0] = std::clamp(static_cast(width / otherlayout.width()), 0.25f, 4.f); + // and it leads its two directions as far apart as the piece has them there + if (std::abs(gap - othergap) >= 0.001f) + { + if (false == otherlayout.divided()) + { + state.median.type = layout.median.type; + state.median.material = layout.median.material; + state.median.width = {0.f, 0.f}; + } + state.median.width[otheratend ? 1 : 0] = gap; + } remember(Record, other); apply(std::vector>{{other, state}}); continue; @@ -1618,6 +1755,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) arms[1].incoming = arriving; arms[1].outgoing = leaving; arms[1].width = lanewidth; + arms[1].median = gap; junction.arms.assign(std::begin(arms), std::end(arms)); junction.centre = 0.5 * (arms[0].position + arms[1].position); dress(junction, layout); @@ -1671,7 +1809,7 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const Error = "Lanes of \"" + Road.name() + "\" differ in width, and a junction takes roads with lanes of the same width"; return false; } - auto const halfwidth{0.5 * (layout.forward + layout.backward) * lanewidth}; + auto const halfwidth{0.5 * (layout.forward + layout.backward) * lanewidth + 0.5 * layout.median_width(T)}; auto const branchhalfwidth{0.5 * (Incoming + Outgoing) * Width}; auto const reach{edge_reach(halfwidth, branchhalfwidth, angle) + kCornerRoom}; auto const branchreach{edge_reach(branchhalfwidth, halfwidth, angle) + kCornerRoom}; @@ -1746,6 +1884,13 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const junction.arms.emplace_back(arm); Result.arm = junction.arms.size() - 1; dress(junction, layout); + if (layout.roundabout) + { + // the traffic going around goes ahead of the traffic joining it + for (auto &entry : junction.arms) + entry.priority = junction_node::right_of_way::priority; + junction.arms.back().priority = junction_node::right_of_way::yield; + } junction.normalize(); return true; } @@ -1826,6 +1971,60 @@ junction_node *editor_road::carry_out(branch const &Branch, record &Record) return junctions.front(); } +std::vector editor_road::roundabout(glm::dvec3 const &Centre, double const Radius, road_node::state const &Layout, std::string const &Island) +{ + std::vector pieces; + auto layout{Layout}; + // a single direction, going around with the middle on its left + if (layout.forward < 1) + layout.forward = std::max(1, layout.backward); + layout.backward = 0; + layout.lanes.clear(); + layout.changes.clear(); + layout.taper = {1.f, 1.f}; + layout.median.width = {0.f, 0.f}; + layout.roundabout = true; + layout.normalize(); + auto const halfwidth{0.5 * layout.width()}; + if (Radius - halfwidth < 1.0) + return pieces; + if (false == Island.empty() && Island != "none") + { + // the island takes all that's inside the road + layout.sides[0].type = road_node::side_type::sidewalk; + layout.sides[0].width = static_cast(Radius - halfwidth); + layout.sides[0].material = Island; + } + else + { + layout.sides[0] = road_node::side_data{}; + } + layout.banks[0] = road_node::bank_data{}; + // arcs short enough for a curve of this kind to pass for a circle + int const count{Radius > 40.0 ? 8 : 4}; + auto const angle{6.283185307179586 / count}; + auto const reach{4.0 / 3.0 * std::tan(0.25 * angle) * Radius}; + auto const point = [&](double const Angle) { return Centre + glm::dvec3{std::cos(Angle), 0.0, std::sin(Angle)} * Radius; }; + // the way the road goes at a point, with the angles going down + auto const heading = [](double const Angle) { return glm::dvec3{std::sin(Angle), 0.0, -std::cos(Angle)}; }; + for (int index = 0; index < count; ++index) + { + auto const from{-angle * index}; + auto const to{-angle * (index + 1)}; + auto state{layout}; + state.axis = segment_data{}; + state.axis.points[segment_data::point::start] = point(from); + state.axis.points[segment_data::point::control1] = heading(from) * reach; + state.axis.points[segment_data::point::control2] = -heading(to) * reach; + state.axis.points[segment_data::point::end] = point(to); + state.axis.radius = static_cast(Radius); + pieces.emplace_back(state); + } + // the last piece ends exactly where the first one starts + pieces.back().axis.points[segment_data::point::end] = pieces.front().axis.points[segment_data::point::start]; + return pieces; +} + std::vector const &editor_road::materials() { static std::vector names; diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp index a05a05ac..df71db18 100644 --- a/editor/editorRoad.hpp +++ b/editor/editorRoad.hpp @@ -30,7 +30,10 @@ class editor_road 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 a road would continue in - // of a junction arm: lanes of a road leading away from it + // of a road which keeps its two directions apart: the roadway of one of them, 1 for the lanes going along the axis, -1 for the ones going against it. + // 0: the end of the whole road + int half{0}; + // of a junction arm, or of a single roadway: lanes of a road leading away from it int forward{0}; int backward{0}; float width{0.f}; @@ -164,6 +167,10 @@ class editor_road 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); + // lays out the pieces of a roundabout: a one-way road going around specified point, to the left of the traffic. + // Radius: of the axis of that road; Layout: what the road is like. the lanes it has along its axis are the ones the roundabout gets. + // Island: what the middle is covered with, "none" to leave the ground showing + static std::vector roundabout(glm::dvec3 const &Centre, double const Radius, road_node::state const &Layout, std::string const &Island); // names of materials and images present in the texture folder, to pick from static std::vector const &materials(); diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index cc54a2a2..7cdbc3d7 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -251,6 +251,7 @@ TAnimModel::TAnimModel( scene::node_data const &Nodedata ) : basic_node( Nodedat bool TAnimModel::Init(std::string const &asName, std::string const &asReplacableTexture) { + m_skintoken = asReplacableTexture; if( asReplacableTexture.substr( 0, 1 ) == "*" ) { // od gwiazdki zaczynają się teksty na wyświetlaczach asText = asReplacableTexture.substr( 1, asReplacableTexture.length() - 1 ); // zapamiętanie tekstu @@ -806,13 +807,18 @@ TAnimModel::export_as_text_( std::ostream &Output ) const { } Output << modelfile << ' '; // texture - auto texturefile { ( - m_materialdata.replacable_skins[ 1 ] != null_handle ? - GfxRenderer->Material( m_materialdata.replacable_skins[ 1 ] )->GetName() : - "none" ) }; - if( texturefile.find( paths::textures ) == 0 ) { - // don't include 'textures/' in the path - texturefile.erase( 0, std::string{ paths::textures }.size() ); + // the skin goes out the way it came in. the materials it was turned into can't give it back: they don't tell + // a set of skins from a single one, a text for a display from no skin at all, or what a texture was to be generated from + auto texturefile { m_skintoken }; + if( texturefile.empty() ) { + texturefile = ( + m_materialdata.replacable_skins[ 1 ] != null_handle ? + GfxRenderer->Material( m_materialdata.replacable_skins[ 1 ] )->GetName() : + "none" ); + if( texturefile.find( paths::textures ) == 0 ) { + // don't include 'textures/' in the path + texturefile.erase( 0, std::string{ paths::textures }.size() ); + } } if( contains( texturefile, ' ' ) ) { Output << "\"" << texturefile << "\"" << ' '; @@ -827,6 +833,23 @@ TAnimModel::export_as_text_( std::ostream &Output ) const { Output << lsLights[ lightidx ] << ' '; } } + // colors given to the lights; goes right after the lights, the way it's read + if( std::any_of( std::begin( m_lightcolors ), std::begin( m_lightcolors ) + iNumLights, []( glm::vec3 const &Color ) { return Color.r >= 0.f; } ) ) { + Output << "lightcolors "; + for( int lightidx = 0; lightidx < iNumLights; ++lightidx ) { + auto const &color { m_lightcolors[ lightidx ] }; + if( color.r < 0.f ) { + // the light was left with the color the model gives it + Output << "-1 "; + continue; + } + auto const component = []( float const Value ) { return std::clamp( static_cast( std::lround( Value * 255.f ) ), 0, 255 ); }; + Output + << std::hex << std::setfill( '0' ) << std::setw( 6 ) + << ( component( color.r ) << 16 | component( color.g ) << 8 | component( color.b ) ) + << std::dec << std::setfill( ' ' ) << ' '; + } + } // potential light transition switch if( false == m_transition ) { Output << "notransition" << ' '; diff --git a/model/AnimModel.h b/model/AnimModel.h index 3c4b9f11..8116f1bb 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -201,6 +201,7 @@ public: material_data m_materialdata; std::string asText; // tekst dla wyświetlacza znakowego + std::string m_skintoken; // replacable skin the way the definition of the node gave it, kept for the export // TODO: wrap into a light state struct, remove fixed element count int iNumLights { 0 }; std::array LightsOn {}; // Ra: te wskaźniki powinny być w ramach TModel3d diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 18e3c701..a5d5ad29 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -119,7 +119,8 @@ state_manager::update( double const Deltatime, int Iterationcount ) { TAnimModel::AnimUpdate( totaltime ); // wykonanie zakolejkowanych animacji simulation::Powergrid.update( totaltime ); - // level crossings and the points road vehicles appear at; ahead of the vehicles, so the drivers act on what these decide + // road junctions, level crossings and the points road vehicles appear at; ahead of the vehicles, so the drivers act on what these decide + simulation::Junctions.update( totaltime ); simulation::Roadpoints.update( totaltime ); simulation::Vehicles.update( Deltatime, Iterationcount ); } diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 16013c67..5a6f8ba4 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -734,6 +734,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch { "crossing", scene::layer_item::track }, { "spawn", scene::layer_item::track }, { "despawn", scene::layer_item::track }, + { "crosswalk", scene::layer_item::track }, { "traction", scene::layer_item::traction }, { "tractionpowersource", scene::layer_item::powersource }, { "model", scene::layer_item::model }, @@ -1509,7 +1510,7 @@ std::pair state_serializer::preview_include(std::string const &Directi // 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" }, { "junction", "endjunction" }, { "crossing", "endcrossing" }, { "spawn", "endspawn" }, { "despawn", "enddespawn" }, - { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, + { "crosswalk", "endcrosswalk" }, { "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/vehicle/Driver.cpp b/vehicle/Driver.cpp index 9f1b2586..b6429a18 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -648,7 +648,7 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) routewanted = 1 + std::floor( Random( static_cast( pTrack->RouteCount() ) - 0.001 ) ); } */ - auto const routewanted { 1 + std::floor( Random( static_cast( pTrack->RouteCount() ) - 0.001 ) ) }; + auto const routewanted { pTrack->RouteDraw() }; sSpeedTable[iLast].iFlags |= (pTrack->CrossSegment(fLastDir < 0 ? tLast->iPrevDirection : tLast->iNextDirection, /* diff --git a/world/Road.cpp b/world/Road.cpp index 22c65289..9a804933 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -10,7 +10,9 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/Road.h" +#include "world/RoadOrder.h" #include "world/Track.h" +#include "vehicle/DynObj.h" #include "simulation/simulation.h" #include "scene/scene.h" #include "rendering/renderer.h" @@ -196,6 +198,9 @@ struct profile_point double height; // above the surface float texture; // texture coordinate across the road double shift{0.0}; // further distance from the axis, which the taper leaves alone + double spread{0.0}; // how much of the gap between the two directions of the road it's moved by: 0.5 for their left edge, -0.5 for the right one + double grow{0.0}; // what the shift changes by along the axis, evenly + double sink{0.0}; // what the height changes by along the axis, evenly }; // how far the taper of a road has got at specified part of its length. it starts and ends level, so the pieces of a road @@ -207,10 +212,13 @@ double taper_progress(double const Fraction) } // stretches provided profile along a part of the axis. the profile points go left to right, each two neighbours make a band of triangles. -// Taper: what the offsets of the profile are multiplied by at the start and at the end of the axis +// Road: the road it's done for, which says how the offsets of the profile change along the axis void loft(std::vector &Output, road_axis const &Axis, std::vector const &Profile, double const From, double const To, double const Step, double const Texturelength, - std::array const &Taper) + road_node::state const &Road) { + auto const &Taper{Road.taper}; + auto const &median{Road.median.width}; + auto const divided{Road.divided()}; if (Profile.size() < 2 || To <= From || Step <= 0.0) { return; @@ -230,10 +238,13 @@ void loft(std::vector &Output, road_axis const &Axis, std::vector< { auto const station{From + (To - From) * section / count}; auto const frame{Axis.at(station)}; - auto const scale{Taper[0] == Taper[1] ? static_cast(Taper[0]) : glm::mix(static_cast(Taper[0]), static_cast(Taper[1]), taper_progress(station / Axis.length()))}; + auto const fraction{Axis.length() > 0.0 ? station / Axis.length() : 0.0}; + auto const scale{Taper[0] == Taper[1] ? static_cast(Taper[0]) : glm::mix(static_cast(Taper[0]), static_cast(Taper[1]), taper_progress(fraction))}; + auto const gap{false == divided ? 0.0 : glm::mix(static_cast(median[0]), static_cast(median[1]), taper_progress(fraction))}; for (std::size_t idx = 0; idx < pointcount; ++idx) { - current[idx] = frame.position + frame.left * (Profile[idx].offset * scale + Profile[idx].shift) + up * Profile[idx].height; + auto const &point{Profile[idx]}; + current[idx] = frame.position + frame.left * (point.offset * scale + point.shift + point.spread * gap + point.grow * fraction) + up * (point.height + point.sink * fraction); } if (section > 0) { @@ -291,6 +302,35 @@ void road_node::state::normalize() forward = 1; } lanewidth = std::max(1.f, lanewidth); + weight = std::clamp(weight, 0.f, 10000.f); + for (auto &width : median.width) + { + width = std::clamp(width, 0.f, 100.f); + } + if (median.material.empty()) + { + median.material = "none"; + } + kerbwidth = std::clamp(kerbwidth, 0.05f, 2.f); + if (kerbmaterial.empty()) + { + kerbmaterial = "none"; + } + if (bankmaterial.empty()) + { + bankmaterial = "none"; + } + for (auto &bank : banks) + { + for (auto &width : bank.width) + { + width = std::clamp(width, 0.f, 100.f); + } + for (auto &drop : bank.drop) + { + drop = std::clamp(drop, -100.f, 100.f); + } + } for (auto &end : taper) { end = std::clamp(end, 0.25f, 4.f); @@ -405,13 +445,44 @@ double road_node::state::scale(double const T) const return glm::mix(static_cast(taper[0]), static_cast(taper[1]), taper_progress(T)); } +// true if the two directions of the road are kept apart anywhere along it +bool road_node::state::divided() const +{ + return forward > 0 && backward > 0 && (median.width[0] > 0.f || median.width[1] > 0.f); +} + +// how far apart the two directions are at specified value of the curve parameter +double road_node::state::median_width(double const T) const +{ + return divided() ? glm::mix(static_cast(median.width[0]), static_cast(median.width[1]), taper_progress(T)) : 0.0; +} + +// distance from the axis of the line the two directions meet at, on a road which doesn't keep them apart +double road_node::state::divide() const +{ + auto offset{0.5 * width()}; + for (std::size_t idx = 0; idx < static_cast(backward) && idx < lanes.size(); ++idx) + { + offset -= lanes[idx].width; + } + return offset; +} + +// width of the whole roadway at specified value of the curve parameter +double road_node::state::span(double const T) const +{ + return width() * scale(T) + median_width(T); +} + // shape of the middle of a lane, laid out in the direction of travel segment_data road_node::state::lane_path(std::size_t const Lane) const { road_axis const centre{axis}; - // on a road which gets wider or narrower the lane moves away from the axis or towards it along the way - auto const beginoffset{lane_offset(Lane) * taper[0]}; - auto const endoffset{lane_offset(Lane) * taper[1]}; + // on a road which gets wider or narrower the lane moves away from the axis or towards it along the way, + // and so it does where the two directions are led apart + auto const side{Lane < static_cast(backward) ? 0.5 : -0.5}; + auto const beginoffset{lane_offset(Lane) * taper[0] + side * median_width(0.0)}; + auto const endoffset{lane_offset(Lane) * taper[1] + side * median_width(1.0)}; auto const begin{centre.frame(0.0)}; auto const end{centre.frame(1.0)}; @@ -608,6 +679,69 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) { m_state.environment = Input.getToken(); } + else if (token == "weight") + { + Input.getTokens(); + Input >> m_state.weight; + } + else if (token == "median") + { + // median + auto const type{Input.getToken()}; + m_state.median.type = (type == "gap" ? median_type::gap : type == "island" ? median_type::island : median_type::painted); + if (type != "gap" && type != "island" && type != "painted") + { + ErrorLog("Bad road: unknown kind of median \"" + type + "\" defined for " + label); + } + Input.getTokens(2); + Input >> m_state.median.width[0] >> m_state.median.width[1]; + m_state.median.material = Input.getToken(); + replace_slashes(m_state.median.material); + } + else if (token == "kerbs") + { + // kerbs + auto const where{Input.getToken()}; + m_state.kerbs = {where == "left" || where == "both", where == "right" || where == "both"}; + if (where != "left" && where != "right" && where != "both") + { + ErrorLog("Bad road: unknown side \"" + where + "\" defined for the kerbs of " + label); + } + Input.getTokens(); + Input >> m_state.kerbwidth; + m_state.kerbmaterial = Input.getToken(); + replace_slashes(m_state.kerbmaterial); + } + else if (token == "bank") + { + // bank + auto const where{Input.getToken()}; + bank_data bank; + bank.set = true; + Input.getTokens(4); + Input >> bank.width[0] >> bank.drop[0] >> bank.width[1] >> bank.drop[1]; + if (where == "left" || where == "both") + { + m_state.banks[0] = bank; + } + if (where == "right" || where == "both") + { + m_state.banks[1] = bank; + } + if (where != "left" && where != "right" && where != "both") + { + ErrorLog("Bad road: unknown side \"" + where + "\" defined for a bank of " + label); + } + } + else if (token == "bankmaterial") + { + m_state.bankmaterial = Input.getToken(); + replace_slashes(m_state.bankmaterial); + } + else if (token == "roundabout") + { + m_state.roundabout = true; + } else { ErrorLog("Bad road: unknown property: \"" + token + "\" defined for " + label); @@ -805,49 +939,147 @@ std::vector road_node::create_shapes() const auto const step{axis.step()}; auto const halfwidth{0.5 * road.width()}; double const texturelength{road.texturelength}; + // where the two directions are kept apart there's a roadway for each, moved away from the axis by half of what's between them + auto const divided{road.divided()}; + auto const divide{road.divide()}; + auto const widest{static_cast(std::max(road.median.width[0], road.median.width[1]))}; + auto const height{static_cast(road.kerbheight)}; + auto const usable = [](std::string const &Material) { return false == Material.empty() && Material != "none"; }; // surface. the image is centered on the axis, and repeated if the road is wider if (road.surface != "none") { auto const tile{static_cast(texture_ratio(road.surface)) * texturelength}; - loft(vertices(road.surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast(0.5 - halfwidth / tile)}}, 0.0, length, step, texturelength, road.taper); - } - - // sides - for (int side = 0; side < 2; ++side) - { - auto const &data{road.sides[side]}; - if (data.type == side_type::none || data.width <= 0.f || data.material.empty() || data.material == "none") + auto const corner = [&](double const Offset, double const Spread) { return profile_point{Offset, 0.0, static_cast(0.5 + (Offset + Spread * widest) / tile), 0.0, Spread}; }; + auto &surface{vertices(road.surface, lighting_data{})}; + if (false == divided) { - continue; - } - auto const direction{side == 0 ? 1.0 : -1.0}; - // corners of the profile, from the edge of the surface outwards - std::vector profile; - if (data.type == side_type::shoulder) - { - // the image is laid out like the one of the legacy roads: the edge of the surface on its right, the bank on its left half - profile.push_back({direction * halfwidth, 0.0, 1.f}); - profile.push_back({direction * halfwidth, 0.0, 0.5f, direction * data.width}); - if (road.slope.x > 0.f) - { - profile.push_back({direction * halfwidth, -road.slope.y, 0.f, direction * (data.width + road.slope.x)}); - } + loft(surface, axis, {corner(halfwidth, 0.0), corner(-halfwidth, 0.0)}, 0.0, length, step, texturelength, road); } else + { + loft(surface, axis, {corner(halfwidth, 0.5), corner(divide, 0.5)}, 0.0, length, step, texturelength, road); + if (road.median.type == median_type::painted) + { + // the surface carries on between the roadways + loft(surface, axis, {corner(divide, 0.5), corner(divide, -0.5)}, 0.0, length, step, texturelength, road); + } + loft(surface, axis, {corner(divide, -0.5), corner(-halfwidth, -0.5)}, 0.0, length, step, texturelength, road); + } + } + + // an island between the roadways: a kerb facing each of them, and what's between the kerbs + if (divided && road.median.type == median_type::island) + { + auto const kerbed{usable(road.kerbmaterial)}; + auto const covered{usable(road.median.material)}; + double const kerbwidth{kerbed ? road.kerbwidth : 0.0}; + if (kerbed) + { + auto const tile{static_cast(texture_ratio(road.kerbmaterial)) * texturelength}; + auto const foot{0.f}; + auto const top{static_cast(height / tile)}; + auto const inner{static_cast((height + kerbwidth) / tile)}; + auto &kerbs{vertices(road.kerbmaterial, lighting_data{})}; + loft(kerbs, axis, {{divide, 0.0, foot, 0.0, 0.5}, {divide, height, top, 0.0, 0.5}, {divide, height, inner, -kerbwidth, 0.5}}, 0.0, length, step, texturelength, road); + loft(kerbs, axis, {{divide, height, inner, kerbwidth, -0.5}, {divide, height, top, 0.0, -0.5}, {divide, 0.0, foot, 0.0, -0.5}}, 0.0, length, step, texturelength, road); + } + if (covered) + { + auto const tile{static_cast(texture_ratio(road.median.material)) * texturelength}; + std::vector profile; + if (false == kerbed) + { + // without kerbs of their own material the sides of the island are covered with what its top is + profile.push_back({divide, 0.0, 0.f, 0.0, 0.5}); + } + profile.push_back({divide, height, static_cast(height / tile), -kerbwidth, 0.5}); + profile.push_back({divide, height, static_cast((height + widest) / tile), kerbwidth, -0.5}); + if (false == kerbed) + { + profile.push_back({divide, 0.0, static_cast((2.0 * height + widest) / tile), 0.0, -0.5}); + } + loft(vertices(road.median.material, lighting_data{}), axis, profile, 0.0, length, step, texturelength, road); + } + } + + // sides: a kerb, then a shoulder or a sidewalk, then a bank leading to the ground; each of them optional + for (int side = 0; side < 2; ++side) + { + auto const &data{road.sides[side]}; + auto const direction{side == 0 ? 1.0 : -1.0}; + auto const sided{data.type != side_type::none && data.width > 0.f && usable(data.material)}; + auto const kerbed{road.kerbs[side] && usable(road.kerbmaterial)}; + // the profiles are given from the edge of the surface outwards, as distances from that edge + auto const emit = [&](std::string const &Material, std::vector Profile) { + for (auto &point : Profile) + { + point.offset = direction * halfwidth; + point.spread = (divided ? direction * 0.5 : 0.0); + point.shift *= direction; + point.grow *= direction; + } + if (side == 0) + { + // the loft takes the corners left to right + std::reverse(Profile.begin(), Profile.end()); + } + loft(vertices(Material, lighting_data{}), axis, Profile, 0.0, length, step, texturelength, road); + }; + double reach{0.0}; // how far from the edge of the surface the next thing starts + double level{0.0}; // and how high + if (kerbed) + { + auto const tile{static_cast(texture_ratio(road.kerbmaterial)) * texturelength}; + double const width{road.kerbwidth}; + std::vector kerb{{0.0, 0.0, 0.f, 0.0}, {0.0, height, static_cast(height / tile), 0.0}, {0.0, height, static_cast((height + width) / tile), width}}; + if (false == sided || data.type != side_type::sidewalk) + { + // with nothing raised behind it the kerb has a back + kerb.push_back({0.0, 0.0, static_cast((2.0 * height + width) / tile), width}); + } + emit(road.kerbmaterial, kerb); + reach = width; + } + if (sided && data.type == side_type::shoulder) + { + // the image is laid out like the one of the legacy roads: the edge of the surface on its right, the bank on its left half + std::vector profile{{0.0, 0.0, 1.f, reach}, {0.0, 0.0, 0.5f, reach + data.width}}; + reach += data.width; + auto const &bank{road.banks[side]}; + glm::dvec2 const start{bank.set ? bank.width[0] : road.slope.x, bank.set ? bank.drop[0] : road.slope.y}; + glm::dvec2 const end{bank.set ? bank.width[1] : road.slope.x, bank.set ? bank.drop[1] : road.slope.y}; + if (start.x > 0.0 || end.x > 0.0) + { + profile.push_back({0.0, -start.y, 0.f, reach + start.x, 0.0, end.x - start.x, start.y - end.y}); + } + emit(data.material, profile); + } + else if (sided) { // the image starts at the foot of the kerb, and is laid out in its own proportions auto const tile{static_cast(texture_ratio(data.material)) * texturelength}; - profile.push_back({direction * halfwidth, 0.0, 0.f}); - profile.push_back({direction * halfwidth, road.kerbheight, static_cast(road.kerbheight / tile)}); - profile.push_back({direction * halfwidth, road.kerbheight, static_cast((road.kerbheight + data.width) / tile), direction * data.width}); + std::vector profile; + if (false == kerbed) + { + profile.push_back({0.0, 0.0, 0.f, 0.0}); + } + profile.push_back({0.0, height, static_cast(height / tile), reach}); + profile.push_back({0.0, height, static_cast((height + data.width) / tile), reach + data.width}); + emit(data.material, profile); + reach += data.width; + level = height; } - if (side == 0) + // a bank next to something other than a shoulder is covered with a material of its own + auto const &bank{road.banks[side]}; + if ((false == sided || data.type != side_type::shoulder) && bank.set && usable(road.bankmaterial) && (bank.width[0] > 0.f || bank.width[1] > 0.f)) { - // the loft takes the corners left to right - std::reverse(profile.begin(), profile.end()); + auto const tile{static_cast(texture_ratio(road.bankmaterial)) * texturelength}; + profile_point foot{0.0, level - bank.drop[0], static_cast(std::max(bank.width[0], bank.width[1]) / tile), reach + bank.width[0]}; + foot.grow = static_cast(bank.width[1]) - bank.width[0]; + foot.sink = static_cast(bank.drop[0]) - bank.drop[1]; + emit(road.bankmaterial, {{0.0, level, 0.f, reach}, foot}); } - loft(vertices(data.material, lighting_data{}), axis, profile, 0.0, length, step, texturelength, road.taper); } // markings. these are painted with plain colour instead of an image @@ -858,18 +1090,21 @@ std::vector road_node::create_shapes() const paint.ambient = paint.diffuse; auto &lines{vertices("colored", paint)}; - // Offset: where the line runs on a road as wide as its lanes say, Shift: how far from there it's kept whatever the width - auto const solid = [&](double const Offset, double const Shift) { - loft(lines, axis, {{Offset, line_lift, 0.f, Shift + 0.5 * line_width}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width}}, 0.0, length, std::min(step, dash_step), texturelength, road.taper); + // Offset: where the line runs on a road as wide as its lanes say, Shift: how far from there it's kept whatever the width, + // Spread: which of the roadways it goes with, where the road has two + auto const solid = [&](double const Offset, double const Shift, double const Spread) { + std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread}}; + loft(lines, axis, line, 0.0, length, std::min(step, dash_step), texturelength, road); }; // the dashes are fitted so the gaps at both ends of the road are a half of the regular one, which makes them match the next piece - auto const dashed = [&](double const Offset, double const Shift) { + auto const dashed = [&](double const Offset, double const Shift, double const Spread) { auto const count{std::max(1, static_cast(std::lround(length / dash_period)))}; auto const period{length / count}; + std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread}}; for (int dash = 0; dash < count; ++dash) { auto const from{(dash + 1.0 / 3.0) * period}; - loft(lines, axis, {{Offset, line_lift, 0.f, Shift + 0.5 * line_width}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width}}, from, from + period / 3.0, dash_step, texturelength, road.taper); + loft(lines, axis, line, from, from + period / 3.0, dash_step, texturelength, road); } }; @@ -877,8 +1112,8 @@ std::vector road_node::create_shapes() const auto const inset{edge_inset + 0.5 * line_width}; if (halfwidth * std::min(road.taper[0], road.taper[1]) - inset > 0.5) { - solid(halfwidth, -inset); - solid(-halfwidth, inset); + solid(halfwidth, -inset, divided ? 0.5 : 0.0); + solid(-halfwidth, inset, divided ? -0.5 : 0.0); } // lines between the lanes. a lane can be left across a dashed line, or across a pair of lines if the dashed one is on its side auto offset{halfwidth}; @@ -887,23 +1122,31 @@ std::vector road_node::create_shapes() const offset -= road.lanes[boundary].width; auto const opposite{road.backward > 0 && boundary + 1 == static_cast(road.backward)}; auto const pair{0.5 * (line_width + line_spacing)}; + if (opposite && divided) + { + // each of the two roadways gets its edge line + solid(offset, inset, 0.5); + solid(offset, -inset, -0.5); + continue; + } + auto const spread{false == divided ? 0.0 : boundary + 1 < static_cast(road.backward) ? 0.5 : -0.5}; switch (road.changes[boundary]) { case change_both: { - dashed(offset, 0.0); + dashed(offset, 0.0, spread); break; } case change_toright: { - dashed(offset, pair); - solid(offset, -pair); + dashed(offset, pair, spread); + solid(offset, -pair, spread); break; } case change_toleft: { - solid(offset, pair); - dashed(offset, -pair); + solid(offset, pair, spread); + dashed(offset, -pair, spread); break; } default: @@ -911,17 +1154,51 @@ std::vector road_node::create_shapes() const if (opposite) { // traffic going opposite ways is kept apart with a double line - solid(offset, pair); - solid(offset, -pair); + solid(offset, pair, spread); + solid(offset, -pair, spread); } else { - solid(offset, 0.0); + solid(offset, 0.0, spread); } break; } } } + if (divided && road.median.type == median_type::painted && length > 0.0) + { + // what's between the roadways is closed to the traffic: slanted stripes are led from one edge line to the other + double const period{3.0}; + double const stripe{0.5}; + glm::vec3 const upwards{0.f, 1.f, 0.f}; + auto const gap_at = [&](double const Station) { return glm::mix(static_cast(road.median.width[0]), static_cast(road.median.width[1]), taper_progress(Station / length)); }; + // point of the edge of one of the roadways, a bit into what's between them. Side: 1 for the left one, -1 for the right one + auto const edge = [&](double const Station, double const Side) { + auto const station{std::clamp(Station, 0.0, length)}; + auto const frame{axis.at(station)}; + auto const scale{glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(station / length))}; + return frame.position + frame.left * (divide * scale + Side * std::max(0.0, 0.5 * gap_at(station) - 2.0 * inset)) + up * line_lift; + }; + for (auto station{0.5 * period}; station < length; station += period) + { + auto const gap{gap_at(station)}; + if (gap < 1.0) + { + continue; + } + // a stripe gets as far along the road as it gets across it + auto const leftstart{edge(station, 1.0)}; + auto const leftend{edge(station + stripe, 1.0)}; + auto const rightstart{edge(station + gap, -1.0)}; + auto const rightend{edge(station + gap + stripe, -1.0)}; + lines.push_back({leftstart, upwards, {0.f, 0.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({rightend, upwards, {1.f, 1.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + } + } } std::vector shapes; @@ -950,6 +1227,13 @@ double const junction_fit{0.05}; // a lane has to end this close to where the ju int const corner_steps{6}; // pieces a rounded corner is made of double const stopline_width{0.5}; double const stopline_inset{0.2}; // distance of a stop line from the end of its road +double const gate_period{0.2}; // seconds between looks at the traffic around a junction +double const gate_reach{120.0}; // how far back along the lanes leading to a junction the vehicles are looked for +double const gate_gap{6.0}; // a vehicle this many seconds from a junction starts to matter to the others, and is told about them +double const gate_zone{15.0}; // and so does one this close, however slow it goes +double const gate_commit{8.0}; // a vehicle let through this close to the junction is on its way, the others wait for it +double const gate_patience{6.0}; // seconds a vehicle let through can stand still before the others stop waiting for it +double const way_clearance{2.2}; // ways through a junction which pass this close to each other can't be taken at the same time // direction on the ground as a vector in space glm::dvec3 flat(glm::dvec2 const &Direction) @@ -1064,10 +1348,24 @@ void junction_node::state::normalize() 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.median = (arm.incoming > 0 && arm.outgoing > 0 ? std::clamp(arm.median, 0.f, 100.f) : 0.f); arm.direction = (glm::length(arm.direction) > 1e-6 ? glm::normalize(arm.direction) : glm::dvec2{0.0, 1.0}); + if (arm.turns.size() > static_cast(arm.incoming)) + { + arm.turns.resize(static_cast(arm.incoming)); + } + for (auto &turns : arm.turns) + { + turns &= (turn_left | turn_straight | turn_right); + } } side.width = std::max(0.f, side.width); kerbheight = std::max(0.f, kerbheight); + kerbwidth = std::clamp(kerbwidth, 0.05f, 2.f); + if (kerbmaterial.empty()) + { + kerbmaterial = "none"; + } slope = glm::max(slope, glm::vec2{0.f}); if (texturelength < 0.01f) { @@ -1087,7 +1385,7 @@ void junction_node::state::normalize() // 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; + return Arm < arms.size() ? static_cast(arms[Arm].incoming + arms[Arm].outgoing) * arms[Arm].width + arms[Arm].median : 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 @@ -1096,7 +1394,8 @@ glm::dvec3 junction_node::state::lane_point(std::size_t const Arm, bool const In 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}; + // what keeps the two directions apart lies past the lanes coming in + auto const offset{Incoming ? divide + (Lane - 0.5) * arm.width : divide - arm.median - (Lane - 0.5) * arm.width}; return arm.position + flat(left_of(arm.direction)) * offset; } @@ -1174,6 +1473,16 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) Input >> m_state.centre.x >> m_state.centre.y >> m_state.centre.z; m_state.centre += Offset; std::vector stoplines; + // what's said about the arms may come ahead of them, so it waits until all is read + std::vector> priorities; + std::vector> medians; + struct turn_property + { + int arm; + int lane; + std::string ways; + }; + std::vector turns; auto token{Input.getToken()}; while (false == token.empty() && token != "endjunction") @@ -1231,6 +1540,15 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) Input.getTokens(2); Input >> m_state.slope.x >> m_state.slope.y; } + else if (token == "kerbs") + { + // kerbs + m_state.kerbs = true; + Input.getTokens(); + Input >> m_state.kerbwidth; + m_state.kerbmaterial = Input.getToken(); + replace_slashes(m_state.kerbmaterial); + } else if (token == "stopline") { // stopline ; the arms it's for may come later, so the numbers wait @@ -1239,6 +1557,34 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) Input >> number; stoplines.emplace_back(number); } + else if (token == "priority") + { + // priority + int number{0}; + Input.getTokens(); + Input >> number; + priorities.emplace_back(number, Input.getToken()); + } + else if (token == "armmedian") + { + // armmedian + int number{0}; + float width{0.f}; + Input.getTokens(2); + Input >> number >> width; + medians.emplace_back(number, width); + } + else if (token == "turns") + { + // turns + turn_property property; + property.arm = 0; + property.lane = 0; + Input.getTokens(2); + Input >> property.arm >> property.lane; + property.ways = Input.getToken(); + turns.emplace_back(property); + } else if (token == "velocity") { Input.getTokens(); @@ -1268,6 +1614,41 @@ void junction_node::import(cParser &Input, glm::dvec3 const &Offset) } m_state.arms[number - 1].stopline = true; } + for (auto const &entry : medians) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to set the median for"); + continue; + } + m_state.arms[entry.first - 1].median = entry.second; + } + for (auto const &entry : priorities) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to set the right of way for"); + continue; + } + auto &priority{m_state.arms[entry.first - 1].priority}; + priority = (entry.second == "main" ? right_of_way::priority : entry.second == "yield" ? right_of_way::yield : entry.second == "stop" ? right_of_way::stop : right_of_way::none); + if (entry.second != "main" && entry.second != "yield" && entry.second != "stop" && entry.second != "none") + { + ErrorLog("Bad junction: unknown right of way \"" + entry.second + "\" defined for " + label); + } + } + for (auto const &entry : turns) + { + if (entry.arm < 1 || entry.arm > static_cast(m_state.arms.size()) || entry.lane < 1 || entry.lane > m_state.arms[entry.arm - 1].incoming) + { + ErrorLog("Bad junction: " + label + " has no lane " + std::to_string(entry.lane) + " leading in at arm " + std::to_string(entry.arm)); + continue; + } + auto &lanes{m_state.arms[entry.arm - 1].turns}; + lanes.resize(std::max(lanes.size(), static_cast(entry.lane)), 0); + auto const has = [&entry](char const Way) { return entry.ways.find(Way) != std::string::npos; }; + lanes[entry.lane - 1] = (has('l') ? turn_left : 0) | (has('s') ? turn_straight : 0) | (has('r') ? turn_right : 0); + } 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"); @@ -1298,6 +1679,7 @@ void junction_node::create_links() { std::vector incoming; std::vector outgoing; + float weight{100.f}; // how willing the drivers are to leave by the road }; std::vector lanes(arms.size()); for (std::size_t arm = 0; arm < arms.size(); ++arm) @@ -1317,6 +1699,7 @@ void junction_node::create_links() { continue; } + lanes[arm].weight = layout.weight; // 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) { @@ -1407,11 +1790,14 @@ void junction_node::create_links() { continue; } - // with more lanes than one the inner lane takes the left turns, the outer one the right turns, and all go straight + // with more lanes than one the inner lane takes the left turns, the outer one the right turns, and all go straight; + // unless the scenery says which ways the lane can be left by + auto const turns{static_cast(lane - 1) < arms[arm].turns.size() ? arms[arm].turns[lane - 1] : 0}; 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)}; + auto const allowed{turns != 0 ? (turns & (exit.kind == way::left ? turn_left : exit.kind == way::right ? turn_right : turn_straight)) != 0 : + lanecount == 1 || exit.kind == way::straight || (exit.kind == way::left && lane == 1) || (exit.kind == way::right && lane == lanecount)}; if (allowed) { ways.emplace_back(exit); @@ -1482,6 +1868,14 @@ void junction_node::create_links() reach += 0.39 * span / ways.size(); } auto const startcontrol{(flat(heading) + up * lane_grade(*entry, true)) * reach}; + gate_data gate; + gate.arm = arm; + gate.lane = lane; + gate.entry = entry; + gate.line = start; + // what a driver calls each way, once they picked it: the number of the path of the crossroads it's on, negative if it's taken against the path + int const codes2[]{1, -2}; + int const codes3[]{1, 5, -4}; for (std::size_t idx = 0; idx < ways.size(); ++idx) { segment_data movement; @@ -1490,6 +1884,13 @@ void junction_node::create_links() movement.points[segment_data::point::control2] = endcontrols[idx]; movement.points[segment_data::point::end] = ends[idx]; m_movements.emplace_back(movement); + way_data route; + route.exit = ways[idx].arm; + route.kind = (ways[idx].kind == way::left ? turn_left : ways[idx].kind == way::right ? turn_right : turn_straight); + route.code = (ways.size() == 3 ? codes3[idx] : ways.size() == 2 ? codes2[idx] : 0); + route.track = ways[idx].track; + route.path = movement; + gate.ways.emplace_back(route); } text.str(""); @@ -1556,6 +1957,16 @@ void junction_node::create_links() link->Switch(0); entry->trNext = link; entry->iNextDirection = 0; + // odds of each way being picked go by how willing the drivers are to take the road it leads to. + // the routes of a crossroads are numbered: left, right, straight ahead + if (ways.size() == 3) + { + link->m_routeweights = {lanes[ways[1].arm].weight, lanes[ways[2].arm].weight, lanes[ways[0].arm].weight}; + } + else + { + link->m_routeweights = {lanes[ways[0].arm].weight, lanes[ways[1].arm].weight, 0.f}; + } // 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}; @@ -1569,19 +1980,356 @@ void junction_node::create_links() } } m_links.emplace_back(link); + gate.link = link; + m_gates.emplace_back(std::move(gate)); } } + create_gates(); +} + +// sets up what the traffic of each lane leading in is told, once the ways through are made +void junction_node::create_gates() +{ + if (m_state.arms.size() < 3) + { + // where a road only changes its lanes nobody gets in the way of anybody + m_gates.clear(); + return; + } + auto const point = [](segment_data const &Path, double const T) { + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + auto const p1{p0 + Path.points[segment_data::point::control1]}; + auto const p2{p3 + Path.points[segment_data::point::control2]}; + auto const u{1.0 - T}; + auto const result{u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3}; + return glm::dvec2{result.x, result.z}; + }; + int const samples{10}; + for (std::size_t index = 0; index < m_gates.size(); ++index) + { + auto &gate{m_gates[index]}; + // lanes the vehicles heading for the junction are on: the one leading in, and the same lane of the pieces before it + gate.approach.emplace_back(gate.entry); + auto reach{glm::distance(glm::dvec3{gate.entry->CurrentSegment()->FastGetPoint_0()}, glm::dvec3{gate.entry->CurrentSegment()->FastGetPoint_1()})}; + for (auto *lane{gate.entry}; reach < gate_reach && gate.approach.size() < 16;) + { + auto *previous{lane->trPrev}; + if (previous == nullptr || previous->trNext != lane || dynamic_cast(previous->m_road) == nullptr || previous->m_paths.size() != 1) + { + break; + } + gate.approach.emplace_back(previous); + reach += glm::distance(glm::dvec3{previous->CurrentSegment()->FastGetPoint_0()}, glm::dvec3{previous->CurrentSegment()->FastGetPoint_1()}); + lane = previous; + } + // the order, found by the drivers a bit ahead of where the lane ends + if (index >= m_orders.size()) + { + m_orders.emplace_back(new road_order()); + m_orders.back()->m_name = m_name + ":gate" + std::to_string(m_orders.size()); + } + gate.order = m_orders[index]; + gate.order->m_velocity = -1.0; + gate.order->m_location = gate.line; + gate.entry->m_events2.emplace_back(gate.order->m_name, gate.order); + // ways which get in the way of each other: the ones from other roads which cross it, or lead to the same lane + for (std::size_t way = 0; way < gate.ways.size(); ++way) + { + for (std::size_t other = 0; other < m_gates.size(); ++other) + { + if (m_gates[other].arm == gate.arm) + { + continue; + } + for (std::size_t otherway = 0; otherway < m_gates[other].ways.size(); ++otherway) + { + auto const &path{gate.ways[way].path}; + auto const &otherpath{m_gates[other].ways[otherway].path}; + auto nearest{std::numeric_limits::max()}; + for (int first = 0; first <= samples; ++first) + { + for (int second = 0; second <= samples; ++second) + { + nearest = std::min(nearest, glm::distance(point(path, static_cast(first) / samples), point(otherpath, static_cast(second) / samples))); + } + } + if (nearest < way_clearance) + { + gate.ways[way].conflicts.emplace_back(other, otherway); + } + } + } + } + } + m_scan = 0.0; + m_quiet = false; } // gives up the paths leading through the junction std::vector junction_node::release_links() { + // the lanes leading in are let go of. a driver who has one of the orders in sight keeps it; it's not to hold anyone anymore + for (auto const &gate : m_gates) + { + if (gate.entry == nullptr || gate.order == nullptr) + { + continue; + } + auto &events{gate.entry->m_events2}; + auto const *order{static_cast(gate.order)}; + events.erase(std::remove_if(events.begin(), events.end(), [order](std::pair const &Entry) { return Entry.second == order; }), events.end()); + } + for (auto *order : m_orders) + { + order->m_velocity = -1.0; + } + m_gates.clear(); std::vector released; released.swap(m_links); m_movements.clear(); return released; } +// tells the vehicles heading for the junction whether they can drive on +void junction_node::update(double const Deltatime) +{ + if (m_gates.empty()) + { + return; + } + m_scan -= Deltatime; + if (m_scan > 0.0) + { + return; + } + auto const elapsed{gate_period - m_scan}; + m_scan = gate_period; + + // ways a vehicle is going to take, or is on: the one its driver picked, or any of them if the driver didn't pick yet + auto const ways_of = [](gate_data const &Gate, TDynamicObject *Vehicle) { + auto const all{(1u << Gate.ways.size()) - 1u}; + if (Gate.ways.size() < 2) + { + return all; + } + auto const code{Vehicle->RouteWish(Gate.link)}; + for (std::size_t way = 0; way < Gate.ways.size(); ++way) + { + if (Gate.ways[way].code == code) + { + return 1u << way; + } + } + return all; + }; + // who's first in line on each lane leading in, and who's on the ways through + auto traffic{false}; + for (auto &gate : m_gates) + { + TDynamicObject *first{nullptr}; + auto nearest{std::numeric_limits::max()}; + for (auto const *lane : gate.approach) + { + for (auto *vehicle : lane->Dynamics) + { + if (vehicle == nullptr) + { + continue; + } + auto const position{vehicle->GetPosition()}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{gate.line.x, gate.line.z})}; + if (distance < nearest) + { + nearest = distance; + first = vehicle; + } + } + } + if (first != gate.first) + { + gate.first = first; + gate.committed = false; + gate.stopped = false; + gate.waited = 0.0; + gate.idle = 0.0; + } + gate.inside = 0; + for (auto *vehicle : gate.link->Dynamics) + { + if (vehicle != nullptr) + { + gate.inside |= ways_of(gate, vehicle); + } + } + if (first != nullptr) + { + gate.distance = nearest; + gate.speed = std::abs(first->MoverParameters->V); + gate.halflength = 0.5 * first->MoverParameters->Dim.L; + gate.taken = ways_of(gate, first); + gate.active = gate.distance < std::max(gate_zone, gate.speed * gate_gap) + gate.halflength; + } + else + { + gate.taken = 0; + gate.active = false; + gate.blocked = false; + } + traffic = traffic || first != nullptr || gate.inside != 0; + } + if (false == traffic) + { + if (false == m_quiet) + { + for (auto &gate : m_gates) + { + gate.order->m_velocity = -1.0; + } + m_quiet = true; + } + return; + } + m_quiet = false; + + auto const &arms{m_state.arms}; + auto const rank = [&arms](gate_data const &Gate) { return arms[Gate.arm].priority == right_of_way::priority ? 2 : arms[Gate.arm].priority == right_of_way::none ? 1 : 0; }; + // true if any of the ways marked for one lane gets in the way of any marked for another + auto const clash = [this](std::size_t const Gate, unsigned const Ways, std::size_t const Other, unsigned const Otherways) { + auto const &gate{m_gates[Gate]}; + for (std::size_t way = 0; way < gate.ways.size(); ++way) + { + if ((Ways & (1u << way)) == 0) + { + continue; + } + for (auto const &conflict : gate.ways[way].conflicts) + { + if (conflict.first == Other && (Otherways & (1u << conflict.second)) != 0) + { + return true; + } + } + } + return false; + }; + auto const turns_left = [](gate_data const &Gate) { + if (Gate.taken == 0) + { + return false; + } + for (std::size_t way = 0; way < Gate.ways.size(); ++way) + { + if ((Gate.taken & (1u << way)) != 0 && Gate.ways[way].kind != turn_left) + { + return false; + } + } + return true; + }; + // true if the first vehicle of one lane goes ahead of the first vehicle of another + auto const precedes = [&](gate_data const &Rival, gate_data const &Gate) { + if (rank(Rival) != rank(Gate)) + { + return rank(Rival) > rank(Gate); + } + // neither has the right of way over the other: the one coming from the right goes first, + // and so does the one coming from the opposite side, if its way is turned across + auto const bearing{turn_angle(-arms[Gate.arm].direction, arms[Rival.arm].direction)}; + if (bearing < -glm::radians(20.0) && bearing > -glm::radians(160.0)) + { + return true; + } + return std::abs(bearing) <= glm::radians(20.0) && turns_left(Gate) && false == turns_left(Rival); + }; + // the lanes are gone through from the ones with the right of way, and among these from the one waiting the longest + std::vector order(m_gates.size()); + for (std::size_t index = 0; index < order.size(); ++index) + { + order[index] = index; + } + std::stable_sort(order.begin(), order.end(), [&](std::size_t const Left, std::size_t const Right) { + return rank(m_gates[Left]) != rank(m_gates[Right]) ? rank(m_gates[Left]) > rank(m_gates[Right]) : m_gates[Left].waited > m_gates[Right].waited; + }); + for (auto const index : order) + { + auto &gate{m_gates[index]}; + if (gate.first == nullptr || false == gate.active) + { + // nobody to tell anything, or too far to be bothered + gate.blocked = false; + gate.committed = false; + gate.order->m_velocity = -1.0; + continue; + } + auto blocked{false}; + for (std::size_t other = 0; other < m_gates.size() && false == blocked; ++other) + { + auto const &rival{m_gates[other]}; + if (rival.arm == gate.arm) + { + continue; + } + // the ones driving through the junction, and the ones which were let in + if (rival.inside != 0 && clash(index, gate.taken, other, rival.inside)) + { + blocked = true; + } + if (rival.first == nullptr || false == rival.active || false == clash(index, gate.taken, other, rival.taken)) + { + continue; + } + if (rival.committed) + { + blocked = true; + } + // the ones to give way to. one which stands waiting for somebody else isn't waited for: + // it'll be told to wait for this one in turn, once this one is let in + else if (precedes(rival, gate) && false == (rival.blocked && rival.speed < 0.5)) + { + blocked = true; + } + } + if (arms[gate.arm].priority == right_of_way::stop && false == gate.stopped) + { + // the stop has to be made, whatever the traffic + if (gate.speed < 0.3 && gate.distance < gate.halflength + 6.0) + { + gate.stopped = true; + } + else + { + blocked = true; + } + } + if (blocked) + { + gate.committed = false; + if (gate.speed < 0.5) + { + gate.waited += elapsed; + } + } + else if (gate.committed) + { + // a vehicle which was let in and doesn't go, for whatever reason, isn't waited for forever + gate.idle = (gate.speed < 0.5 ? gate.idle + elapsed : 0.0); + if (gate.idle > gate_patience) + { + gate.committed = false; + gate.idle = 0.0; + } + } + else if (gate.distance < gate.halflength + gate_commit) + { + gate.committed = true; + gate.idle = 0.0; + } + gate.blocked = blocked; + gate.order->m_velocity = (blocked ? 0.0 : -1.0); + } +} + // true if there's a vehicle on any path of the junction bool junction_node::occupied() const { @@ -1628,33 +2376,14 @@ std::vector junction_node::create_shapes() const shapes.emplace_back(std::move(shape)); } } - auto const &side{m_state.side}; - if (side.type != road_node::side_type::none && side.width > 0.f && false == side.material.empty() && side.material != "none") + // the corners are lined the way the edges of a road are, so the kerbs, the shoulders and the sidewalks of the roads carry on around the junction + struct corner_point { - // the corners are lined the way the sides of a road are, so the shoulders and the sidewalks of the roads carry on around the junction - struct corner_point - { - double offset; // distance from the edge of the surface - double height; - float texture; - }; - std::vector profile; - if (side.type == road_node::side_type::shoulder) - { - profile.push_back({0.0, 0.0, 1.f}); - profile.push_back({side.width, 0.0, 0.5f}); - if (m_state.slope.x > 0.f) - { - profile.push_back({static_cast(side.width) + m_state.slope.x, -m_state.slope.y, 0.f}); - } - } - else - { - auto const tile{static_cast(texture_ratio(side.material)) * m_state.texturelength}; - profile.push_back({0.0, 0.0, 0.f}); - profile.push_back({0.0, m_state.kerbheight, static_cast(m_state.kerbheight / tile)}); - profile.push_back({side.width, m_state.kerbheight, static_cast((m_state.kerbheight + side.width) / tile)}); - } + double offset; // distance from the edge of the surface + double height; + float texture; + }; + auto const line_corners = [&](std::string const &Material, std::vector const &Profile) { auto const order{m_state.arm_order()}; std::vector vertices; for (std::size_t idx = 0; idx < corners.size() && idx < order.size(); ++idx) @@ -1665,7 +2394,7 @@ std::vector junction_node::create_shapes() const continue; } // the way out of the surface at each point of the corner. at its ends it's the way across the roads there, - // so the side meets what the roads have along their edges + // so what lines the corner meets what the roads have along their edges std::vector outwards(corner.size()); outwards.front() = flat(left_of(m_state.arms[order[idx]].direction)); outwards.back() = -flat(left_of(m_state.arms[order[(idx + 1) % order.size()]].direction)); @@ -1682,10 +2411,10 @@ std::vector junction_node::create_shapes() const auto const tangent{corner[point + 1] - corner[point]}; auto const texturestart{static_cast(station / m_state.texturelength)}; auto const textureend{static_cast((station + length) / m_state.texturelength)}; - for (std::size_t band = 0; band + 1 < profile.size(); ++band) + for (std::size_t band = 0; band + 1 < Profile.size(); ++band) { - auto const &inner{profile[band]}; - auto const &outer{profile[band + 1]}; + auto const &inner{Profile[band]}; + auto const &outer{Profile[band + 1]}; auto const leftstart{corner[point] + outwards[point] * inner.offset + up * inner.height}; auto const rightstart{corner[point] + outwards[point] * outer.offset + up * outer.height}; auto const leftend{corner[point + 1] + outwards[point + 1] * inner.offset + up * inner.height}; @@ -1705,9 +2434,51 @@ std::vector junction_node::create_shapes() const if (false == vertices.empty()) { scene::shape_node shape; - shape.make_terrain(GfxRenderer->Fetch_Material(side.material), std::move(vertices), glm::dvec3{0.0}); + shape.make_terrain(GfxRenderer->Fetch_Material(Material), std::move(vertices), glm::dvec3{0.0}); shapes.emplace_back(std::move(shape)); } + }; + auto const &side{m_state.side}; + auto const sided{side.type != road_node::side_type::none && side.width > 0.f && false == side.material.empty() && side.material != "none"}; + auto const kerbed{m_state.kerbs && false == m_state.kerbmaterial.empty() && m_state.kerbmaterial != "none"}; + double const height{m_state.kerbheight}; + double reach{0.0}; // how far from the edge of the surface the side starts + if (kerbed) + { + auto const tile{static_cast(texture_ratio(m_state.kerbmaterial)) * m_state.texturelength}; + double const width{m_state.kerbwidth}; + std::vector kerb{{0.0, 0.0, 0.f}, {0.0, height, static_cast(height / tile)}, {width, height, static_cast((height + width) / tile)}}; + if (false == sided || side.type != road_node::side_type::sidewalk) + { + // with nothing raised behind it the kerb has a back + kerb.push_back({width, 0.0, static_cast((2.0 * height + width) / tile)}); + } + line_corners(m_state.kerbmaterial, kerb); + reach = width; + } + if (sided) + { + std::vector profile; + if (side.type == road_node::side_type::shoulder) + { + profile.push_back({reach, 0.0, 1.f}); + profile.push_back({reach + side.width, 0.0, 0.5f}); + if (m_state.slope.x > 0.f) + { + profile.push_back({reach + side.width + m_state.slope.x, -m_state.slope.y, 0.f}); + } + } + else + { + auto const tile{static_cast(texture_ratio(side.material)) * m_state.texturelength}; + if (false == kerbed) + { + profile.push_back({0.0, 0.0, 0.f}); + } + profile.push_back({reach, height, static_cast(height / tile)}); + profile.push_back({reach + side.width, height, static_cast((height + side.width) / tile)}); + } + line_corners(side.material, profile); } if (m_state.markings != road_node::marking_colour::none) { @@ -1883,12 +2654,38 @@ void junction_node::export_as_text_(std::ostream &Output) const Output << "side " << (m_state.side.type == road_node::side_type::shoulder ? "shoulder" : "sidewalk") << ' ' << m_state.side.width << ' ' << (m_state.side.material.empty() ? "none" : m_state.side.material) << ' ' << "kerb " << m_state.kerbheight << ' ' << "slope " << m_state.slope.x << ' ' << m_state.slope.y << ' '; } + if (m_state.kerbs) + { + Output << "kerbs " << m_state.kerbwidth << ' ' << m_state.kerbmaterial << ' '; + if (m_state.side.type == road_node::side_type::none) + { + Output << "kerb " << m_state.kerbheight << ' '; + } + } for (std::size_t arm = 0; arm < m_state.arms.size(); ++arm) { if (m_state.arms[arm].stopline) { Output << "stopline " << arm + 1 << ' '; } + if (m_state.arms[arm].median > 0.f) + { + Output << "armmedian " << arm + 1 << ' ' << m_state.arms[arm].median << ' '; + } + auto const priority{m_state.arms[arm].priority}; + if (priority != right_of_way::none) + { + Output << "priority " << arm + 1 << ' ' << (priority == right_of_way::priority ? "main" : priority == right_of_way::yield ? "yield" : "stop") << ' '; + } + for (std::size_t lane = 0; lane < m_state.arms[arm].turns.size(); ++lane) + { + auto const turns{m_state.arms[arm].turns[lane]}; + if (turns != 0) + { + Output << "turns " << arm + 1 << ' ' << lane + 1 << ' '; + Output << ((turns & turn_left) != 0 ? "l" : "") << ((turns & turn_straight) != 0 ? "s" : "") << ((turns & turn_right) != 0 ? "r" : "") << ' '; + } + } } Output << "velocity " << m_state.velocity << ' ' << "friction " << m_state.friction << ' ' << "environment " << m_state.environment << ' '; Output << "endjunction" @@ -1936,7 +2733,15 @@ TTrack *road_node::create_track(std::string const &Name, segment_data const &Pat // radius() subclass details, calculates node's bounding radius float road_node::radius_() { - return static_cast(0.5 * m_state.length() + 0.5 * m_state.width() * std::max(m_state.taper[0], m_state.taper[1]) + std::max(m_state.sides[0].width, m_state.sides[1].width) + m_state.slope.x); + auto reach{static_cast(m_state.slope.x)}; + for (auto const &bank : m_state.banks) + { + if (bank.set) + { + reach = std::max(reach, static_cast(std::max(bank.width[0], bank.width[1]))); + } + } + return static_cast(0.5 * m_state.length() + 0.5 * std::max(m_state.span(0.0), m_state.span(1.0)) + m_state.kerbwidth + std::max(m_state.sides[0].width, m_state.sides[1].width) + reach); } // serialize() subclass details, sends content of the subclass to provided stream @@ -2008,6 +2813,35 @@ void road_node::export_as_text_(std::ostream &Output) const Output << "kerb " << road.kerbheight << ' ' << "slope " << road.slope.x << ' ' << road.slope.y << ' ' << "markings " << (road.markings == marking_colour::white ? "white" : road.markings == marking_colour::orange ? "orange" : "none") << ' '; // properties passed on to the lanes Output << "friction " << road.friction << ' ' << "environment " << road.environment << ' '; + if (road.weight != 100.f) + { + Output << "weight " << road.weight << ' '; + } + if (road.median.width[0] > 0.f || road.median.width[1] > 0.f) + { + Output << "median " << (road.median.type == median_type::gap ? "gap" : road.median.type == median_type::island ? "island" : "painted") << ' '; + Output << road.median.width[0] << ' ' << road.median.width[1] << ' ' << road.median.material << ' '; + } + if (road.kerbs[0] || road.kerbs[1]) + { + Output << "kerbs " << (road.kerbs[0] && road.kerbs[1] ? "both" : road.kerbs[0] ? "left" : "right") << ' ' << road.kerbwidth << ' ' << road.kerbmaterial << ' '; + } + for (int side = 0; side < 2; ++side) + { + auto const &bank{road.banks[side]}; + if (bank.set) + { + Output << "bank " << sidenames[side] << ' ' << bank.width[0] << ' ' << bank.drop[0] << ' ' << bank.width[1] << ' ' << bank.drop[1] << ' '; + } + } + if (road.bankmaterial != "none") + { + Output << "bankmaterial " << road.bankmaterial << ' '; + } + if (road.roundabout) + { + Output << "roundabout "; + } // footer Output << "endroad" << "\n"; @@ -2067,6 +2901,18 @@ void junction_table::InitJunctions() } } +// to be called with each step of the simulation +void junction_table::update(double const Deltatime) +{ + for (auto *junction : m_items) + { + if (junction != nullptr && false == junction->m_editorremoved) + { + junction->update(Deltatime); + } + } +} + // generates geometry of the junctions and puts it in the scene void junction_table::create_geometry(scene::scratch_data &Scratchpad) { diff --git a/world/Road.h b/world/Road.h index 11465b5b..99e85fcd 100644 --- a/world/Road.h +++ b/world/Road.h @@ -25,6 +25,7 @@ namespace scene struct scratch_data; class basic_section; } +class road_order; // 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 @@ -54,6 +55,9 @@ class owned_shapes // deal with a road without knowing about it. // scenery entry: // node road [ ]... endroad +// properties, past the ones of the lanes and the look: median , +// kerbs , bank , +// bankmaterial , weight , roundabout class road_node : public scene::basic_node { @@ -90,6 +94,26 @@ class road_node : public scene::basic_node float width{0.f}; std::string material; }; + // what keeps the two directions of a road apart, where they're led away from each other + enum class median_type + { + gap, // nothing: the ground shows between two roadways + painted, // the surface carries on, marked as closed to the traffic + island // raised on a kerb + }; + struct median_data + { + median_type type{median_type::painted}; + std::array width{0.f, 0.f}; // at the start and at the end of the axis + std::string material{"none"}; // what an island is covered with + }; + // slope leading from the edge of the road down, or up, to the ground + struct bank_data + { + bool set{false}; // false: a shoulder gets the bank the road gives to all its shoulders, anything else gets none + std::array width{0.f, 0.f}; // at the start and at the end of the axis + std::array drop{0.f, 0.f}; // how far below the edge of the road it ends; negative if it rises + }; // everything the scenery says about the road. the lanes go left to right when facing along the axis: // the ones going against it, outermost first, then the ones going along it struct state @@ -112,6 +136,14 @@ class road_node : public scene::basic_node float sounddistance{25.f}; int quality{15}; std::string environment{"flat"}; + float weight{100.f}; // how willing the drivers are to take the road when leaving a junction, next to the other roads they can take there + median_data median; + std::array kerbs{false, false}; // left, right: a kerb runs along the edge of the surface, as high as the kerb of a sidewalk + float kerbwidth{0.15f}; + std::string kerbmaterial{"none"}; + std::array banks; // left, right + std::string bankmaterial{"none"}; // what a bank is covered with, other than one of a shoulder, which goes with the shoulder + bool roundabout{false}; // the piece is a part of a roundabout: traffic on it goes ahead of the traffic joining it // brings the content to a usable form: values within their limits, an entry for each lane and for each pair of neighbours void normalize(); @@ -127,6 +159,14 @@ class road_node : public scene::basic_node double width() const; // what the widths of the lanes are multiplied by at specified value of the curve parameter double scale(double const T) const; + // true if the two directions of the road are kept apart anywhere along it + bool divided() const; + // how far apart the two directions are at specified value of the curve parameter + double median_width(double const T) const; + // distance from the axis of the line the two directions meet at, on a road which doesn't keep them apart + double divide() const; + // width of the whole roadway at specified value of the curve parameter: the lanes and what's between the directions + double span(double const T) const; // shape of the middle of a lane, laid out in the direction of travel segment_data lane_path(std::size_t const Lane) const; // length of the axis @@ -211,13 +251,32 @@ class road_node : public scene::basic_node // scenery entry: // node junction arm ... [ ]... endjunction // properties: surface , texlength , markings , side , kerb , -// slope , stopline , velocity , friction , environment -// the arms don't have to be level with each other: the surface is spanned between their ends and the centre +// slope , stopline , velocity , friction , environment , +// priority , turns , +// armmedian , kerbs +// the arms don't have to be level with each other: the surface is spanned between their ends and the centre. +// the junction also tells the vehicles when to wait: the ones which have to give way wait for the ones with the right of way, +// and where neither has it, for the ones coming from their right, or from the opposite side when turning left across their way class junction_node : public scene::basic_node { public: // types + // what the vehicles coming by a road are to do about the ones coming by the other roads + enum class right_of_way + { + none, // the rule of the right hand + priority, // the others give way + yield, // gives way + stop // stops, then gives way + }; + // ways a lane can be left by + enum turn_flags : int + { + turn_left = 1, + turn_straight = 2, + turn_right = 4 + }; struct arm_data { glm::dvec3 position{0.0}; // middle of the end of the road @@ -226,6 +285,46 @@ class junction_node : public scene::basic_node int outgoing{1}; // lanes leading out of it float width{3.5f}; // width of a lane bool stopline{false}; // a line is painted across the lanes leading into the junction + float median{0.f}; // how far apart the two directions of the road are there + right_of_way priority{right_of_way::none}; + // ways each lane leading into the junction can be left by, a sum of turn_flags; the lanes are counted from the middle of the road. + // a lane with no entry, or with 0, gets what the junction works out: the inner lane takes the left turns, the outer one the right turns + std::vector turns; + }; + // a way through the junction from a lane leading into it + struct way_data + { + std::size_t exit{0}; // arm it leaves by + int kind{turn_straight}; // one of turn_flags + int code{0}; // what a driver calls it, once they picked it + TTrack *track{nullptr}; // lane it leads to + segment_data path; + std::vector> conflicts; // ways it can't be taken along with: lane leading in, and a way from that lane + }; + // a lane leading into the junction, with what its traffic is told + struct gate_data + { + std::size_t arm{0}; + int lane{1}; + TTrack *entry{nullptr}; // the lane + TTrack *link{nullptr}; // path, or crossroads of paths, leading on from it + std::vector ways; + std::vector approach; // lanes the vehicles heading for the junction are looked for on, nearest first + glm::dvec3 line{0.0}; // where the lane ends + road_order *order{nullptr}; + // what's going on at the moment + TDynamicObject *first{nullptr}; // vehicle nearest to the junction + double distance{0.0}; // of that vehicle from the end of the lane + double speed{0.0}; + double halflength{0.0}; + unsigned taken{0}; // ways that vehicle may take, a bit for each + unsigned inside{0}; // ways with a vehicle on them + bool active{false}; // the vehicle is near enough to matter + bool blocked{false}; // it's told to wait + bool committed{false}; // it was let through, the others wait for it + bool stopped{false}; // it made its stop at the stop sign + double waited{0.0}; // how long it's been standing there + double idle{0.0}; // how long it's been standing there since it was let through }; struct state { @@ -235,6 +334,9 @@ class junction_node : public scene::basic_node float texturelength{4.f}; road_node::marking_colour markings{road_node::marking_colour::white}; road_node::side_data side; // what the corners between the roads are lined with, the way the sides of a road are + bool kerbs{false}; // a kerb runs around the corners, the way it runs along the edges of a road + float kerbwidth{0.15f}; + std::string kerbmaterial{"none"}; float kerbheight{0.12f}; glm::vec2 slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder float velocity{30.f}; // speed limit on the way through @@ -280,6 +382,13 @@ class junction_node : public scene::basic_node } // true if there's a vehicle on any path of the junction bool occupied() const; + // tells the vehicles heading for the junction whether they can drive on. to be called with each step of the simulation + void update(double const Deltatime); + // lanes leading into the junction, with what their traffic is told at the moment + std::vector const &gates() const + { + return m_gates; + } // 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 @@ -304,10 +413,16 @@ class junction_node : public scene::basic_node 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); + // sets up what the traffic of each lane leading in is told, once the ways through are made + void create_gates(); // members state m_state; std::vector m_links; std::vector m_movements; + std::vector m_gates; + std::vector m_orders; // orders for the drivers, used again each time the gates are made. never freed, as a driver can hold on to one + double m_scan{0.0}; // time left to the next look at the traffic + bool m_quiet{false}; // there was no traffic the last time bool m_merged{false}; owned_shapes m_shapes; }; @@ -330,6 +445,8 @@ class junction_table : public basic_table public: // legacy style initialization, to be performed when the tracks are joined, ahead of the roads void InitJunctions(); + // to be called with each step of the simulation + void update(double const Deltatime); // generates geometry of the junctions and puts it in the scene void create_geometry(scene::scratch_data &Scratchpad); }; diff --git a/world/RoadOrder.h b/world/RoadOrder.h new file mode 100644 index 00000000..79bf2d43 --- /dev/null +++ b/world/RoadOrder.h @@ -0,0 +1,60 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +#include + +#include "world/Event.h" + +// an order for the drivers of road vehicles heading for a place they may have to wait at: a closed level crossing, +// a junction they have to give way at. to them it's what a signal is to the driver of a train. it doesn't go to +// the event manager and never runs: the drivers find it on the lane they scan, and ask it what speed it allows +class road_order : public basic_event +{ + + public: + road_order() + { + m_passive = true; + } + void init() override {} + std::string input_text() const override + { + return "SetVelocity"; + } + TCommandType input_command() const override + { + return TCommandType::cm_SetVelocity; + } + double input_value(int const Index) const override + { + return m_velocity; + } + glm::dvec3 input_location() const override + { + return m_location; + } + // members + double m_velocity{-1.0}; // 0: stop, -1: no limit + glm::dvec3 m_location{0.0}; + + private: + std::string type() const override + { + return "roadorder"; + } + void deserialize_(cParser &Input, scene::scratch_data &Scratchpad) override {} + void run_() override {} + void export_as_text_(std::ostream &Output) const override {} +}; + +//--------------------------------------------------------------------------- diff --git a/world/RoadPoint.cpp b/world/RoadPoint.cpp index 4946cf81..3663a7d3 100644 --- a/world/RoadPoint.cpp +++ b/world/RoadPoint.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "world/Track.h" #include "world/Event.h" +#include "world/RoadOrder.h" #include "vehicle/DynObj.h" #include "vehicle/Driver.h" #include "vehicle/Train.h" @@ -37,6 +38,11 @@ constexpr double near_zone{30.0}; // a rail vehicle this close to the stop lines constexpr double clear_zone{20.0}; // a vehicle isn't put on the road with another one this close constexpr double retry_period{1.0}; // seconds to the next try, when a vehicle can't be put on the road constexpr double front_room{2.5}; // half the length a vehicle is presumed to have before it's made +constexpr double queue_reach{250.0}; // how far back from a closed crossing the vehicles standing on the lanes are taken to wait for it +constexpr double stuck_time{15.0}; // seconds a vehicle of a spawn point can stand still before it's taken off the road +constexpr double stuck_speed{0.1}; // m/s, slower than this is standing still +constexpr double reverse_speed{0.5}; // m/s, a vehicle going backwards faster than this is taken off the road +constexpr double stripe_width{0.5}; // of a pedestrian crossing; the gaps are as wide // a path as a curve: points along it, and distances along it class path_curve @@ -154,46 +160,34 @@ bool out_of_reach(segment_data const &Path, glm::dvec3 const &Point, double cons return glm::distance(glm::dvec2{start.x, start.z}, glm::dvec2{Point.x, Point.z}) > span + Reach; } -// an order for the drivers heading for a closed crossing; to them it's what a signal showing stop is to the driver of a train. -// it doesn't go to the event manager and never runs: the drivers find it on the lane they scan, and ask it what speed it allows -class stop_event : public basic_event +// an order for the drivers heading for a closed crossing +using stop_event = road_order; + +// the road passing under specified point: the one whose axis is nearest. T: receives the value of the curve parameter of its axis there +road_node const *road_under(glm::dvec3 const &Point, double &T) { - - public: - stop_event() + road_node const *result{nullptr}; + auto best{std::numeric_limits::max()}; + for (auto const *road : simulation::Roads.sequence()) { - m_passive = true; + if (road == nullptr || road->m_editorremoved || out_of_reach(road->definition().axis, Point, 50.0)) + { + continue; + } + auto const &layout{road->definition()}; + path_curve const axis{layout.axis}; + double distance{0.0}; + auto const t{axis.nearest(Point, distance)}; + if (distance > 0.5 * layout.span(t) + 1.0 || std::abs(axis.point(t).y - Point.y) > 3.0 || distance >= best) + { + continue; + } + best = distance; + result = road; + T = t; } - void init() override {} - std::string input_text() const override - { - return "SetVelocity"; - } - TCommandType input_command() const override - { - return TCommandType::cm_SetVelocity; - } - double input_value(int const Index) const override - { - return m_velocity; - } - glm::dvec3 input_location() const override - { - return m_location; - } - // members - double m_velocity{-1.0}; // 0: stop, -1: no limit - glm::dvec3 m_location{0.0}; - - private: - std::string type() const override - { - return "roadstop"; - } - void deserialize_(cParser &Input, scene::scratch_data &Scratchpad) override {} - void run_() override {} - void export_as_text_(std::ostream &Output) const override {} -}; + return result; +} // lane of a road with the point nearest to specified one, within specified distance from it. Station: receives the distance of that point from the start of the lane TTrack *nearest_lane(glm::dvec3 const &Point, double const Reach, double &Station) @@ -242,24 +236,25 @@ void roadpoint_node::state::normalize() interval = std::clamp(interval, 1.f, 3600.f); variation = std::clamp(variation, 0.f, 1.f); velocity = std::clamp(velocity, 5.f, 200.f); - copies = std::clamp(copies, 1, 16); + count = std::clamp(count, 1, 64); radius = std::clamp(radius, 0.5f, 50.f); + length = std::clamp(length, 1.f, 20.f); } roadpoint_node::roadpoint_node(scene::node_data const &Nodedata) : basic_node(Nodedata) { - m_state.kind = (Nodedata.type == "spawn" ? kind_type::spawn : Nodedata.type == "despawn" ? kind_type::despawn : kind_type::crossing); + m_state.kind = (Nodedata.type == "spawn" ? kind_type::spawn : Nodedata.type == "despawn" ? kind_type::despawn : Nodedata.type == "crosswalk" ? kind_type::crosswalk : kind_type::crossing); } // scenery keyword for specified kind of point std::string roadpoint_node::keyword(kind_type const Kind) { - return Kind == kind_type::spawn ? "spawn" : Kind == kind_type::despawn ? "despawn" : "crossing"; + return Kind == kind_type::spawn ? "spawn" : Kind == kind_type::despawn ? "despawn" : Kind == kind_type::crosswalk ? "crosswalk" : "crossing"; } bool roadpoint_node::is_keyword(std::string const &Type) { - return Type == "crossing" || Type == "spawn" || Type == "despawn"; + return Type == "crossing" || Type == "spawn" || Type == "despawn" || Type == "crosswalk"; } // reads vehicles out of a text @@ -341,6 +336,8 @@ void roadpoint_node::import(cParser &Input, glm::dvec3 const &Offset) Input.getTokens(3); Input >> m_state.position.x >> m_state.position.y >> m_state.position.z; m_state.position += Offset; + int copies{0}; // of each vehicle, the way the first sceneries with spawn points gave the size of the set + auto counted{false}; auto token{Input.getToken()}; while (false == token.empty() && token != "end" + type) @@ -374,10 +371,21 @@ void roadpoint_node::import(cParser &Input, glm::dvec3 const &Offset) Input.getTokens(); Input >> m_state.velocity; } + else if (token == "count") + { + Input.getTokens(); + Input >> m_state.count; + counted = true; + } else if (token == "copies") { Input.getTokens(); - Input >> m_state.copies; + Input >> copies; + } + else if (token == "length") + { + Input.getTokens(); + Input >> m_state.length; } else if (token == "radius") { @@ -441,6 +449,10 @@ void roadpoint_node::import(cParser &Input, glm::dvec3 const &Offset) } token = Input.getToken(); } + if (false == counted && copies > 0) + { + m_state.count = copies * static_cast(std::max(1, m_state.vehicles.size())); + } m_state.normalize(); location(m_state.position); } @@ -449,10 +461,10 @@ void roadpoint_node::import(cParser &Input, glm::dvec3 const &Offset) void roadpoint_node::define(state const &State) { auto const vehicles{m_state.vehicles}; - auto const copies{m_state.copies}; + auto const count{m_state.count}; m_state = State; m_state.normalize(); - if (m_prepared && (m_state.vehicles != vehicles || m_state.copies != copies)) + if (m_prepared && (m_state.vehicles != vehicles || m_state.count != count)) { // vehicles can't be unmade, there's too much holding on to them. the old ones are left alone: // those which wait stay where they are, those on the road drive on until something takes them away @@ -492,6 +504,24 @@ void roadpoint_node::bind() } break; } + case kind_type::crosswalk: + { + // the stripes go across the road the point is on + double t{0.0}; + auto const *road{road_under(m_state.position, t)}; + if (road != nullptr) + { + auto const &layout{road->definition()}; + path_curve const axis{layout.axis}; + m_onroad = true; + m_along = axis.direction(t); + m_halfwidth = 0.5 * layout.span(t); + // the point itself may be off the axis, the stripes aren't + auto const centre{axis.point(t)}; + m_state.position = centre; + } + break; + } case kind_type::despawn: { // the vehicles are taken from every lane which passes close enough @@ -525,25 +555,8 @@ void roadpoint_node::bind() void roadpoint_node::bind_crossing() { // the road the crossing is on: the one whose axis passes nearest - road_node const *crossed{nullptr}; - auto best{std::numeric_limits::max()}; - for (auto const *road : simulation::Roads.sequence()) - { - if (road == nullptr || road->m_editorremoved || out_of_reach(road->definition().axis, m_state.position, 50.0)) - { - continue; - } - auto const &layout{road->definition()}; - path_curve const axis{layout.axis}; - double distance{0.0}; - auto const t{axis.nearest(m_state.position, distance)}; - if (distance > 0.5 * layout.width() * layout.scale(t) + 1.0 || std::abs(axis.point(t).y - m_state.position.y) > 3.0 || distance >= best) - { - continue; - } - best = distance; - crossed = road; - } + double where{0.0}; + auto const *crossed{road_under(m_state.position, where)}; if (crossed != nullptr) { auto const &lanes{crossed->tracks()}; @@ -659,6 +672,24 @@ void roadpoint_node::bind_crossing() { m_rails.emplace_back(entry.first); } + // lanes the vehicles wait on while the crossing is closed: the ones with the stops, and what leads to them + for (auto const &stop : m_stops) + { + auto *lane{stop.track}; + double reach{0.0}; + for (int guard = 0; lane != nullptr && guard < 64 && reach < queue_reach; ++guard) + { + if (std::find(m_queue.begin(), m_queue.end(), lane) == m_queue.end()) + { + m_queue.emplace_back(lane); + } + if (false == lane->m_paths.empty()) + { + reach += glm::distance(lane->m_paths.front().points[segment_data::point::start], lane->m_paths.front().points[segment_data::point::end]); + } + lane = lane->trPrev; + } + } close(m_closed); } @@ -683,8 +714,10 @@ void roadpoint_node::unbind() } m_stops.clear(); m_rails.clear(); + m_queue.clear(); m_lanes.clear(); m_seen.clear(); + m_onroad = false; } // makes the vehicles of a spawn point @@ -709,47 +742,48 @@ void roadpoint_node::prepare() // vehicles are known by their names, so a point without one makes something up for them static int nameless{0}; auto const basename{m_name.empty() ? "roadspawn" + std::to_string(++nameless) : m_name}; - for (auto const &definition : m_state.vehicles) + // the listed vehicles are gone through as many times as it takes, from a random one on, so the set gets each kind in turn + auto const kinds{m_state.vehicles.size()}; + auto const start{kinds > 0 ? std::min(kinds - 1, static_cast(LocalRandom(0.0, static_cast(kinds)))) : 0}; + for (int index = 0; index < m_state.count && kinds > 0; ++index) { - for (int copy = 0; copy < m_state.copies; ++copy) + auto const &definition{m_state.vehicles[(start + static_cast(index)) % kinds]}; + // a vehicle like any other of the scenery, made where the point is and driven at the speed of the point... + auto *vehicle{new TDynamicObject()}; + auto const length{vehicle->Init(basename + ":" + std::to_string(++m_made), definition.folder, definition.skin, definition.type, m_lanes.front(), m_offset + front_room, "headdriver", + m_state.velocity, "", static_cast(definition.load), definition.loadtype, false, "")}; + if (length == 0.0) { - // a vehicle like any other of the scenery, made where the point is and driven at the speed of the point... - auto *vehicle{new TDynamicObject()}; - auto const length{vehicle->Init(basename + ":" + std::to_string(++m_made), definition.folder, definition.skin, definition.type, m_lanes.front(), m_offset + front_room, "headdriver", - m_state.velocity, "", static_cast(definition.load), definition.loadtype, false, "")}; - if (length == 0.0) - { - ErrorLog("Bad road point: vehicle \"" + definition.folder + "/" + definition.type + "\" of spawn \"" + m_name + "\" couldn't be made"); - if (vehicle->MyTrack != nullptr) - { - vehicle->MyTrack->RemoveDynamicObject(vehicle); - } - delete vehicle; - continue; - } - if (vehicle->mdModel != nullptr) - { - for (auto const &smokesource : vehicle->mdModel->smoke_sources()) - { - simulation::Particles.insert(smokesource.first, vehicle, smokesource.second); - } - } - if (false == simulation::Vehicles.insert(vehicle)) - { - ErrorLog("Bad road point: vehicle name \"" + vehicle->name() + "\" made for spawn \"" + m_name + "\" is taken already"); - } - pooled_vehicle entry; - entry.vehicle = vehicle; - entry.velocity = vehicle->MoverParameters->V; - m_pool.emplace_back(entry); - // ...which is taken off the road right away, the way the vehicles leaving the scenery are, to wait for its turn + ErrorLog("Bad road point: vehicle \"" + definition.folder + "/" + definition.type + "\" of spawn \"" + m_name + "\" couldn't be made"); if (vehicle->MyTrack != nullptr) { vehicle->MyTrack->RemoveDynamicObject(vehicle); - vehicle->MyTrack = nullptr; } - vehicle->bEnabled = false; + delete vehicle; + continue; } + if (vehicle->mdModel != nullptr) + { + for (auto const &smokesource : vehicle->mdModel->smoke_sources()) + { + simulation::Particles.insert(smokesource.first, vehicle, smokesource.second); + } + } + if (false == simulation::Vehicles.insert(vehicle)) + { + ErrorLog("Bad road point: vehicle name \"" + vehicle->name() + "\" made for spawn \"" + m_name + "\" is taken already"); + } + pooled_vehicle entry; + entry.vehicle = vehicle; + entry.velocity = vehicle->MoverParameters->V; + m_pool.emplace_back(entry); + // ...which is taken off the road right away, the way the vehicles leaving the scenery are, to wait for its turn + if (vehicle->MyTrack != nullptr) + { + vehicle->MyTrack->RemoveDynamicObject(vehicle); + vehicle->MyTrack = nullptr; + } + vehicle->bEnabled = false; } if (m_pool.empty()) { @@ -757,6 +791,12 @@ void roadpoint_node::prepare() } } +// true if specified vehicle stands in the line of vehicles held by the crossing +bool roadpoint_node::holds(TDynamicObject const &Vehicle) const +{ + return m_state.kind == kind_type::crossing && m_closed && false == m_editorremoved && std::find(m_queue.begin(), m_queue.end(), Vehicle.MyTrack) != m_queue.end(); +} + std::size_t roadpoint_node::waiting() const { return static_cast(std::count_if(m_pool.begin(), m_pool.end(), [](pooled_vehicle const &Entry) { return false == Entry.vehicle->bEnabled && Entry.vehicle->MyTrack == nullptr; })); @@ -790,6 +830,11 @@ void roadpoint_node::update(double const Deltatime) update_despawn(); break; } + case kind_type::crosswalk: + { + // paint only + break; + } } } @@ -846,6 +891,36 @@ void roadpoint_node::update_spawn(double const Deltatime) // the point was made in the editor, or the scenery was loaded for editing prepare(); } + // vehicles of the point which got stuck are taken off the road, to wait for their next turn: the ones standing still + // for too long, unless it's a closed level crossing they wait at, and the ones which started to go backwards + for (auto &entry : m_pool) + { + auto *vehicle{entry.vehicle}; + if (false == vehicle->bEnabled || vehicle->MyTrack == nullptr || (simulation::Train != nullptr && simulation::Train->Dynamic() == vehicle)) + { + // not on the road, or taken over by the user + entry.standing = 0.0; + continue; + } + auto const speed{vehicle->MoverParameters->V}; + // forward is the way it went when it was put on the road + auto const backwards{speed * entry.velocity < 0.0 && std::abs(speed) > reverse_speed}; + if (std::abs(speed) > stuck_speed || simulation::Roadpoints.holds(*vehicle)) + { + entry.standing = 0.0; + } + else + { + entry.standing += Deltatime; + } + if (backwards || entry.standing > stuck_time) + { + WriteLog("Road traffic: vehicle \"" + vehicle->name() + "\" taken off the road, " + (backwards ? "it was going backwards" : "it stood still for too long")); + entry.standing = 0.0; + vehicle->bEnabled = false; + TDynamicObject::bDynamicRemove = true; + } + } if (m_pool.empty() || m_lanes.empty()) { return; @@ -933,15 +1008,41 @@ void roadpoint_node::update_despawn() std::vector roadpoint_node::create_shapes() const { std::vector shapes; - if (m_state.kind != kind_type::crossing || false == m_state.stoplines || m_stops.empty()) - { - return shapes; - } std::vector vertices; glm::dvec3 const lift{0.0, line_lift, 0.0}; glm::vec3 const upwards{0.f, 1.f, 0.f}; + if (m_state.kind == kind_type::crosswalk && m_onroad) + { + // stripes laid along the road, side by side from one edge of it to the other + glm::dvec3 across{m_along.z, 0.0, -m_along.x}; + if (glm::length2(across) > 1e-12) + { + across = glm::normalize(across); + auto const reach{m_halfwidth - stopline_inset}; + auto const stripes{std::max(1, static_cast(std::floor((2.0 * reach + stripe_width) / (2.0 * stripe_width))))}; + // the stripes are spread evenly over the width, with one at each edge + auto const first{-0.5 * (stripes * 2 - 1) * stripe_width}; + auto const back{-m_along * static_cast(m_state.length)}; + for (int stripe = 0; stripe < stripes; ++stripe) + { + auto const offset{first + stripe * 2.0 * stripe_width}; + auto const right{m_state.position + across * offset + m_along * (0.5 * m_state.length) + lift}; + auto const left{right + across * stripe_width}; + vertices.push_back({right + back, upwards, {0.f, 0.f}}); + vertices.push_back({right, upwards, {1.f, 0.f}}); + vertices.push_back({left + back, upwards, {0.f, 1.f}}); + vertices.push_back({right, upwards, {1.f, 0.f}}); + vertices.push_back({left, upwards, {1.f, 1.f}}); + vertices.push_back({left + back, upwards, {0.f, 1.f}}); + } + } + } for (auto const &stop : m_stops) { + if (m_state.kind != kind_type::crossing || false == m_state.stoplines) + { + break; + } // a line across the lane, ending where the vehicles are to stop glm::dvec3 across{stop.direction.z, 0.0, -stop.direction.x}; if (glm::length2(across) < 1e-12) @@ -993,7 +1094,7 @@ void roadpoint_node::hide() // radius() subclass details, calculates node's bounding radius float roadpoint_node::radius_() { - return m_state.kind == kind_type::crossing ? m_state.clearance + 5.f : m_state.radius; + return m_state.kind == kind_type::crossing ? m_state.clearance + 5.f : m_state.kind == kind_type::crosswalk ? static_cast(m_halfwidth) + m_state.length : m_state.radius; } // serialize() subclass details, sends content of the subclass to provided stream @@ -1025,7 +1126,7 @@ void roadpoint_node::export_as_text_(std::ostream &Output) const } case kind_type::spawn: { - Output << "interval " << m_state.interval << ' ' << "variation " << m_state.variation << ' ' << "velocity " << m_state.velocity << ' ' << "copies " << m_state.copies << ' '; + Output << "interval " << m_state.interval << ' ' << "variation " << m_state.variation << ' ' << "velocity " << m_state.velocity << ' ' << "count " << m_state.count << ' '; for (auto const &vehicle : m_state.vehicles) { // the way vehicles are put in a scenery. the path and the offset are there to keep that form, the point doesn't use them @@ -1043,6 +1144,11 @@ void roadpoint_node::export_as_text_(std::ostream &Output) const Output << "radius " << m_state.radius << ' '; break; } + case kind_type::crosswalk: + { + Output << "length " << m_state.length << ' '; + break; + } } Output << "end" << type << "\n"; } @@ -1110,6 +1216,12 @@ void roadpoint_table::create_geometry() } } +// true if specified vehicle stands in the line of vehicles held by a closed level crossing +bool roadpoint_table::holds(TDynamicObject const &Vehicle) const +{ + return std::any_of(m_items.begin(), m_items.end(), [&Vehicle](roadpoint_node const *Point) { return Point != nullptr && Point->holds(Vehicle); }); +} + // to be called with each step of the simulation void roadpoint_table::update(double const Deltatime) { diff --git a/world/RoadPoint.h b/world/RoadPoint.h index e4a91d46..4b390ba6 100644 --- a/world/RoadPoint.h +++ b/world/RoadPoint.h @@ -24,17 +24,20 @@ http://mozilla.org/MPL/2.0/. class TDynamicObject; class basic_event; -// a place on a road where something happens to the traffic. three kinds of it: +// a place on a road where something happens to the traffic, or is painted for it: // - a level crossing: vehicles are stopped ahead of the rails for as long as a train is near, // - a point where vehicles appear, drawn from a set made ready while the scenery is loaded, -// - a point where vehicles are taken away. the ones which came from a point of the previous kind go back to its set. +// - a point where vehicles are taken away. the ones which came from a point of the previous kind go back to its set, +// - a pedestrian crossing: stripes painted across the road. // scenery entries: // node crossing [clearance ] [warning ] [stoplines ] endcrossing -// node spawn [interval ] [variation <0..1>] [velocity ] [copies ] +// node spawn [interval ] [variation <0..1>] [velocity ] [count ] // [node dynamic [] enddynamic]... endspawn // node despawn [radius ] enddespawn +// node crosswalk [length ] endcrosswalk // the vehicles of a spawn point are given the way vehicles are put in a scenery; where they're put and how they're driven -// is up to the point, so the path, the offset, the driver and the velocity of these entries don't matter +// is up to the point, so the path, the offset, the driver and the velocity of these entries don't matter. the listed vehicles +// are what the point draws from to have vehicles of its own on the roads; one which gets stuck is taken off the road class roadpoint_node : public scene::basic_node { @@ -44,7 +47,8 @@ class roadpoint_node : public scene::basic_node { crossing, spawn, - despawn + despawn, + crosswalk }; struct vehicle_data { @@ -71,10 +75,12 @@ class roadpoint_node : public scene::basic_node float interval{20.f}; // seconds from a vehicle to the next float variation{0.5f}; // part of the interval which is drawn at random float velocity{50.f}; // speed the vehicles appear with, and are told to keep, km/h - int copies{2}; // number of each vehicle made ready + int count{6}; // how many vehicles of the point can be on the roads at a time. that many are made ready, drawn from the listed ones std::vector vehicles; // point where vehicles are taken away float radius{2.f}; // vehicles on lanes this close to the point are taken + // pedestrian crossing + float length{4.f}; // how much of the road the stripes take along it // brings the content to a usable form void normalize(); @@ -135,6 +141,8 @@ class roadpoint_node : public scene::basic_node return m_pool.size(); } std::size_t waiting() const; + // true if specified vehicle stands in the line of vehicles held by the crossing, which is closed + bool holds(TDynamicObject const &Vehicle) const; // generates geometry of the stop lines std::vector create_shapes() const; // puts geometry of the point in the scene, or takes it back @@ -153,6 +161,7 @@ class roadpoint_node : public scene::basic_node { TDynamicObject *vehicle{nullptr}; double velocity{0.0}; // what its speed was when it was made, to be given back each time it's put on the road + double standing{0.0}; // how long it's been standing still on the road }; // methods float radius_() override; @@ -171,6 +180,7 @@ class roadpoint_node : public scene::basic_node std::vector m_stops; std::vector m_events; // orders for the drivers, one for each stop. these are never freed, as a driver can hold on to one std::vector m_rails; // rails a train closes the crossing from + std::vector m_queue; // lanes the vehicles held by the crossing wait on std::map m_seen; // rail vehicles near, with how far each was the last time bool m_closed{false}; double m_hold{0.0}; // time left for the crossing to stay closed after the last train was seen @@ -182,6 +192,10 @@ class roadpoint_node : public scene::basic_node bool m_prepared{false}; int m_made{0}; // number of vehicles made so far, for their names double m_timer{0.0}; // time left to the next vehicle + // pedestrian crossing: the road at the point + bool m_onroad{false}; + glm::dvec3 m_along{0.0, 0.0, 1.0}; // the way the road goes + double m_halfwidth{0.0}; // of the road owned_shapes m_shapes; }; @@ -199,6 +213,8 @@ class roadpoint_table : public basic_table void create_geometry(); // to be called with each step of the simulation void update(double const Deltatime); + // true if specified vehicle stands in the line of vehicles held by a closed level crossing + bool holds(TDynamicObject const &Vehicle) const; private: bool m_geometry{false}; // the scene is ready to take geometry of the points diff --git a/world/Track.cpp b/world/Track.cpp index 3df6aa54..aa5445eb 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1942,6 +1942,29 @@ bool TTrack::SwitchForced(int i, TDynamicObject *o) return true; }; +// picks one of the routes through a crossroads for a driver heading for it +int TTrack::RouteDraw() const { + + auto const count { std::min( RouteCount(), static_cast( m_routeweights.size() ) ) }; + auto total { 0.0 }; + for( int route = 0; route < count; ++route ) { + total += std::max( 0.f, m_routeweights[ route ] ); + } + if( total <= 0.0 ) { + // even odds, the way it's always been for the crossroads of the scenery + return 1 + static_cast( std::floor( Random( static_cast( RouteCount() ) - 0.001 ) ) ); + } + auto draw { Random( total ) }; + auto last { 1 }; + for( int route = 0; route < count; ++route ) { + if( m_routeweights[ route ] <= 0.f ) { continue; } + last = route + 1; + draw -= m_routeweights[ route ]; + if( draw < 0.0 ) { break; } + } + return last; +} + int TTrack::CrossSegment(int from, int into) { // ustawienie wskaźnika na segement w pożądanym kierunku (into) od strony (from) // zwraca kod segmentu, z kierunkiem jazdy jako znakiem ± diff --git a/world/Track.h b/world/Track.h index b894655d..72a6b7ff 100644 --- a/world/Track.h +++ b/world/Track.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -187,6 +188,7 @@ public: std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; 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 + std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; // sleepermodel optional parameter ------------------------------------------------- @@ -280,6 +282,8 @@ public: int RouteCount() const { return SwitchExtension != nullptr ? SwitchExtension->iRoads - 1 : 1; } + // picks one of the routes through a crossroads for a driver heading for it. the routes are numbered from 1 + int RouteDraw() const; void Load(cParser *parser, glm::dvec3 const &pOrigin); void init_segments( bool const Initial ); void update_location();