diff --git a/CMakeLists.txt b/CMakeLists.txt index bdda3ea9..09144647 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ set(SOURCES "utilities/Timer.cpp" "world/Track.cpp" "world/Road.cpp" +"world/RoadPoint.cpp" "world/Traction.cpp" "world/TractionPower.cpp" "vehicle/Train.cpp" diff --git a/application/editormode.h b/application/editormode.h index 15d5de4d..47f21892 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -500,9 +500,10 @@ class editor_mode : public application_mode { bool window{false}; // the road window is open, the left mouse button belongs to its tools bool lanes{true}; // lanes of the roads are drawn as trajectories - int tool{0}; // 0: select, 1: build + int tool{0}; // 0: select, 1: build, 2: place road_node *selected{nullptr}; junction_node *junction{nullptr}; // selected junction + roadpoint_node *marker{nullptr}; // selected level crossing or traffic point std::vector points; // selected points where the pieces meet or end road_node::state settings; // layout shown in the window: of the selected piece, or of the pieces about to be built bool whole{true}; // layout changes go to every piece of the road the selected one belongs to @@ -522,6 +523,17 @@ 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 + // 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 + 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}; + glm::dvec3 targetdirection{0.0}; // the way the traffic goes there + std::string targetnote; // what's worth knowing about that point, or why there's none + glm::dvec3 aimed{0.0}; // where the cursor was when the target was worked out, and for what kind of point + int aimedkind{-1}; + char vehicles[4096]{}; // vehicles to add to the set of a spawn point, written the way they're put in a scenery road_plan plan; // what a click would make at the moment, worked out once a frame for display glm::dvec3 hover{0.0}; // point of a road or a junction under the cursor a road can be led out of bool hashover{false}; @@ -550,6 +562,16 @@ class editor_mode : public application_mode void road_point_width(float const Width); void road_point_heights(std::vector const &Heights); bool road_points_apply(std::vector> const &Changes, std::vector> const &Junctions); + // takes the selected points out of their roads + bool road_points_delete(); + // 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 + void roadpoint_aim(bool const Fresh); + void roadpoint_place(); + void roadpoint_apply(roadpoint_node &Point, roadpoint_node::state const &State); + // Point: the point the controls are for, nullptr for the one about to be made + bool render_roadpoint_layout(roadpoint_node::state &State, roadpoint_node const *Point); // Fresh: the ground which wasn't looked up yet is gathered on the spot, instead of a bit of it each frame road_plan road_preview(bool const Fresh); // Firstgrade, Lastgrade: slopes the road has to start and end with, nullptr for none diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 59c78b98..4c38c798 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "world/Track.h" #include "world/Road.h" +#include "world/RoadPoint.h" #include "simulation/simulation.h" #include "imgui/imgui.h" @@ -133,6 +134,10 @@ double const kSnapRadius{4.0}; // an end of a road this close to the cursor is w double const kPointRadius{2.5}; // a point where pieces meet this close to the click is what gets selected, instead of the piece double const kPointRange{400.0}; // the points are drawn for the roads this close to the camera double const kSideMargin{1.0}; // how far past the edge of a road the cursor still counts as being on it +double const kMarkerRadius{2.0}; // a level crossing or a traffic point this close to the click is what gets selected +double const kRailReach{40.0}; // rails crossing the road this close to the cursor are taken for a single level crossing +double const kLaneReach{2.5}; // a lane with its middle this close to the cursor is the one a traffic point is put on +double const kFullTurn{6.283185307179586}; double const kJoinClearance{15.0}; // a road isn't led into the side of another this close to the point it's led from double const kReach{3000.0}; // clicks landing further than this are taken for misses double const kLaneRange{600.0}; // lanes are drawn for the roads this close to the camera @@ -151,6 +156,19 @@ ImU32 const kSelectedColor{IM_COL32(40, 220, 255, 230)}; ImU32 const kPreviewColor{IM_COL32(255, 210, 60, 255)}; ImU32 const kRefusedColor{IM_COL32(240, 60, 60, 255)}; 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)}; + +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; +} + +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"; +} // gathers new definitions for the pieces which meet or end at provided points. // Change: receives a definition, which end of it is at the point, and the number of the point @@ -209,6 +227,8 @@ void editor_mode::update_road_tool() tool.selected = nullptr; if (tool.junction != nullptr && tool.junction->m_editorremoved) tool.junction = nullptr; + if (tool.marker != nullptr && tool.marker->m_editorremoved) + tool.marker = nullptr; if ((tool.branchroad != nullptr && tool.branchroad->m_editorremoved) || (tool.branchjunction != nullptr && tool.branchjunction->m_editorremoved)) { // what the road was to be led out of is gone @@ -220,6 +240,11 @@ void editor_mode::update_road_tool() tool.points.erase(std::remove_if(tool.points.begin(), tool.points.end(), [](glm::dvec3 const &Point) { return editor_road::ends_at(Point).empty(); }), tool.points.end()); tool.plan = road_plan{}; tool.hashover = false; + if (false == tool.window || tool.tool != 2) + { + tool.hastarget = false; + tool.aimedkind = -1; + } if (false == tool.window) { tool.chain = false; @@ -230,6 +255,14 @@ void editor_mode::update_road_tool() if (glm::distance(tool.mouse, camera) > kReach) { tool.plan.error = "The cursor isn't over anything near enough"; + tool.hastarget = false; + tool.aimedkind = -1; + return; + } + if (tool.tool == 2) + { + // the point a click would make at the moment, to be drawn over the view + roadpoint_aim(false); return; } if (tool.tool != 1) @@ -289,6 +322,280 @@ void editor_mode::junction_select(junction_node *Junction) copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material); } +void editor_mode::roadpoint_select(roadpoint_node *Point) +{ + auto &tool{m_roadtool}; + tool.marker = Point; + if (Point == nullptr) + return; + std::string reason; + tool.status = "Selected: " + Point->name(); + if (false == editor_road::can_edit(*Point, &reason)) + tool.status += ". It can't be changed: " + reason; +} + +// works out the point a click of the place tool would make with the cursor where it is +void editor_mode::roadpoint_aim(bool const Fresh) +{ + auto &tool{m_roadtool}; + if (false == Fresh && tool.aimedkind == tool.placekind && glm::distance(tool.aimed, tool.mouse) < 0.1) + { + // the cursor didn't move, so neither did what it points at + return; + } + tool.aimed = tool.mouse; + tool.aimedkind = tool.placekind; + auto const kind{point_kind(tool.placekind)}; + tool.target = tool.placing[tool.placekind]; + tool.target.kind = kind; + tool.hastarget = false; + tool.targetnote.clear(); + tool.targetdirection = glm::dvec3{0.0}; + if (kind == roadpoint_node::kind_type::crossing) + { + auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; + if (road == nullptr) + { + tool.targetnote = "Point at the place a road crosses the rails"; + return; + } + glm::dvec3 middle{0.0}; + double halfspan{0.0}; + if (editor_road::rails_across(*road, tool.mouse, kRailReach, middle, halfspan)) + { + // the crossing goes halfway between the outermost tracks. the distance is rounded up, so it reads well in the scenery file + tool.target.position = middle; + tool.target.clearance = static_cast(std::ceil((halfspan + tool.stopmargin) * 2.0) * 0.5); + } + else + { + tool.target.position = road->definition().point(editor_road::nearest_parameter(road->definition(), tool.mouse)); + tool.target.clearance = std::max(tool.stopmargin, 2.0f); + tool.targetnote = "No rails cross the road here; a crossing put here won't close until some do"; + } + tool.hastarget = true; + return; + } + if (false == editor_road::nearest_lane(tool.mouse, kLaneReach, tool.target.position, tool.targetdirection)) + { + tool.targetnote = "Point at a lane of a road"; + return; + } + tool.hastarget = true; +} + +// puts down the point the place tool is set for, as a step which can be taken back +void editor_mode::roadpoint_place() +{ + auto &tool{m_roadtool}; + roadpoint_aim(true); + if (false == tool.hastarget) + { + tool.status = tool.targetnote; + return; + } + auto const created{editor_road::create(std::vector{tool.target})}; + if (created.empty()) + { + tool.status = "No scenery is loaded"; + return; + } + auto const *point{created.front()}; + auto const &state{point->definition()}; + editor_road::record record; + record.points_created = created; + push_road_snapshot(std::move(record)); + char text[64]; + switch (state.kind) + { + case roadpoint_node::kind_type::crossing: + { + std::snprintf(text, sizeof(text), "%.1f", state.clearance); + tool.status = "Level crossing made: " + std::to_string(point->stops().size()) + " lanes stop " + text + " m from its middle, "; + tool.status += std::to_string(point->rails()) + " tracks are watched for trains."; + break; + } + 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."); + break; + } + case roadpoint_node::kind_type::despawn: + { + tool.status = "Removal point made, it takes vehicles from " + std::to_string(point->lanes().size()) + (point->lanes().size() == 1 ? " lane." : " lanes."); + break; + } + } + if (false == tool.targetnote.empty()) + tool.status += " " + tool.targetnote + "."; +} + +// replaces the definition of a level crossing or a traffic point, as a step which can be taken back +void editor_mode::roadpoint_apply(roadpoint_node &Point, roadpoint_node::state const &State) +{ + auto &tool{m_roadtool}; + std::string reason; + if (false == editor_road::can_edit(Point, &reason)) + { + tool.status = "\"" + Point.name() + "\" can't be changed: " + reason; + return; + } + editor_road::record record; + record.points.emplace_back(&Point, Point.definition()); + editor_road::apply(std::vector>{{&Point, State}}); + push_road_snapshot(std::move(record)); + tool.status = "Changed"; +} + +// draws controls for a level crossing or a traffic point. returns: true if something was changed +bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoint_node const *Point) +{ + auto &tool{m_roadtool}; + bool changed{false}; + // values typed for a point which is there already are taken when confirmed, so the point isn't made anew with each digit + auto const flags{Point != nullptr ? ImGuiInputTextFlags_EnterReturnsTrue : ImGuiInputTextFlags_None}; + auto const number = [flags](char const *Label, float &Value, float const Step, char const *Format) { + ImGui::SetNextItemWidth(120.0f); + return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, flags); + }; + ImVec4 const warning{1.0f, 0.6f, 0.3f, 1.0f}; + switch (State.kind) + { + case roadpoint_node::kind_type::crossing: + { + if (Point != nullptr) + { + ImGui::Text("%d lanes stop here, %d tracks are watched for trains", static_cast(Point->stops().size()), static_cast(Point->rails())); + if (Point->rails() == 0) + ImGui::TextColored(warning, "No rails pass through it, so it never closes"); + // the crossing is tied to what's there when it's made or changed, and when the roads change + if (ImGui::Button("Look for the rails again")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "For when the tracks were laid or changed after the crossing was made."); + if (number("Stop this far from the middle [m]", State.clearance, 0.5f, "%.1f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the middle of the crossing.\n" + "The tracks passing within that distance from the middle are the ones the crossing is closed for."); + } + else + { + if (number("Stop this far from the outermost track [m]", tool.stopmargin, 0.5f, "%.1f")) + tool.stopmargin = std::clamp(tool.stopmargin, 2.0f, 50.0f); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the axis of the first track they'd get to."); + } + if (number("Closes with a train this far away [m]", State.warning, 50.0f, "%.0f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Measured along the tracks, both ways, with both ways of each switch counted.\n" + "A rail vehicle within that distance which gets nearer closes the crossing, and so does one right at it.\n" + "It opens a few seconds after the last one is gone."); + if (ImGui::Checkbox("Paint the stop lines", &State.stoplines)) + changed = true; + break; + } + case roadpoint_node::kind_type::spawn: + { + if (Point != nullptr) + { + if (Point->lanes().empty()) + ImGui::TextColored(warning, "It isn't on a lane, so nothing appears here"); + else if (Point->vehicles() == 0) + ImGui::TextDisabled("The vehicles are made when the simulation runs"); + else + ImGui::Text("%d vehicles made, %d of them wait for their turn", static_cast(Point->vehicles()), static_cast(Point->waiting())); + } + if (number("A vehicle every [s]", State.interval, 1.0f, "%.0f")) + changed = true; + if (number("Uneven by [0-1]", State.variation, 0.1f, "%.2f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Part of the time between vehicles which is drawn at random each time.\n" + "0: they come like clockwork, 1: anything from one right after another to twice the time."); + if (number("Speed [km/h]", State.velocity, 5.0f, "%.0f")) + changed = true; + 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)) + 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."); + // the set + ImGui::Separator(); + ImGui::Text("Vehicles of the set: %d", static_cast(State.vehicles.size())); + int removed{-1}; + for (std::size_t index = 0; index < State.vehicles.size(); ++index) + { + auto const &vehicle{State.vehicles[index]}; + ImGui::PushID(static_cast(index)); + if (ImGui::SmallButton("x")) + removed = static_cast(index); + ImGui::SameLine(); + if (vehicle.load > 0) + ImGui::Text("%s / %s, %s, %d %s", vehicle.folder.c_str(), vehicle.type.c_str(), vehicle.skin.c_str(), vehicle.load, vehicle.loadtype.c_str()); + else + ImGui::Text("%s / %s, %s", vehicle.folder.c_str(), vehicle.type.c_str(), vehicle.skin.c_str()); + ImGui::PopID(); + } + if (removed >= 0) + { + State.vehicles.erase(State.vehicles.begin() + removed); + changed = true; + } + if (State.vehicles.empty()) + ImGui::TextColored(warning, "No vehicles, so nothing appears here"); + ImGui::InputTextMultiline("##vehicles", tool.vehicles, sizeof(tool.vehicles), ImVec2(-1.0f, ImGui::GetTextLineHeight() * 5.0f)); + if (ImGui::Button("Add to the set")) + { + auto const parsed{roadpoint_node::parse_vehicles(tool.vehicles)}; + if (parsed.empty()) + { + tool.status = "No vehicle found in the text"; + } + else + { + State.vehicles.insert(State.vehicles.end(), parsed.begin(), parsed.end()); + tool.vehicles[0] = 0; + changed = true; + } + } + ImGui::SameLine(); + if (ImGui::Button("Empty the set") && false == State.vehicles.empty()) + { + State.vehicles.clear(); + changed = true; + } + ImGui::TextDisabled("Paste the vehicles the way they're put in a scenery:"); + ImGui::TextDisabled("node -1 0 none dynamic "); + ImGui::TextDisabled(" enddynamic"); + ImGui::TextDisabled("or as , one vehicle a line."); + ImGui::TextDisabled("The path, the offset, the driver and the velocity don't matter."); + break; + } + case roadpoint_node::kind_type::despawn: + { + if (Point != nullptr) + ImGui::Text("Takes vehicles from %d %s", static_cast(Point->lanes().size()), Point->lanes().size() == 1 ? "lane" : "lanes"); + if (number("Takes vehicles this close [m]", State.radius, 0.5f, "%.1f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Road vehicles on the lanes passing within this distance from the point are taken off the road when they get here,\n" + "except for the one the user drives. Vehicles which came from a spawn point go back to wait for their next turn.\n" + "The distance it starts with takes in a single lane."); + break; + } + } + if (changed && Point != nullptr) + State.normalize(); + return changed; +} + // replaces the definition of a junction, as a step which can be taken back void editor_mode::junction_apply(junction_node &Junction, junction_node::state const &State) { @@ -636,8 +943,23 @@ void editor_mode::road_click() return; } tool.mouse = ground; + if (tool.tool == 2) + { + roadpoint_place(); + return; + } if (tool.tool == 0) { + if (auto *marker{editor_road::nearest_point(ground, kMarkerRadius)}; marker != nullptr) + { + // a level crossing or a traffic point + tool.points.clear(); + tool.junction = nullptr; + road_select(nullptr); + roadpoint_select(marker); + return; + } + tool.marker = nullptr; glm::dvec3 joint; if (editor_road::nearest_joint(ground, kPointRadius, joint)) { @@ -883,13 +1205,29 @@ void editor_mode::road_apply() bool editor_mode::road_delete() { auto &tool{m_roadtool}; - if (false == tool.window || (tool.selected == nullptr && tool.junction == nullptr)) - { - // with points selected the key isn't meant for whatever else is selected in the editor - return tool.window && false == tool.points.empty(); - } + if (false == tool.window) + return false; std::string reason; editor_road::record record; + if (tool.marker != nullptr) + { + auto *point{tool.marker}; + if (false == editor_road::can_edit(*point, &reason)) + { + tool.status = "Can't delete: " + reason; + return true; + } + editor_road::remove(std::vector{point}); + record.points_removed.emplace_back(point); + push_road_snapshot(std::move(record)); + tool.marker = nullptr; + tool.status = "\"" + point->name() + "\" deleted, Ctrl+Z brings it back"; + return true; + } + if (false == tool.points.empty()) + return road_points_delete(); + if (tool.selected == nullptr && tool.junction == nullptr) + return false; if (tool.junction != nullptr) { auto *junction{tool.junction}; @@ -919,6 +1257,30 @@ bool editor_mode::road_delete() return true; } +// takes the selected points out of their roads +bool editor_mode::road_points_delete() +{ + auto &tool{m_roadtool}; + editor_road::record record; + std::string notes; + int taken{0}; + for (auto const &point : tool.points) + { + std::string error; + if (editor_road::dissolve(point, record, error)) + ++taken; + else if (notes.find(error) == std::string::npos) + notes += " A point was left: " + error + "."; + } + push_road_snapshot(std::move(record)); + // the points which stayed are still selected, so it's clear which ones these are + tool.points.erase(std::remove_if(tool.points.begin(), tool.points.end(), [](glm::dvec3 const &Point) { return editor_road::ends_at(Point).empty(); }), tool.points.end()); + tool.status = (taken == 0 ? std::string{"Nothing deleted."} : taken == 1 ? std::string{"Point deleted, Ctrl+Z brings it back."} : + std::to_string(taken) + " points deleted, Ctrl+Z brings them back."); + tool.status += notes; + return true; +} + bool editor_mode::road_split() { auto &tool{m_roadtool}; @@ -1049,8 +1411,14 @@ void editor_mode::push_road_snapshot(editor_road::record Record) node = Record.junctions.front().first; else if (false == Record.junctions_created.empty()) node = Record.junctions_created.front(); - else + else if (false == Record.junctions_removed.empty()) node = Record.junctions_removed.front(); + else if (false == Record.points.empty()) + node = Record.points.front().first; + else if (false == Record.points_created.empty()) + node = Record.points_created.front(); + else + node = Record.points_removed.front(); EditorSnapshot snap; snap.action = EditorSnapshot::Action::RoadEdit; snap.node_name = node->name(); @@ -1072,6 +1440,8 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec entry.second = entry.first->definition(); for (auto &entry : current.roads.junctions) entry.second = entry.first->definition(); + for (auto &entry : current.roads.points) + entry.second = entry.first->definition(); Opposite.push_back(std::move(current)); // things are taken out before the rest is changed and put in after it, so nothing gets joined with what's on its way out @@ -1079,23 +1449,28 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec { editor_road::remove(record.roads_created); editor_road::remove(record.junctions_created); + editor_road::remove(record.points_created); } else { editor_road::remove(record.roads_removed); editor_road::remove(record.junctions_removed); + editor_road::remove(record.points_removed); } editor_road::apply(record.roads); editor_road::apply(record.junctions); + editor_road::apply(record.points); if (Undo) { editor_road::revive(record.roads_removed); editor_road::revive(record.junctions_removed); + editor_road::revive(record.points_removed); } else { editor_road::revive(record.roads_created); editor_road::revive(record.junctions_created); + editor_road::revive(record.points_created); } auto &tool{m_roadtool}; @@ -1104,6 +1479,9 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); if (tool.junction != nullptr) junction_select(tool.junction->m_editorremoved ? nullptr : tool.junction); + if (tool.marker != nullptr && tool.marker->m_editorremoved) + tool.marker = nullptr; + tool.aimedkind = -1; } // a field for the name of a material, with a list of the ones in the texture folder to pick from. returns: true if the material was changed @@ -1333,6 +1711,10 @@ void editor_mode::render_road_window() ImGui::RadioButton("Select", &tool.tool, 0); ImGui::SameLine(); ImGui::RadioButton("Build", &tool.tool, 1); + ImGui::SameLine(); + ImGui::RadioButton("Place", &tool.tool, 2); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Level crossings, and the points where vehicles appear on the roads or are taken off them"); if (tool.tool != previoustool) { road_cancel(); @@ -1340,6 +1722,7 @@ void editor_mode::render_road_window() { tool.junction = nullptr; tool.points.clear(); + tool.marker = nullptr; // the fields for the names of materials show the layout for the roads to be built again road_select(nullptr); } @@ -1379,6 +1762,45 @@ void editor_mode::render_road_window() // what's set here is what the next pieces get render_road_layout(tool.settings); } + else if (tool.tool == 2) + { + ImGui::RadioButton("Level crossing", &tool.placekind, 0); + ImGui::SameLine(); + ImGui::RadioButton("Spawn point", &tool.placekind, 1); + ImGui::SameLine(); + ImGui::RadioButton("Removal point", &tool.placekind, 2); + tool.placekind = std::clamp(tool.placekind, 0, 2); + switch (tool.placekind) + { + case 0: + ImGui::TextDisabled("LMB on a road where the rails cross it. The vehicles stop ahead"); + ImGui::TextDisabled("of the rails for as long as a train is coming."); + break; + case 1: + 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; + 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."); + break; + } + ImGui::Separator(); + // what's set here is what the next points get + auto &placing{tool.placing[tool.placekind]}; + placing.kind = point_kind(tool.placekind); + render_roadpoint_layout(placing, nullptr); + } + else if (tool.marker != nullptr) + { + auto &point{*tool.marker}; + auto state{point.definition()}; + ImGui::Text("%s, %s", point.name().c_str(), point_label(state.kind)); + if (render_roadpoint_layout(state, &point)) + roadpoint_apply(point, state); + if (ImGui::Button("Delete (Del)")) + road_delete(); + } else if (false == tool.points.empty()) { // the road at the selected points @@ -1428,6 +1850,12 @@ void editor_mode::render_road_window() if (ImGui::IsItemHovered()) ImGui::SetTooltip("Puts each point %.2f m over the ground under it; the height above the ground is set in the Build tool.", tool.offset); } + if (ImGui::Button(tool.points.size() == 1 ? "Delete the point (Del)" : "Delete the points (Del)")) + road_points_delete(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Takes the point out of the road: the pieces on its two sides become one piece,\n" + "led from the far end of one to the far end of the other the way the road goes there.\n" + "A point the road ends at takes the last piece of the road with it."); } else if (tool.junction != nullptr) { @@ -1456,7 +1884,8 @@ void editor_mode::render_road_window() } else { - ImGui::TextDisabled("LMB: select a road piece, a junction, or a point where pieces meet"); + ImGui::TextDisabled("LMB: select a road piece, a junction, a point where pieces meet,"); + ImGui::TextDisabled("a level crossing or a traffic point"); } if (false == tool.status.empty()) { @@ -1587,6 +2016,77 @@ void editor_mode::draw_road_overlay() const drawpoint(arm.position, Color, 5.0f, true); }; + // a circle lying on the ground + auto const drawring = [&](glm::dvec3 const &Centre, double const Radius, ImU32 const Color) { + int const pieces{24}; + glm::dvec3 previous{Centre + glm::dvec3{Radius, 0.0, 0.0}}; + for (int i = 1; i <= pieces; ++i) + { + auto const angle{kFullTurn * i / pieces}; + glm::dvec3 const next{Centre + glm::dvec3{Radius * std::cos(angle), 0.0, Radius * std::sin(angle)}}; + projection.line(drawlist, previous, next, Color, 2.0f); + previous = next; + } + }; + auto const drawlabel = [&](glm::dvec3 const &Point, ImU32 const Color, char const *Text) { + ImVec2 screen; + if (false == projection.room(drawlist) || false == projection.project(Point, screen) || projection.outside(screen, screen)) + return; + drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 8.0f), Color, Text); + }; + // a level crossing or a traffic point. Point: the node it's drawn for, nullptr for one which isn't made yet + auto const drawmarker = [&](roadpoint_node::state const &State, roadpoint_node const *Point, ImU32 const Color) { + drawpoint(State.position, Color, 6.0f, true); + auto const labelled{glm::distance(State.position, camera) < 200.0}; + switch (State.kind) + { + case roadpoint_node::kind_type::crossing: + { + // the tracks within the circle are the ones it's closed for, the lines are where the vehicles stop + drawring(State.position, State.clearance, Color); + auto const closed{Point != nullptr && Point->closed()}; + if (Point != nullptr) + { + for (auto const &stop : Point->stops()) + { + glm::dvec3 across{stop.direction.z, 0.0, -stop.direction.x}; + if (glm::length(across) < 1e-6) + continue; + across = glm::normalize(across) * (0.5 * stop.width); + projection.line(drawlist, stop.position - across, stop.position + across, closed ? kRefusedColor : Color, 4.0f); + } + } + if (labelled) + drawlabel(State.position, Color, closed ? "crossing, closed" : "crossing"); + break; + } + case roadpoint_node::kind_type::spawn: + { + drawpoint(State.position, Color, 11.0f, false); + if (labelled) + drawlabel(State.position, Color, "spawn"); + break; + } + case roadpoint_node::kind_type::despawn: + { + drawring(State.position, State.radius, Color); + if (labelled) + drawlabel(State.position, Color, "removal"); + break; + } + } + }; + for (auto const *point : simulation::Roadpoints.sequence()) + { + if (point == nullptr || point->m_editorremoved || glm::distance(point->location(), camera) > kPointRange) + 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); + if (point == tool.marker) + drawpoint(point->definition().position, kSelectedColor, 15.0f, false); + } + if (tool.selected != nullptr && false == tool.selected->m_editorremoved) { auto const &state{tool.selected->definition()}; @@ -1649,6 +2149,30 @@ void editor_mode::draw_road_overlay() const } } } + else if (tool.tool == 2) + { + if (tool.hastarget) + { + drawmarker(tool.target, nullptr, kPreviewColor); + if (glm::length(tool.targetdirection) > 1e-6) + { + // the way the traffic goes on the lane + auto const direction{glm::normalize(tool.targetdirection)}; + glm::dvec3 const across{direction.z, 0.0, -direction.x}; + auto const tip{tool.target.position + direction * 5.0}; + projection.line(drawlist, tool.target.position, tip, kPreviewColor, 2.0f); + projection.line(drawlist, tip, tip - direction * 1.2 + across * 0.8, kPreviewColor, 2.0f); + projection.line(drawlist, tip, tip - direction * 1.2 - across * 0.8, kPreviewColor, 2.0f); + } + hint = std::string{"LMB: put a "} + point_label(tool.target.kind) + " here"; + if (false == tool.targetnote.empty()) + hint += " " + tool.targetnote; + } + else + { + hint = (tool.targetnote.empty() ? std::string{"LMB: put a "} + point_label(point_kind(tool.placekind)) + " on a road" : tool.targetnote); + } + } else { // points where the pieces meet or end, to pick from @@ -1664,10 +2188,11 @@ void editor_mode::draw_road_overlay() const drawpoint(point, kSelectedColor, 6.0f, true); drawpoint(point, kSelectedColor, 11.0f, false); } - hint = false == tool.points.empty() ? "LMB: select a point Shift+LMB: add a point or take it out Ctrl+Z: undo" : - tool.selected != nullptr ? "LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo" : - tool.junction != nullptr ? "LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo" : - "LMB: select a piece, a junction or a point (white dot)"; + hint = tool.marker != nullptr ? "LMB: select a piece, a junction, a point or a marker Del: delete Ctrl+Z: undo" : + false == tool.points.empty() ? "LMB: select a point Shift+LMB: add a point or take it out Del: delete the points Ctrl+Z: undo" : + tool.selected != nullptr ? "LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo" : + tool.junction != nullptr ? "LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo" : + "LMB: select a piece, a junction, a point (white dot) or a marker"; } // shown above the place the track tools put their hints at ImGuiIO const &io = ImGui::GetIO(); diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index d713bfc1..339ff00a 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -317,6 +317,66 @@ void retire(std::vector const &Tracks) editor_track::retire(*track); } +// value of the curve parameter for the point of a curve nearest to specified point, seen from above +double nearest_on(std::array const &Points, glm::dvec3 const &Point) +{ + auto const distance = [&](double const T) { + auto const position{curve_point(Points, T)}; + return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z}); + }; + double best{0.0}; + for (int i = 1; i <= 64; ++i) + if (distance(i / 64.0) < distance(best)) + best = i / 64.0; + double step{1.0 / 64.0}; + for (int i = 0; i < 24; ++i) + { + step *= 0.5; + if (best - step >= 0.0 && distance(best - step) < distance(best)) + best -= step; + else if (best + step <= 1.0 && distance(best + step) < distance(best)) + best += step; + } + return best; +} + +double planar_distance(glm::dvec3 const &Left, glm::dvec3 const &Right) +{ + return glm::distance(glm::dvec2{Left.x, Left.z}, glm::dvec2{Right.x, Right.z}); +} + +// true if two lines cross when seen from above. Along, Otheralong: receive how far along each of them the crossing is, 0 to 1 +bool lines_cross(glm::dvec3 const &Start, glm::dvec3 const &End, glm::dvec3 const &Otherstart, glm::dvec3 const &Otherend, double &Along, double &Otheralong) +{ + glm::dvec2 const line{End.x - Start.x, End.z - Start.z}; + glm::dvec2 const other{Otherend.x - Otherstart.x, Otherend.z - Otherstart.z}; + auto const denominator{line.x * other.y - line.y * other.x}; + if (std::abs(denominator) < 1e-12) + return false; + glm::dvec2 const offset{Otherstart.x - Start.x, Otherstart.z - Start.z}; + Along = (offset.x * other.y - offset.y * other.x) / denominator; + Otheralong = (offset.x * line.y - offset.y * line.x) / denominator; + return Along >= 0.0 && Along <= 1.0 && Otheralong >= 0.0 && Otheralong <= 1.0; +} + +// the same piece of a road laid out from its other end: what it looks like and where its lanes lead stays as it was +road_node::state flipped(road_node::state const &State) +{ + auto result{State}; + std::swap(result.axis.points[segment_data::point::start], result.axis.points[segment_data::point::end]); + std::swap(result.axis.points[segment_data::point::control1], result.axis.points[segment_data::point::control2]); + result.axis.rolls = {-State.axis.rolls[1], -State.axis.rolls[0]}; + std::swap(result.forward, result.backward); + // the lanes are listed left to right for someone facing along the axis, who's now looking the other way + std::reverse(result.lanes.begin(), result.lanes.end()); + std::reverse(result.changes.begin(), result.changes.end()); + for (auto &change : result.changes) + 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]); + return result; +} + // smallest radius of the bend made by a bezier curve, seen from above; 0 if the curve is practically straight double tightest_radius(segment_data const &Path) { @@ -468,6 +528,8 @@ void editor_road::refresh(std::vector const &Roads, std::vector affected; std::vector crossings; + // level crossings and traffic points hold on to the lanes, which are about to be replaced + simulation::Roadpoints.unbind(); auto const gather = [&]() { std::vector roads{affected}; std::vector junctions{crossings}; @@ -541,6 +603,7 @@ void editor_road::refresh(std::vector const &Roads, std::vectorm_editorremoved) road->close_ends(); + simulation::Roadpoints.bind(); } std::vector editor_road::create(std::vector const &States) @@ -702,6 +765,288 @@ void editor_road::revive(std::vector const &Junctions) }); } +bool editor_road::can_edit(roadpoint_node const &Point, std::string *Reason) +{ + std::string reason; + auto editable{true}; + if (simulation::Region == nullptr) + { + reason = "No scenery is loaded"; + editable = false; + } + else + { + editable = scene::Layers.editable(&Point, &reason); + } + if (false == editable && Reason != nullptr) + *Reason = reason; + return editable; +} + +std::vector editor_road::create(std::vector const &States) +{ + static int counter{0}; + std::vector created; + if (simulation::Region == nullptr) + return created; + for (auto const &state : States) + { + scene::node_data data; + data.type = roadpoint_node::keyword(state.kind); + data.range_max = -1.0; + data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing + // the vehicles of a spawn point are named after it, so each point needs a name of its own + do + { + data.name = data.type + "_" + std::to_string(++counter); + } while (simulation::Roadpoints.find(data.name) != nullptr); + auto *point{new roadpoint_node(data)}; + point->define(state); + point->mark_dirty(); + simulation::Roadpoints.insert(point); + scene::Layers.count(point->layer(), scene::layer_item::track); + point->bind(); + point->show(); + created.emplace_back(point); + } + return created; +} + +void editor_road::apply(std::vector> const &Changes) +{ + for (auto const &change : Changes) + { + auto *point{change.first}; + if (point == nullptr) + continue; + point->hide(); + point->unbind(); + point->define(change.second); + point->mark_dirty(); + point->bind(); + point->show(); + } +} + +void editor_road::remove(std::vector const &Points) +{ + for (auto *point : Points) + { + if (point == nullptr || point->m_editorremoved) + continue; + point->hide(); + // a crossing which goes lets the vehicles it was holding drive on + point->unbind(); + point->m_editorremoved = true; + scene::Layers.count(point->layer(), scene::layer_item::track, -1); + } +} + +void editor_road::revive(std::vector const &Points) +{ + for (auto *point : Points) + { + if (point == nullptr || false == point->m_editorremoved) + continue; + point->m_editorremoved = false; + point->mark_dirty(); + scene::Layers.count(point->layer(), scene::layer_item::track); + point->bind(); + point->show(); + } +} + +roadpoint_node *editor_road::nearest_point(glm::dvec3 const &Point, double const Radius) +{ + roadpoint_node *result{nullptr}; + auto best{Radius}; + for (auto *point : simulation::Roadpoints.sequence()) + { + if (point == nullptr || point->m_editorremoved) + continue; + auto const &position{point->definition().position}; + auto const distance{planar_distance(position, Point)}; + if (distance > best || std::abs(position.y - Point.y) > 3.0) + continue; + best = distance; + result = point; + } + return result; +} + +bool editor_road::nearest_lane(glm::dvec3 const &Point, double const Reach, glm::dvec3 &Position, glm::dvec3 &Direction) +{ + auto best{Reach}; + auto found{false}; + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const &state{road->definition()}; + 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) + continue; + for (auto const *lane : road->tracks()) + { + if (lane == nullptr || lane->m_paths.empty()) + continue; + auto const points{curve_points(lane->m_paths.front())}; + auto const t{nearest_on(points, Point)}; + auto const position{curve_point(points, t)}; + auto const distance{planar_distance(position, Point)}; + // lanes passing over or under the point don't count + if (distance > best || std::abs(position.y - Point.y) > 3.0) + continue; + best = distance; + Position = position; + // the lanes are laid out the way the traffic goes + Direction = curve_point(points, std::min(1.0, t + 0.01)) - curve_point(points, std::max(0.0, t - 0.01)); + found = true; + } + } + return found; +} + +bool editor_road::rails_across(road_node const &Road, glm::dvec3 const &Near, double const Reach, glm::dvec3 &Middle, double &Halfspan) +{ + int const pieces{32}; // lines the axis is taken for + int const railpieces{16}; // and each of the rails + auto const axis{curve_points(Road.definition().axis)}; + std::vector line; + for (int i = 0; i <= pieces; ++i) + line.emplace_back(curve_point(axis, static_cast(i) / pieces)); + // values of the curve parameter of the axis the rails cross it at + auto first{2.0}; + auto last{-1.0}; + for (auto const *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0) + continue; + for (auto const &path : track->m_paths) + { + auto const rail{curve_points(path)}; + // the path is no longer than the lines joining its points, which is good enough to tell it's nowhere near + if (planar_distance(rail[0], Near) > glm::distance(rail[0], rail[1]) + glm::distance(rail[1], rail[2]) + glm::distance(rail[2], rail[3]) + Reach) + continue; + auto previous{rail[0]}; + for (int j = 1; j <= railpieces; ++j) + { + auto const next{curve_point(rail, static_cast(j) / railpieces)}; + for (int i = 0; i < pieces; ++i) + { + double along{0.0}; + double railalong{0.0}; + if (false == lines_cross(line[i], line[i + 1], previous, next, along, railalong)) + continue; + auto const point{glm::mix(line[i], line[i + 1], along)}; + // rails passing over or under the road don't make a level crossing + if (planar_distance(point, Near) > Reach || std::abs(glm::mix(previous.y, next.y, railalong) - point.y) > 3.0) + continue; + auto const t{(i + along) / pieces}; + first = std::min(first, t); + last = std::max(last, t); + } + previous = next; + } + } + } + if (last < first) + return false; + Middle = curve_point(axis, 0.5 * (first + last)); + Halfspan = std::max(planar_distance(curve_point(axis, first), Middle), planar_distance(curve_point(axis, last), Middle)); + return true; +} + +bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string &Error) +{ + if (false == junctions_at(Joint).empty()) + { + Error = "the point is where a road meets a junction. Delete the junction or the road instead"; + return false; + } + auto const ends{ends_at(Joint)}; + if (ends.empty()) + { + Error = "there's no point of a road there"; + return false; + } + if (ends.size() > 2 || (ends.size() == 2 && ends[0].road == ends[1].road)) + { + Error = "more than two ends of roads meet at the point"; + return false; + } + std::string reason; + for (auto const &end : ends) + { + if (false == can_edit(*end.road, &reason)) + { + Error = "\"" + end.road->name() + "\" can't be changed: " + reason; + return false; + } + } + if (ends.size() == 1) + { + // the point a road ends at: the last piece goes + remove(std::vector{ends.front().road}); + Record.roads_removed.emplace_back(ends.front().road); + return true; + } + // the piece which leads to the point stays, and takes the place of the two + auto kept{ends[0]}; + auto dropped{ends[1]}; + if (false == kept.atend && dropped.atend) + std::swap(kept, dropped); + auto before{kept.road->definition()}; + auto after{dropped.road->definition()}; + if (false == kept.atend) + before = flipped(before); + if (dropped.atend) + after = flipped(after); + if (before.forward != after.forward || before.backward != after.backward) + { + Error = "the road has other lanes on each side of the point. Give both sides the same lanes first"; + return false; + } + auto const lengths{std::make_pair(before.length(), after.length())}; + auto const whole{lengths.first + lengths.second}; + if (lengths.first < 0.01 || lengths.second < 0.01 || before.width() <= 0.0) + { + Error = "the pieces at the point are too short"; + return false; + } + auto const start{curve_points(before.axis)}; + auto const end{curve_points(after.axis)}; + auto merged{before}; + merged.axis.points[segment_data::point::end] = end[3]; + merged.axis.rolls[1] = after.axis.rolls[1]; + // the far ends keep the way they go, with their control points moved out to make up for the ones which go with the point. + // a piece cut in two has the control points at the cut as far from it as the parts of the piece each of the two took, + // so going by these brings such a piece back to what it was + auto const reach{std::make_pair(glm::distance(start[2], start[3]), glm::distance(end[0], end[1]))}; + auto const share{std::clamp(reach.first + reach.second > 1e-6 ? reach.first / (reach.first + reach.second) : lengths.first / whole, 0.05, 0.95)}; + auto control1{(start[1] - start[0]) / share}; + auto control2{(end[2] - end[3]) / (1.0 - share)}; + auto const straight{end[3] - start[0]}; + auto const along = [&straight](glm::dvec3 const &Direction) { return glm::length(glm::cross(glm::normalize(Direction), glm::normalize(straight))) < 1e-4; }; + if (glm::length(straight) > 0.01 && along(control1) && along(-control2) && glm::dot(control1, straight) > 0.0 && glm::dot(control2, straight) < 0.0) + { + // two pieces of a straight make a straight + control1 = glm::dvec3{0.0}; + control2 = glm::dvec3{0.0}; + } + merged.axis.points[segment_data::point::control1] = control1; + 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()); + remember(Record, kept.road); + remove(std::vector{dropped.road}); + Record.roads_removed.emplace_back(dropped.road); + apply(std::vector>{{kept.road, merged}}); + return true; +} + road_node *editor_road::split(road_node &Road, double const T) { if (T <= 0.001 || T >= 0.999) @@ -764,25 +1109,7 @@ std::vector editor_road::chain(road_node &Road) double editor_road::nearest_parameter(road_node::state const &State, glm::dvec3 const &Point) { - auto const points{curve_points(State.axis)}; - auto const distance = [&](double const T) { - auto const position{curve_point(points, T)}; - return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z}); - }; - double best{0.0}; - for (int i = 1; i <= 64; ++i) - if (distance(i / 64.0) < distance(best)) - best = i / 64.0; - double step{1.0 / 64.0}; - for (int i = 0; i < 24; ++i) - { - step *= 0.5; - if (best - step >= 0.0 && distance(best - step) < distance(best)) - best -= step; - else if (best + step <= 1.0 && distance(best + step) < distance(best)) - best += step; - } - return best; + return nearest_on(curve_points(State.axis), Point); } road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin) diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp index fd41bb0f..a05a05ac 100644 --- a/editor/editorRoad.hpp +++ b/editor/editorRoad.hpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include #include "world/Road.h" +#include "world/RoadPoint.h" // editor operations on the roads. a road in the scenery is a string of pieces joined end to end, // each piece being a road node with lanes and geometry of its own; roads meet at junctions @@ -54,10 +55,14 @@ class editor_road std::vector> junctions; // junctions with what they were like before std::vector junctions_created; std::vector junctions_removed; + std::vector> points; // crossings and traffic points with what they were like before + std::vector points_created; + std::vector points_removed; bool empty() const { - return roads.empty() && roads_created.empty() && roads_removed.empty() && junctions.empty() && junctions_created.empty() && junctions_removed.empty(); + return roads.empty() && roads_created.empty() && roads_removed.empty() && junctions.empty() && junctions_created.empty() && junctions_removed.empty() && points.empty() && + points_created.empty() && points_removed.empty(); } }; // a road led out of the side of another road, or out of a junction between its arms: the junction this takes, @@ -91,6 +96,25 @@ class editor_road static std::vector create(std::vector const &States); static void remove(std::vector const &Junctions); static void revive(std::vector const &Junctions); + // level crossings, and the points the vehicles appear at or are taken away at. these go by the lanes and the rails they find + // around them, so unlike the roads they can be changed with vehicles around + static bool can_edit(roadpoint_node const &Point, std::string *Reason = nullptr); + static std::vector create(std::vector const &States); + static void apply(std::vector> const &Changes); + static void remove(std::vector const &Points); + static void revive(std::vector const &Points); + // the crossing or traffic point nearest to specified point, within specified distance from it + static roadpoint_node *nearest_point(glm::dvec3 const &Point, double const Radius); + // place on the middle of a lane nearest to specified point, within specified distance from it. + // Direction: receives the way the traffic goes there. returns: false if there's no lane that close + static bool nearest_lane(glm::dvec3 const &Point, double const Reach, glm::dvec3 &Position, glm::dvec3 &Direction); + // rails crossing specified piece within specified distance from a point. Middle: receives the point of the axis halfway between + // the outermost of them, Halfspan: how far along the road these are from it. returns: false if no rails cross the piece there + static bool rails_across(road_node const &Road, glm::dvec3 const &Near, double const Reach, glm::dvec3 &Middle, double &Halfspan); + // takes a point out of a road. the two pieces which meet at it are made one, led from the far end of one to the far end + // of the other the way these ends go; a point a road ends at takes the last piece of the road with it. + // what gets done is added to Record. returns: false if it can't be done, with the reason in Error + static bool dissolve(glm::dvec3 const &Joint, record &Record, std::string &Error); // cuts the piece in two at specified point of its axis. returns: the new piece, holding the part past the cut static road_node *split(road_node &Road, double const T); // the pieces which make a single road together with specified one, in no particular order diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 9fe5baf4..2036d063 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "world/Track.h" #include "world/Road.h" +#include "world/RoadPoint.h" #include "simulation/simulation.h" #include "model/AnimModel.h" @@ -923,6 +924,68 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[source.layer].edits.emplace_back(std::move(edit)); rewritten.emplace(junction); } + // level crossings and traffic points of the roads + std::vector savedroadpoints; + std::vector droppedroadpoints; + auto const roadpoint_text = [](roadpoint_node &Point, glm::dvec3 const &Offset) { + auto const definition{Point.definition()}; + auto local{definition}; + local.position -= Offset; + Point.define(local); + std::string text; + Point.export_as_text(text); + Point.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *point : simulation::Roadpoints.sequence()) + { + if (point == nullptr || point->from_template()) + { + continue; + } + auto const nodelayer{resolve(point->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(point)}; + if (lookup == m_sources.end()) + { + if (point->m_editorremoved || false == point->dirty()) + { + continue; + } + created[nodelayer].emplace_back(point, roadpoint_text(*point, layer(nodelayer).context_insert().offset)); + savedroadpoints.push_back(point); + continue; + } + auto const &source{lookup->second}; + if ((false == point->m_editorremoved && false == point->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (point->m_editorremoved) + { + droppedroadpoints.push_back(point); + } + else + { + edit.text = roadpoint_text(*point, source.context.offset); + edit.length = edit.text.size(); + edit.node = point; + savedroadpoints.push_back(point); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(point); + } for (auto const &erased : m_erased) { file_patch::edit edit; @@ -1382,6 +1445,14 @@ save_result node_layers::save(std::vector const &Rootstatements) { m_sources.erase(junction); } + for (auto *point : savedroadpoints) + { + point->m_dirty = false; + } + for (auto const *point : droppedroadpoints) + { + m_sources.erase(point); + } if (removedterrain) { // binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index b24ffb70..18e3c701 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "world/MemCell.h" #include "world/Track.h" #include "world/Road.h" +#include "world/RoadPoint.h" #include "world/Traction.h" #include "world/TractionPower.h" #include "audio/sound.h" @@ -39,6 +40,7 @@ memory_table Memory; path_table Paths; road_table Roads; junction_table Junctions; +roadpoint_table Roadpoints; traction_table Traction; powergridsource_table Powergrid; instance_table Instances; @@ -117,6 +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 + simulation::Roadpoints.update( totaltime ); simulation::Vehicles.update( Deltatime, Iterationcount ); } diff --git a/simulation/simulation.h b/simulation/simulation.h index 67bfae0e..b36d70a0 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -84,6 +84,7 @@ extern memory_table Memory; extern path_table Paths; extern road_table Roads; extern junction_table Junctions; +extern roadpoint_table Roadpoints; extern traction_table Traction; extern powergridsource_table Powergrid; extern instance_table Instances; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index e2598fdc..16013c67 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/lightarray.h" #include "world/TractionPower.h" #include "world/Road.h" +#include "world/RoadPoint.h" #include "application/application.h" #include "rendering/renderer.h" #include "utilities/Logs.h" @@ -182,6 +183,7 @@ state_serializer::deserialize_continue(std::shared_ptr state // so it's inserted in the region only after that file had its chance to be written simulation::Roads.create_geometry( Scratchpad ); simulation::Junctions.create_geometry( Scratchpad ); + simulation::Roadpoints.create_geometry(); return false; } @@ -402,6 +404,8 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc // the junctions tie the lanes of the roads together, what's left loose after that gets closed by the roads simulation::Junctions.InitJunctions(); simulation::Roads.InitRoads(); + // crossings and traffic points go by the lanes, which are complete at this point + simulation::Roadpoints.InitRoadpoints(); simulation::Traction.InitTraction(); simulation::Events.InitEvents(); simulation::Events.InitLaunchers(); @@ -538,6 +542,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch scene::Groups.insert( scene::Groups.handle(), junction ); scene::Layers.track( junction, { sourcebegin, Input.TokenEnd() } ); } + else if( roadpoint_node::is_keyword( nodedata.type ) ) { + // level crossing, or a point where road vehicles appear or are taken away + auto *point { new roadpoint_node( nodedata ) }; + point->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Roadpoints.insert( point ) ) { + ErrorLog( "Bad scenario: duplicate road point name \"" + point->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), point ); + scene::Layers.track( point, { sourcebegin, Input.TokenEnd() } ); + } else if( nodedata.type == "traction" ) { auto *traction { deserialize_traction( Input, Scratchpad, nodedata ) }; @@ -713,6 +731,9 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch { "track", scene::layer_item::track }, { "road", scene::layer_item::track }, { "junction", scene::layer_item::track }, + { "crossing", scene::layer_item::track }, + { "spawn", scene::layer_item::track }, + { "despawn", scene::layer_item::track }, { "traction", scene::layer_item::traction }, { "tractionpowersource", scene::layer_item::powersource }, { "model", scene::layer_item::model }, @@ -1427,6 +1448,11 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons junction->export_as_text( scmfile ); } } + for( auto const *point : Roadpoints.sequence() ) { + if( point != nullptr && false == point->m_editorremoved && point->dirty() == Dirty && point->group() == null_handle ) { + point->export_as_text( scmfile ); + } + } // traction scmfile << "// traction\n"; for( auto const *traction : Traction.sequence() ) { @@ -1482,7 +1508,8 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st std::pair state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { // statements which take more than a single token, with the tokens ending them static std::unordered_map const nodeends { - { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "junction", "endjunction" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, + { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "junction", "endjunction" }, { "crossing", "endcrossing" }, { "spawn", "endspawn" }, { "despawn", "enddespawn" }, + { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, { "triangles", "endtri" }, { "triangle_strip", "endtri" }, { "triangle_fan", "endtri" }, { "lines", "endline" }, { "line_strip", "endline" }, { "line_loop", "endline" }, { "memcell", "endmemcell" }, { "eventlauncher", "end" }, { "sound", "endsound" } }; static std::unordered_map const statementends { diff --git a/utilities/Classes.h b/utilities/Classes.h index 74aface6..ec60965c 100644 --- a/utilities/Classes.h +++ b/utilities/Classes.h @@ -47,6 +47,8 @@ class road_node; // kawałek drogi, z którego powstają pasy ruchu class road_table; class junction_node; class junction_table; +class roadpoint_node; // przejazd kolejowy albo miejsce, w którym pojazdy drogowe się pojawiają lub znikają +class roadpoint_table; struct dictionary_source; class trainset_desc; class scenery_desc; diff --git a/world/RoadPoint.cpp b/world/RoadPoint.cpp new file mode 100644 index 00000000..4946cf81 --- /dev/null +++ b/world/RoadPoint.cpp @@ -0,0 +1,1125 @@ +/* +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/. +*/ + +#include "stdafx.h" +#include "world/RoadPoint.h" + +#include "world/Track.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "vehicle/Driver.h" +#include "vehicle/Train.h" +#include "model/Model3d.h" +#include "simulation/simulation.h" +#include "rendering/particles.h" +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" + +namespace +{ + +constexpr double stopline_width{0.5}; +constexpr double stopline_inset{0.15}; // gap between the ends of a stop line and the edges of its lane +constexpr double line_lift{0.02}; // the line is raised over the surface, so the two don't fight for depth +constexpr double scan_period{0.2}; // seconds between looks at the rails of a crossing +constexpr double hold_time{4.0}; // seconds a crossing stays closed after the last train was seen +constexpr double near_zone{30.0}; // a rail vehicle this close to the stop lines keeps the crossing closed whichever way it goes +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 + +// a path as a curve: points along it, and distances along it +class path_curve +{ + + public: + explicit path_curve(segment_data const &Path) + { + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + m_points[0] = start; + m_points[3] = end; + if (control1 == glm::dvec3{0.0} && control2 == glm::dvec3{0.0}) + { + m_points[1] = start + (end - start) / 3.0; + m_points[2] = end - (end - start) / 3.0; + } + else + { + m_points[1] = start + control1; + m_points[2] = end + control2; + } + m_stations[0] = 0.0; + auto previous{m_points[0]}; + for (int idx = 1; idx <= samples; ++idx) + { + auto const current{point(static_cast(idx) / samples)}; + m_stations[idx] = m_stations[idx - 1] + glm::distance(previous, current); + previous = current; + } + } + double length() const + { + return m_stations[samples]; + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * m_points[0] + 3.0 * u * u * T * m_points[1] + 3.0 * u * T * T * m_points[2] + T * T * T * m_points[3]; + } + glm::dvec3 direction(double const T) const + { + auto const u{1.0 - T}; + auto const result{3.0 * u * u * (m_points[1] - m_points[0]) + 6.0 * u * T * (m_points[2] - m_points[1]) + 3.0 * T * T * (m_points[3] - m_points[2])}; + return glm::length2(result) > 1e-12 ? glm::normalize(result) : glm::dvec3{0.0, 0.0, 1.0}; + } + // value of the curve parameter for specified distance from the start, and the other way around + double parameter(double const Station) const + { + if (Station <= 0.0) + { + return 0.0; + } + if (Station >= length()) + { + return 1.0; + } + auto const idx{std::distance(m_stations.begin(), std::upper_bound(m_stations.begin(), m_stations.end(), Station))}; + auto const span{m_stations[idx] - m_stations[idx - 1]}; + return (static_cast(idx - 1) + (span > 0.0 ? (Station - m_stations[idx - 1]) / span : 0.0)) / samples; + } + double station(double const T) const + { + auto const position{std::clamp(T, 0.0, 1.0) * samples}; + auto const idx{std::min(samples - 1, static_cast(position))}; + return glm::mix(m_stations[idx], m_stations[idx + 1], position - idx); + } + // value of the curve parameter for the point nearest to specified one, seen from above; Distance receives how far it is + double nearest(glm::dvec3 const &Point, double &Distance) const + { + auto const distance = [&](double const T) { + auto const position{point(T)}; + return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z}); + }; + double best{0.0}; + for (int idx = 1; idx <= samples; ++idx) + { + if (distance(static_cast(idx) / samples) < distance(best)) + { + best = static_cast(idx) / samples; + } + } + auto step{1.0 / samples}; + for (int pass = 0; pass < 16; ++pass) + { + step *= 0.5; + if (best - step >= 0.0 && distance(best - step) < distance(best)) + { + best -= step; + } + else if (best + step <= 1.0 && distance(best + step) < distance(best)) + { + best += step; + } + } + Distance = distance(best); + return best; + } + + private: + static constexpr int samples{32}; + std::array m_points; + std::array m_stations{}; +}; + +// true if the path can't have a point within specified distance from specified one; a cheap way to tell most of the scenery apart +bool out_of_reach(segment_data const &Path, glm::dvec3 const &Point, double const Reach) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + // the curve lies within its control points, which are no further from the start than this + auto const span{glm::length(Path.points[segment_data::point::control1]) + glm::distance(start, end) + glm::length(Path.points[segment_data::point::control2])}; + 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 +{ + + public: + stop_event() + { + 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 "roadstop"; + } + void deserialize_(cParser &Input, scene::scratch_data &Scratchpad) override {} + void run_() override {} + void export_as_text_(std::ostream &Output) const override {} +}; + +// 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) +{ + TTrack *result{nullptr}; + auto best{Reach}; + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + { + continue; + } + for (auto *lane : road->tracks()) + { + if (lane == nullptr || lane->m_paths.empty() || out_of_reach(lane->m_paths.front(), Point, Reach)) + { + continue; + } + path_curve const curve{lane->m_paths.front()}; + double distance{0.0}; + auto const t{curve.nearest(Point, distance)}; + if (distance >= best || std::abs(curve.point(t).y - Point.y) > 3.0) + { + continue; + } + best = distance; + result = lane; + Station = curve.station(t); + } + } + return result; +} + +double planar_distance(glm::dvec3 const &Left, glm::dvec3 const &Right) +{ + return glm::distance(glm::dvec2{Left.x, Left.z}, glm::dvec2{Right.x, Right.z}); +} + +} // namespace + +// brings the content to a usable form +void roadpoint_node::state::normalize() +{ + clearance = std::clamp(clearance, 1.f, 100.f); + warning = std::clamp(warning, 50.f, 5000.f); + 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); + radius = std::clamp(radius, 0.5f, 50.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); +} + +// 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"; +} + +bool roadpoint_node::is_keyword(std::string const &Type) +{ + return Type == "crossing" || Type == "spawn" || Type == "despawn"; +} + +// reads vehicles out of a text +std::vector roadpoint_node::parse_vehicles(std::string const &Text) +{ + std::vector result; + auto const add = [&result](std::vector const &Tokens, std::size_t const First, std::string const &Load, std::string const &Loadtype) { + vehicle_data vehicle; + vehicle.folder = Tokens[First]; + vehicle.skin = Tokens[First + 1]; + vehicle.type = Tokens[First + 2]; + replace_slashes(vehicle.folder); + replace_slashes(vehicle.skin); + replace_slashes(vehicle.type); + vehicle.load = std::max(0, std::atoi(Load.c_str())); + if (vehicle.load > 0) + { + vehicle.loadtype = Loadtype; + } + result.emplace_back(vehicle); + }; + std::vector tokens; + { + std::istringstream stream{Text}; + std::string token; + while (stream >> token) + { + tokens.emplace_back(token); + } + } + if (std::find(tokens.begin(), tokens.end(), "dynamic") != tokens.end()) + { + // node dynamic [] enddynamic + for (std::size_t idx = 0; idx < tokens.size(); ++idx) + { + if (tokens[idx] != "dynamic" || idx + 3 >= tokens.size()) + { + continue; + } + auto last{idx + 4}; + while (last < tokens.size() && tokens[last] != "enddynamic") + { + ++last; + } + // past the vehicle itself come the path, the offset, the driver and the velocity, then the load + auto const load{idx + 8}; + add(tokens, idx + 1, load < last ? tokens[load] : std::string{}, load + 1 < last ? tokens[load + 1] : std::string{}); + idx = last; + } + return result; + } + std::istringstream lines{Text}; + std::string line; + while (std::getline(lines, line)) + { + std::istringstream stream{line}; + std::vector parts; + std::string part; + while (stream >> part) + { + parts.emplace_back(part); + } + if (parts.size() >= 3) + { + add(parts, 0, parts.size() > 3 ? parts[3] : std::string{}, parts.size() > 4 ? parts[4] : std::string{}); + } + } + return result; +} + +// restores content of the node from provided input stream; reads up to and including the closing statement +void roadpoint_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + auto const type{keyword(m_state.kind)}; + auto const label{type + " \"" + m_name + "\""}; + auto const kind{m_state.kind}; + m_state = state{}; + m_state.kind = kind; + Input.getTokens(3); + Input >> m_state.position.x >> m_state.position.y >> m_state.position.z; + m_state.position += Offset; + + auto token{Input.getToken()}; + while (false == token.empty() && token != "end" + type) + { + if (token == "clearance") + { + Input.getTokens(); + Input >> m_state.clearance; + } + else if (token == "warning") + { + Input.getTokens(); + Input >> m_state.warning; + } + else if (token == "stoplines") + { + m_state.stoplines = (Input.getToken() != "no"); + } + else if (token == "interval") + { + Input.getTokens(); + Input >> m_state.interval; + } + else if (token == "variation") + { + Input.getTokens(); + Input >> m_state.variation; + } + else if (token == "velocity") + { + Input.getTokens(); + Input >> m_state.velocity; + } + else if (token == "copies") + { + Input.getTokens(); + Input >> m_state.copies; + } + else if (token == "radius") + { + Input.getTokens(); + Input >> m_state.radius; + } + else if (token == "node") + { + // a vehicle, given the way vehicles are put in a scenery: + // node dynamic [] enddynamic + std::string discard; + std::string nodetype; + Input.getTokens(4); + Input >> discard >> discard >> discard >> nodetype; + vehicle_data vehicle; + if (nodetype == "dynamic") + { + vehicle.folder = Input.getToken(); + vehicle.skin = Input.getToken(); + vehicle.type = Input.getToken(); + replace_slashes(vehicle.folder); + replace_slashes(vehicle.skin); + replace_slashes(vehicle.type); + } + else + { + ErrorLog("Bad road point: " + label + " lists a \"" + nodetype + "\" where it takes vehicles only"); + } + // where the vehicle is put and how it's driven is up to the point, what's left to take is its load + std::vector rest; + auto part{Input.getToken()}; + while (false == part.empty() && part != "enddynamic" && part != "end" + type) + { + rest.emplace_back(part); + part = Input.getToken(); + } + if (rest.size() > 4) + { + vehicle.load = std::atoi(rest[4].c_str()); + } + if (rest.size() > 5 && vehicle.load > 0) + { + vehicle.loadtype = rest[5]; + } + if (false == vehicle.type.empty()) + { + m_state.vehicles.emplace_back(vehicle); + } + if (part != "enddynamic") + { + ErrorLog("Bad road point: a vehicle of " + label + " isn't closed with \"enddynamic\""); + if (part == "end" + type) + { + break; + } + } + } + else + { + ErrorLog("Bad road point: unknown property: \"" + token + "\" defined for " + label); + } + token = Input.getToken(); + } + m_state.normalize(); + location(m_state.position); +} + +// replaces what the node knows about itself +void roadpoint_node::define(state const &State) +{ + auto const vehicles{m_state.vehicles}; + auto const copies{m_state.copies}; + m_state = State; + m_state.normalize(); + if (m_prepared && (m_state.vehicles != vehicles || m_state.copies != copies)) + { + // 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 + m_pool.clear(); + m_prepared = false; + } + location(m_state.position); + m_area.radius = -1.f; +} + +// ties the point to the lanes and the rails as they are at the moment +void roadpoint_node::bind() +{ + unbind(); + m_bound = true; + if (m_editorremoved) + { + return; + } + switch (m_state.kind) + { + case kind_type::crossing: + { + bind_crossing(); + break; + } + case kind_type::spawn: + { + // the vehicles are put on the lane the point is on + double station{0.0}; + auto *lane{nearest_lane(m_state.position, 3.0, station)}; + if (lane != nullptr) + { + m_lanes.emplace_back(lane); + // at the very start of a lane which leads from nowhere the vehicle would hang over its end + m_offset = (lane->trPrev == nullptr ? std::max(station, 2.0 * front_room) : station); + } + break; + } + case kind_type::despawn: + { + // the vehicles are taken from every lane which passes close enough + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + { + continue; + } + for (auto *lane : road->tracks()) + { + if (lane == nullptr || lane->m_paths.empty() || out_of_reach(lane->m_paths.front(), m_state.position, m_state.radius)) + { + continue; + } + path_curve const curve{lane->m_paths.front()}; + double distance{0.0}; + auto const t{curve.nearest(m_state.position, distance)}; + if (distance <= m_state.radius && std::abs(curve.point(t).y - m_state.position.y) <= 3.0) + { + m_lanes.emplace_back(lane); + } + } + } + break; + } + } +} + +// finds the rails of a crossing, and the places the vehicles of each lane are to stop at +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; + } + if (crossed != nullptr) + { + auto const &lanes{crossed->tracks()}; + for (std::size_t idx = 0; idx < lanes.size(); ++idx) + { + auto *lane{lanes[idx]}; + if (lane == nullptr || lane->m_paths.empty()) + { + continue; + } + // the lanes are laid out the way the traffic goes, so the place to stop at is back along the lane from the crossing + double distance{0.0}; + path_curve curve{lane->m_paths.front()}; + auto station{curve.station(curve.nearest(m_state.position, distance))}; + double remaining{m_state.clearance}; + for (int guard = 0; guard < 16; ++guard) + { + if (station >= remaining) + { + station -= remaining; + break; + } + // it's on the piece before this one, if there's any: the same lane of the road, not a way through a junction + auto *previous{lane->trPrev}; + auto const *owner{previous != nullptr ? dynamic_cast(previous->m_road) : nullptr}; + if (owner == nullptr || previous->m_paths.empty() || previous->trNext != lane || std::find(owner->tracks().begin(), owner->tracks().end(), previous) == owner->tracks().end()) + { + station = 0.0; + break; + } + remaining -= station; + lane = previous; + curve = path_curve{lane->m_paths.front()}; + station = curve.length(); + } + auto const t{curve.parameter(station)}; + stop_data stop; + stop.track = lane; + stop.position = curve.point(t); + stop.direction = curve.direction(t); + stop.width = (idx < crossed->definition().lanes.size() ? crossed->definition().lanes[idx].width : crossed->definition().lanewidth); + m_stops.emplace_back(stop); + } + } + // orders for the drivers. the ones made before are used again, as a driver may still hold on to one + while (m_events.size() < m_stops.size()) + { + auto *event{new stop_event()}; + event->m_name = m_name + ":stop" + std::to_string(m_events.size() + 1); + m_events.emplace_back(event); + } + for (std::size_t idx = 0; idx < m_stops.size(); ++idx) + { + auto *event{static_cast(m_events[idx])}; + event->m_location = m_stops[idx].position; + // lanes are driven from their first point to the second, which is the way events of the second kind are for + m_stops[idx].track->m_events2.emplace_back(event->m_name, event); + } + + // rails: the tracks passing between the stop lines, and what leads to them within the warning distance + std::vector> reached; + auto const known = [&reached](TTrack const *Track) { return std::any_of(reached.begin(), reached.end(), [Track](std::pair const &Entry) { return Entry.first == Track; }); }; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0) + { + continue; + } + for (auto const &path : track->m_paths) + { + if (out_of_reach(path, m_state.position, m_state.clearance)) + { + continue; + } + path_curve const curve{path}; + double distance{0.0}; + auto const t{curve.nearest(m_state.position, distance)}; + if (distance <= m_state.clearance && std::abs(curve.point(t).y - m_state.position.y) <= 3.0) + { + reached.emplace_back(track, 0.0); + break; + } + } + } + for (std::size_t idx = 0; idx < reached.size(); ++idx) + { + auto *track{reached[idx].first}; + double length{0.0}; + for (auto const &path : track->m_paths) + { + length = std::max(length, path_curve{path}.length()); + } + auto const travelled{reached[idx].second + length}; + if (travelled > m_state.warning) + { + continue; + } + std::vector neighbours{track->trPrev, track->trNext}; + if (track->SwitchExtension != nullptr) + { + // both ways of a switch count, as the train can come by either + neighbours.insert(neighbours.end(), {track->SwitchExtension->pPrevs[0], track->SwitchExtension->pPrevs[1], track->SwitchExtension->pNexts[0], track->SwitchExtension->pNexts[1]}); + } + for (auto *neighbour : neighbours) + { + if (neighbour != nullptr && (neighbour->iCategoryFlag & 1) != 0 && false == known(neighbour)) + { + reached.emplace_back(neighbour, travelled); + } + } + } + for (auto const &entry : reached) + { + m_rails.emplace_back(entry.first); + } + close(m_closed); +} + +// lets go of the lanes and the rails +void roadpoint_node::unbind() +{ + if (false == m_bound) + { + return; + } + m_bound = false; + for (std::size_t idx = 0; idx < m_stops.size() && idx < m_events.size(); ++idx) + { + auto &events{m_stops[idx].track->m_events2}; + auto const *event{m_events[idx]}; + events.erase(std::remove_if(events.begin(), events.end(), [event](std::pair const &Entry) { return Entry.second == event; }), events.end()); + } + // a driver who has one of the orders in sight keeps it; it's not to hold anyone anymore + for (auto *event : m_events) + { + static_cast(event)->m_velocity = -1.0; + } + m_stops.clear(); + m_rails.clear(); + m_lanes.clear(); + m_seen.clear(); +} + +// makes the vehicles of a spawn point +void roadpoint_node::prepare() +{ + if (m_prepared || m_state.kind != kind_type::spawn || m_editorremoved) + { + return; + } + if (false == m_bound) + { + bind(); + } + if (m_lanes.empty()) + { + ErrorLog("Bad road point: spawn \"" + m_name + "\" isn't on a lane of a road, there's nowhere to put its vehicles"); + return; + } + m_prepared = true; + // the first vehicle shows up after a part of the interval, so points with the same interval don't all start at once + m_timer = LocalRandom(0.0, m_state.interval); + // 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) + { + for (int copy = 0; copy < m_state.copies; ++copy) + { + // 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 + if (vehicle->MyTrack != nullptr) + { + vehicle->MyTrack->RemoveDynamicObject(vehicle); + vehicle->MyTrack = nullptr; + } + vehicle->bEnabled = false; + } + } + if (m_pool.empty()) + { + ErrorLog("Bad road point: spawn \"" + m_name + "\" has no vehicles to put on the road"); + } +} + +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; })); +} + +// to be called with each step of the simulation +void roadpoint_node::update(double const Deltatime) +{ + if (m_editorremoved) + { + return; + } + if (false == m_bound) + { + bind(); + } + switch (m_state.kind) + { + case kind_type::crossing: + { + update_crossing(Deltatime); + break; + } + case kind_type::spawn: + { + update_spawn(Deltatime); + break; + } + case kind_type::despawn: + { + update_despawn(); + break; + } + } +} + +// closes the crossing while there's a rail vehicle coming, or standing right at it +void roadpoint_node::update_crossing(double const Deltatime) +{ + m_scan -= Deltatime; + m_hold -= Deltatime; + if (m_scan <= 0.0) + { + m_scan = scan_period; + std::map seen; + for (auto const *track : m_rails) + { + for (auto const *vehicle : track->Dynamics) + { + if (vehicle == nullptr) + { + continue; + } + auto const distance{planar_distance(vehicle->GetPosition(), m_state.position)}; + seen.emplace(vehicle, distance); + auto const before{m_seen.find(vehicle)}; + // a vehicle getting closer is on its way here; one which stands still or goes away isn't, unless it's at the crossing itself + if (distance < m_state.clearance + near_zone || (before != m_seen.end() && distance < before->second - 0.01)) + { + m_hold = hold_time; + } + } + } + m_seen.swap(seen); + } + if ((m_hold > 0.0) != m_closed) + { + close(m_hold > 0.0); + } +} + +// tells the drivers whether they can go +void roadpoint_node::close(bool const Closed) +{ + m_closed = Closed; + for (std::size_t idx = 0; idx < m_stops.size() && idx < m_events.size(); ++idx) + { + static_cast(m_events[idx])->m_velocity = (Closed ? 0.0 : -1.0); + } +} + +// puts a waiting vehicle on the road when its time comes +void roadpoint_node::update_spawn(double const Deltatime) +{ + if (false == m_prepared) + { + // the point was made in the editor, or the scenery was loaded for editing + prepare(); + } + if (m_pool.empty() || m_lanes.empty()) + { + return; + } + m_timer -= Deltatime; + if (m_timer > 0.0) + { + return; + } + // not with something in the way + auto const *lane{m_lanes.front()}; + for (auto const *track : {lane, static_cast(lane->trPrev), static_cast(lane->trNext)}) + { + if (track == nullptr) + { + continue; + } + for (auto const *other : track->Dynamics) + { + if (other != nullptr && planar_distance(other->GetPosition(), m_state.position) < clear_zone) + { + m_timer = retry_period; + return; + } + } + } + // one of the vehicles which wait, picked at random + std::vector ready; + for (auto const &entry : m_pool) + { + if (false == entry.vehicle->bEnabled && entry.vehicle->MyTrack == nullptr) + { + ready.emplace_back(&entry); + } + } + if (ready.empty()) + { + m_timer = retry_period; + return; + } + auto const &entry{*ready[std::min(ready.size() - 1, static_cast(LocalRandom(0.0, static_cast(ready.size()))))]}; + auto *vehicle{entry.vehicle}; + // it comes back the way it was made: whole, at speed, with the driver told to drive on + vehicle->bEnabled = true; + vehicle->MoverParameters->DamageFlag = 0; + vehicle->MoverParameters->EngDmgFlag = 0; + vehicle->MoverParameters->V = entry.velocity; + vehicle->place_on_track(m_lanes.front(), m_offset + 0.5 * vehicle->MoverParameters->Dim.L, false); + if (vehicle->Mechanik != nullptr) + { + // the same order a vehicle put in the scenery gets; it has the driver forget the road they knew + vehicle->Mechanik->PutCommand("Timetable:", m_state.velocity, 0, nullptr); + } + m_timer = m_state.interval * (1.0 + m_state.variation * LocalRandom(-1.0, 1.0)); +} + +// takes away the road vehicles which got to the point +void roadpoint_node::update_despawn() +{ + for (auto const *lane : m_lanes) + { + for (auto *vehicle : lane->Dynamics) + { + if (vehicle == nullptr || false == vehicle->bEnabled || vehicle->MoverParameters->CategoryFlag != 2) + { + continue; + } + if (simulation::Train != nullptr && simulation::Train->Dynamic() == vehicle) + { + // not the one the user sits in + continue; + } + if (planar_distance(vehicle->GetPosition(), m_state.position) > m_state.radius + 0.5 * vehicle->MoverParameters->Dim.L) + { + continue; + } + // the way vehicles reaching the end of the scenery go: marked here, taken off the road once all vehicles made their move + vehicle->bEnabled = false; + TDynamicObject::bDynamicRemove = true; + } + } +} + +// generates geometry of the stop lines +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}; + for (auto const &stop : m_stops) + { + // 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) + { + continue; + } + across = glm::normalize(across); + glm::dvec3 back{-stop.direction.x, 0.0, -stop.direction.z}; + back = glm::normalize(back) * stopline_width; + auto const reach{std::max(0.25, 0.5 * stop.width - stopline_inset)}; + auto const right{stop.position - across * reach + lift}; + auto const left{stop.position + across * reach + lift}; + 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}}); + } + if (false == vertices.empty()) + { + lighting_data paint; + paint.diffuse = glm::vec4{0.92f, 0.92f, 0.92f, 1.f}; + paint.ambient = paint.diffuse; + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material("colored"), std::move(vertices), glm::dvec3{0.0}); + shape.lighting(paint); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + +// puts geometry of the point in the scene as shapes of its own, which can be taken back +void roadpoint_node::show() +{ + if (m_shapes.shown() || m_editorremoved || simulation::Region == nullptr || Global.NvRenderer) + { + // NOTE: the experimental renderer draws only what the scenery had when it was loaded + return; + } + m_shapes.show(create_shapes(), m_state.position); +} + +void roadpoint_node::hide() +{ + m_shapes.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; +} + +// serialize() subclass details, sends content of the subclass to provided stream +void roadpoint_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +// deserialize() subclass details, restores content of the subclass from provided stream +void roadpoint_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +// export() subclass details, sends basic content of the class in legacy (text) format to provided stream +void roadpoint_node::export_as_text_(std::ostream &Output) const +{ + auto const type{keyword(m_state.kind)}; + Output << type << ' '; + auto const precision{Output.precision(std::numeric_limits::digits10)}; + Output << m_state.position.x << ' ' << m_state.position.y << ' ' << m_state.position.z << ' '; + Output.precision(precision); + switch (m_state.kind) + { + case kind_type::crossing: + { + Output << "clearance " << m_state.clearance << ' ' << "warning " << m_state.warning << ' ' << "stoplines " << (m_state.stoplines ? "yes" : "no") << ' '; + break; + } + case kind_type::spawn: + { + Output << "interval " << m_state.interval << ' ' << "variation " << m_state.variation << ' ' << "velocity " << m_state.velocity << ' ' << "copies " << m_state.copies << ' '; + 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 + Output << "node -1 0 none dynamic " << vehicle.folder << ' ' << vehicle.skin << ' ' << vehicle.type << " none 0 headdriver " << m_state.velocity << ' ' << vehicle.load << ' '; + if (vehicle.load > 0 && false == vehicle.loadtype.empty()) + { + Output << vehicle.loadtype << ' '; + } + Output << "enddynamic "; + } + break; + } + case kind_type::despawn: + { + Output << "radius " << m_state.radius << ' '; + break; + } + } + Output << "end" << type << "\n"; +} + +// legacy style initialization, to be performed when the lanes of the roads are tied together +void roadpoint_table::InitRoadpoints() +{ + for (auto *point : m_items) + { + if (point == nullptr) + { + continue; + } + point->bind(); + if (false == Global.editor_session) + { + // the vehicles are made now, so they don't hold the simulation up later. a scenery opened for editing + // does without them until the simulation is run + point->prepare(); + } + } +} + +// lets go of the lanes and the rails, ahead of a change of the roads +void roadpoint_table::unbind() +{ + for (auto *point : m_items) + { + if (point != nullptr) + { + point->unbind(); + } + } +} + +// ties all points anew, after the roads were changed +void roadpoint_table::bind() +{ + for (auto *point : m_items) + { + if (point == nullptr) + { + continue; + } + // the stop lines go where the lanes are now + point->hide(); + point->bind(); + if (m_geometry) + { + point->show(); + } + } +} + +// puts geometry of the points in the scene +void roadpoint_table::create_geometry() +{ + m_geometry = true; + for (auto *point : m_items) + { + if (point != nullptr) + { + point->show(); + } + } +} + +// to be called with each step of the simulation +void roadpoint_table::update(double const Deltatime) +{ + for (auto *point : m_items) + { + if (point != nullptr) + { + point->update(Deltatime); + } + } +} + +//--------------------------------------------------------------------------- diff --git a/world/RoadPoint.h b/world/RoadPoint.h new file mode 100644 index 00000000..e4a91d46 --- /dev/null +++ b/world/RoadPoint.h @@ -0,0 +1,207 @@ +/* +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 +#include + +#include + +#include "utilities/Classes.h" +#include "utilities/Names.h" +#include "scene/scenenode.h" +#include "world/Road.h" + +class TDynamicObject; +class basic_event; + +// a place on a road where something happens to the traffic. three kinds of 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. +// scenery entries: +// node crossing [clearance ] [warning ] [stoplines ] endcrossing +// node spawn [interval ] [variation <0..1>] [velocity ] [copies ] +// [node dynamic [] enddynamic]... endspawn +// node despawn [radius ] enddespawn +// 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 +class roadpoint_node : public scene::basic_node +{ + + public: + // types + enum class kind_type + { + crossing, + spawn, + despawn + }; + struct vehicle_data + { + std::string folder; // where the vehicle is defined, in the folder of the vehicles + std::string skin; + std::string type; // name of its definition files + int load{0}; + std::string loadtype; + + bool operator==(vehicle_data const &Other) const + { + return folder == Other.folder && skin == Other.skin && type == Other.type && load == Other.load && loadtype == Other.loadtype; + } + }; + struct state + { + kind_type kind{kind_type::crossing}; + glm::dvec3 position{0.0}; + // level crossing + float clearance{6.f}; // distance along the road from the point to the places the vehicles stop at + float warning{800.f}; // how far along the rails a train coming closes the crossing + bool stoplines{true}; // lines are painted where the vehicles stop + // point where vehicles appear + 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 + std::vector vehicles; + // point where vehicles are taken away + float radius{2.f}; // vehicles on lanes this close to the point are taken + + // brings the content to a usable form + void normalize(); + }; + // place the vehicles of a lane stop at while a crossing is closed + struct stop_data + { + TTrack *track{nullptr}; // the lane it's on + glm::dvec3 position{0.0}; + glm::dvec3 direction{0.0, 0.0, 1.0}; // the way the lane goes there + float width{3.5f}; // of the lane + }; + // constructors + explicit roadpoint_node(scene::node_data const &Nodedata); + // methods + // restores content of the node from provided input stream; reads up to and including the closing statement + void import(cParser &Input, glm::dvec3 const &Offset); + state const &definition() const + { + return m_state; + } + // replaces what the node knows about itself. what it's tied to stays, it's up to the caller to bind it again. + // a spawn point given another set of vehicles makes them anew; the ones it had stay on the road until they're taken away + void define(state const &State); + // scenery keyword for specified kind of point, and the statement closing its entry + static std::string keyword(kind_type const Kind); + static bool is_keyword(std::string const &Type); + // reads vehicles out of a text. each is given the way vehicles are put in a scenery, or, if there's no such entry + // in the text, as " [ ]" on a line of its own + static std::vector parse_vehicles(std::string const &Text); + // ties the point to the lanes and the rails as they are at the moment, or lets go of them + void bind(); + void unbind(); + // makes the vehicles of a spawn point, once. it loads their models, which takes a while + void prepare(); + // to be called with each step of the simulation + void update(double const Deltatime); + // state for display + bool closed() const + { + return m_closed; + } + std::vector const &stops() const + { + return m_stops; + } + std::vector const &lanes() const + { + return m_lanes; + } + std::size_t rails() const + { + return m_rails.size(); + } + // vehicles made for a spawn point, and how many of them wait for their turn + std::size_t vehicles() const + { + return m_pool.size(); + } + std::size_t waiting() const; + // generates geometry of the stop lines + std::vector create_shapes() const; + // puts geometry of the point in the scene, or takes it back + void show(); + void hide(); + bool shown() const + { + return m_shapes.shown(); + } + // members + bool m_editorremoved{false}; // removed in the editor; kept around so the removal can be undone + + private: + // types + struct pooled_vehicle + { + 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 + }; + // methods + float radius_() override; + void serialize_(std::ostream &Output) const override; + void deserialize_(std::istream &Input) override; + void export_as_text_(std::ostream &Output) const override; + void bind_crossing(); + void update_crossing(double const Deltatime); + void update_spawn(double const Deltatime); + void update_despawn(); + void close(bool const Closed); + // members + state m_state; + bool m_bound{false}; + // level crossing + 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::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 + double m_scan{0.0}; // time left to the next look at the rails + // spawn and despawn + std::vector m_lanes; // spawn: the lane the vehicles are put on. despawn: lanes the vehicles are taken from + double m_offset{0.0}; // spawn: distance of the point from the start of that lane + std::vector m_pool; + 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 + owned_shapes m_shapes; +}; + +// collection of the road points present in the scene +class roadpoint_table : public basic_table +{ + + public: + // legacy style initialization, to be performed when the lanes of the roads are tied together + void InitRoadpoints(); + // lets go of the lanes and the rails, ahead of a change of the roads, and ties all points anew when it's done + void unbind(); + void bind(); + // puts geometry of the points in the scene + void create_geometry(); + // to be called with each step of the simulation + void update(double const Deltatime); + + private: + bool m_geometry{false}; // the scene is ready to take geometry of the points +}; + +//---------------------------------------------------------------------------