diff --git a/application/editormode.h b/application/editormode.h index 50cd8111..51ff130e 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -512,6 +512,7 @@ class editor_mode : public application_mode 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 + bool varied{false}; // the selected piece is shown with lanes of its own for its end bool chain{false}; // the point to build from is set glm::dvec3 point{0.0}; // where the next piece starts glm::dvec2 direction{0.0, 1.0}; // the way it has to leave that point diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index d453b47e..83429178 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -313,6 +313,12 @@ void editor_mode::road_select(road_node *Road) if (false == editor_road::can_edit(*Road, &reason)) tool.status += ". It can't be changed: " + reason; } + else + { + // the layout stays for the build tool, which makes pieces with the same lanes all the way + tool.settings.missing = {}; + } + tool.varied = tool.settings.changing(); copy_text(tool.surface, sizeof(tool.surface), tool.settings.surface); for (int side = 0; side < 2; ++side) copy_text(tool.sides[side], sizeof(tool.sides[side]), tool.settings.sides[side].material); @@ -392,6 +398,11 @@ void editor_mode::roadpoint_aim(bool const Fresh) tool.targetnote = "Point at the place a road crosses the rails"; return; } + if (road->definition().changing()) + { + tool.targetnote = "The road gains or loses lanes on this piece; a level crossing goes on a piece which keeps its lanes"; + return; + } glm::dvec3 middle{0.0}; double halfspan{0.0}; if (editor_road::rails_across(*road, tool.mouse, kRailReach, middle, halfspan)) @@ -1197,7 +1208,8 @@ void editor_mode::road_click() { // carried on from its start the road keeps the direction of its axis, so the lanes stay what they were tool.reversed = (false == end.atend); - tool.settings = end.road->definition(); + // with the lanes it has at that end + tool.settings = end.road->definition().section(end.atend ? 1 : 0); // and it stays as wide as it is at that end, with its directions as far apart auto const scale{tool.settings.taper[end.atend ? 1 : 0]}; tool.settings.taper = {scale, scale}; @@ -1216,6 +1228,7 @@ void editor_mode::road_click() tool.settings = end.road->definition(); tool.settings.sides[end.half > 0 ? 0 : 1] = road_node::side_data{}; tool.settings.banks = {}; + tool.settings.missing = {}; } tool.settings.forward = end.forward; tool.settings.backward = end.backward; @@ -1320,6 +1333,7 @@ void editor_mode::road_click() { auto state{tool.settings}; state.axis = pieces[index]; + state.missing = {}; auto const from{along(index)}; auto const to{along(index + 1)}; auto const &median{tool.settings.median}; @@ -1373,7 +1387,8 @@ void editor_mode::road_apply() if (tool.selected == nullptr) return; tool.settings.normalize(); - auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; + // other lanes at the end are something the selected piece has for itself; the rest of the road keeps the lanes it has + auto const pieces{tool.whole && false == tool.settings.changing() ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; editor_road::record record; std::vector> changes; // the banks are kept for each piece as they are, unless it's the banks which were just changed @@ -1674,6 +1689,11 @@ bool editor_mode::road_split() tool.status = "Can't split: " + reason; return true; } + if (road->definition().changing()) + { + tool.status = "Can't split a piece which gains or loses lanes; give it the same lanes at both ends first"; + return true; + } editor_road::record record; record.roads.emplace_back(road, road->definition()); auto *created{editor_road::split(*road, editor_road::nearest_parameter(road->definition(), tool.mouse))}; @@ -1735,7 +1755,7 @@ void editor_mode::road_point_width(float const Width) { // the pieces on both sides of a point get to that width there, and from there go back to their own along their length auto const changes{point_changes(m_roadtool.points, [Width](road_node::state &State, bool const Atend, std::size_t const) { - auto const width{State.width()}; + auto const width{State.width(Atend ? 1 : 0)}; if (width > 0.0) State.taper[Atend ? 1 : 0] = std::clamp(static_cast(Width / width), 0.25f, 4.f); })}; @@ -1931,19 +1951,52 @@ bool editor_mode::render_road_layout(road_node::state &State) }; auto const material = [this](char const *Label, char *Buffer, std::size_t const Size, std::string &Value) { return render_road_material(Label, Buffer, Size, Value); }; - // lanes - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt("Lanes along the axis", &State.forward)) - changed = true; - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt("Lanes against the axis", &State.backward)) - changed = true; - if (changed) + // lanes. a piece of a road which is there already can end with other lanes than it starts with { - // a different set of lanes starts with the common width and speed limit, and the default rules for changing lanes - State.lanes.clear(); - State.changes.clear(); - State.normalize(); + auto const selecting{tool.tool == 0 && tool.selected != nullptr}; + int counts[2][2]{{State.lanes_at(0, true), State.lanes_at(0, false)}, {State.lanes_at(1, true), State.lanes_at(1, false)}}; + bool relaned{false}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt(selecting && tool.varied ? "Lanes along the axis, at the start" : "Lanes along the axis", &counts[0][0])) + relaned = true; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt(selecting && tool.varied ? "Lanes against the axis, at the start" : "Lanes against the axis", &counts[0][1])) + relaned = true; + if (selecting) + { + // ticked it only shows the lanes of the end to be set; unticked it gives the end the lanes of the start + if (ImGui::Checkbox("Other lanes at the end of the piece", &tool.varied) && false == tool.varied && State.changing()) + relaned = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The road gains or loses lanes along this piece: it gets wider or narrower, a lane it gains branches off the outermost lane\n" + "of its direction (two at most), a lane it loses joins the outermost one left.\n" + "What's next to the piece is fitted to the lanes the piece has at that end.\n" + "The axis goes the way the lanes drawn in green do: the start of the piece is where these come from.\n" + "Split the piece (K) first to have the lanes change over a part of it."); + if (tool.varied) + { + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt("Lanes along the axis, at the end", &counts[1][0])) + relaned = true; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt("Lanes against the axis, at the end", &counts[1][1])) + relaned = true; + } + } + // the traffic going along the axis gets on the piece at its start, the one going against it at its end + if (State.lanes_at(1, true) - State.lanes_at(0, true) > 2 || State.lanes_at(0, false) - State.lanes_at(1, false) > 2) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "A lane forks into three at most: lanes past the second one gained can't be got on"); + if (relaned) + { + if (false == selecting || false == tool.varied) + { + counts[1][0] = counts[0][0]; + counts[1][1] = counts[0][1]; + } + // a different set of lanes starts with the common width and speed limit, and the default rules for changing lanes + State.lanes_between(counts[0][0], counts[0][1], counts[1][0], counts[1][1]); + changed = true; + } } if (number("Lane width [m]", State.lanewidth, 0.25f, "%.2f")) { @@ -2342,7 +2395,7 @@ void editor_mode::render_road_window() if (false == ends.empty()) { auto const &layout{ends.front().road->definition()}; - auto width{static_cast(layout.width() * layout.taper[ends.front().atend ? 1 : 0])}; + auto width{static_cast(layout.width(ends.front().atend ? 1 : 0) * layout.taper[ends.front().atend ? 1 : 0])}; if (number("Road width here [m]", width, 0.25f, "%.2f")) road_point_width(std::clamp(width, 1.0f, 100.0f)); if (ImGui::IsItemHovered()) @@ -2410,12 +2463,20 @@ void editor_mode::render_road_window() else if (tool.selected != nullptr) { auto const &road{*tool.selected}; - ImGui::Text("%s, %.1f m, %d + %d lanes", road.name().c_str(), road.definition().length(), road.definition().forward, road.definition().backward); + auto const &layout{road.definition()}; + if (layout.changing()) + ImGui::Text("%s, %.1f m, %d + %d to %d + %d lanes", road.name().c_str(), layout.length(), layout.lanes_at(0, true), layout.lanes_at(0, false), layout.lanes_at(1, true), + layout.lanes_at(1, false)); + else + ImGui::Text("%s, %.1f m, %d + %d lanes", road.name().c_str(), layout.length(), layout.forward, layout.backward); if (ImGui::Button("Delete (Del)")) road_delete(); ImGui::SameLine(); ImGui::TextDisabled("K: split at the cursor"); - ImGui::Checkbox("Changes go to the whole road", &tool.whole); + if (tool.settings.changing()) + ImGui::TextDisabled("This piece has lanes of its own at its end, so it's changed alone"); + else + ImGui::Checkbox("Changes go to the whole road", &tool.whole); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nWith this off the selected piece alone is changed, and the road next to it is fitted to it:\nmade wider or narrower towards it, or given a stretch where the lanes change if their number differs.\nSplit a piece (K) to get a piece of the length you need."); ImGui::Separator(); @@ -2474,15 +2535,18 @@ void editor_mode::draw_road_overlay() const if (false == projection.room(drawlist)) break; auto const against{static_cast(road->definition().backward)}; - auto const &tracks{road->tracks()}; - for (std::size_t lane = 0; lane < tracks.size(); ++lane) + // a lane of a piece which gains or loses lanes is a path at each end of the piece; the ways between these are drawn below + auto const lanecount{road->definition().lanes.size()}; + for (std::size_t slot = 0; slot < 2 * lanecount; ++slot) { - if (tracks[lane] == nullptr || tracks[lane]->m_paths.empty()) + auto const lane{slot / 2}; + auto const *track{road->lane_track(lane, static_cast(slot % 2))}; + if (track == nullptr || track->m_paths.empty() || (slot % 2 == 1 && track == road->lane_track(lane, 0))) continue; - auto const &path{tracks[lane]->m_paths.front()}; + auto const &path{track->m_paths.front()}; auto const color{lane >= against ? kForwardColor : kBackwardColor}; drawpath(path, color, 2.0f, is_curved(path) ? pieces(path, road->location()) : 1); - if (range > 300.0) + if (range > 300.0 || road->definition().changing()) continue; auto const middle{path_point(path, 0.5)}; auto direction{path_point(path, 0.52) - middle}; @@ -2499,6 +2563,9 @@ void editor_mode::draw_road_overlay() const if (turn != nullptr && false == turn->m_paths.empty() && range < 300.0) drawpath(turn->m_paths.front(), kTurnColor, 1.5f, 8); } + // ways from the lanes at one end to the lanes at the other + for (auto const &way : road->ways()) + drawpath(way, kWayColor, 1.5f, pieces(way, road->location())); } // ways through the junctions for (auto const *junction : simulation::Junctions.sequence()) diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index 3fe9da2c..8e2113d1 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -143,6 +143,11 @@ road_node::state part(road_node::state const &State, double const From, double c result.median.width = {static_cast(State.median_width(From)), static_cast(State.median_width(To))}; // an island is rounded at the ends of the piece, not where the piece is cut result.median.round = {From <= 0.0 ? State.median.round[0] : 0.f, To >= 1.0 ? State.median.round[1] : 0.f}; + // and it's at its ends a piece is short of lanes + if (From > 0.0) + result.missing[0] = {0, 0}; + if (To < 1.0) + result.missing[1] = {0, 0}; for (std::size_t side = 0; side < result.banks.size(); ++side) { auto const &bank{State.banks[side]}; @@ -166,8 +171,8 @@ std::vector end_points(road_node::state const &State, bool const Ate if (glm::length(left) < 1e-9) return result; left = glm::normalize(left); - auto const halfwidth{0.5 * State.width()}; - auto const divide{State.divide()}; + auto const halfwidth{0.5 * State.width(Atend ? 1 : 0)}; + auto const divide{State.divide(Atend ? 1 : 0)}; // lanes going against the axis are on its left result.emplace_back(result.front() + left * (0.5 * (halfwidth + divide) * State.scale(t) + 0.5 * gap)); result.emplace_back(result.front() + left * (0.5 * (divide - halfwidth) * State.scale(t) - 0.5 * gap)); @@ -275,6 +280,12 @@ bool follow(road_node &Road, double const T, bool const Forward, double const Di Cut.road = piece; Cut.t = (remaining <= 0.0 ? from : parameter_at(lengths, forward ? here + remaining : here - remaining)); Cut.keepstart = (false == forward); + if (state.changing() && Cut.t > 0.001 && Cut.t < 0.999) + { + // partway along such a piece the road has no whole number of lanes to make an arm for + Error = "Too close to a place where the road changes its lanes"; + return false; + } return true; } // the junction takes this piece up to its end, as what would be left of it is too short to be a road @@ -313,9 +324,10 @@ bool cut_arm(road_cut const &Cut, junction_node::arm_data &Arm) Arm.position = state.point(Cut.t); // the way out of the junction is the way the kept part of the piece goes from the cut Arm.direction = planar(state.tangent(Cut.t)) * (Cut.keepstart ? -1.0 : 1.0); - // lanes heading for the cut lead into the junction - Arm.incoming = (Cut.keepstart ? state.forward : state.backward); - Arm.outgoing = (Cut.keepstart ? state.backward : state.forward); + // lanes heading for the cut lead into the junction. a piece which gains or loses lanes is only cut at one of its ends + auto const end{Cut.t > 0.5 ? 1 : 0}; + Arm.incoming = state.lanes_at(end, Cut.keepstart); + Arm.outgoing = state.lanes_at(end, false == Cut.keepstart); Arm.median = static_cast(state.median_width(Cut.t)); return editor_road::lane_width(state, Cut.t, Arm.width); } @@ -406,6 +418,8 @@ road_node::state flipped(road_node::state const &State) 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); + result.missing[0] = {State.missing[1][1], State.missing[1][0]}; + result.missing[1] = {State.missing[0][1], State.missing[0][0]}; // 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()); @@ -660,6 +674,10 @@ void editor_road::refresh(std::vector const &Roads, std::vectorm_editorremoved) + road->create_links(); for (auto *junction : Junctions) if (false == junction->m_editorremoved) junction->show(); @@ -1071,6 +1089,11 @@ bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string before = flipped(before); if (dropped.atend) after = flipped(after); + if (before.changing() || after.changing()) + { + Error = "the road changes its lanes next to the point. Give that piece the same lanes at both its ends first"; + return false; + } 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"; @@ -1134,7 +1157,8 @@ bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string road_node *editor_road::split(road_node &Road, double const T) { - if (T <= 0.001 || T >= 0.999) + // partway along a piece which gains or loses lanes the road has no whole number of them + if (T <= 0.001 || T >= 0.999 || Road.definition().changing()) return nullptr; auto first{Road.definition()}; auto second{first}; @@ -1194,13 +1218,16 @@ road_node *editor_road::split(road_node &Road, double const T) std::vector editor_road::chain(road_node &Road) { std::vector result{&Road}; + // a piece which gains or loses lanes is where a road of one layout ends and one of another begins + if (Road.definition().changing()) + return result; for (std::size_t idx = 0; idx < result.size(); ++idx) { for (auto const atend : {false, true}) { // a road goes on only where exactly two pieces meet auto const next{roads_at(axis_end(result[idx]->definition(), atend), result[idx])}; - if (next.size() == 1) + if (next.size() == 1 && false == next.front()->definition().changing()) add_unique(result, next.front()); } } @@ -1308,7 +1335,7 @@ editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double con result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0)); // the first of the points is the roadway of the lanes going against the axis result.half = (half == 1 ? -1 : 1); - auto const lanes{half == 1 ? state.backward : state.forward}; + auto const lanes{state.lanes_at(atend ? 1 : 0, half != 1)}; // lanes which leave the road there go along the axis of the road carrying them on, the ones which come to it go against that axis auto const leaving{(half == 2) == atend}; result.forward = (leaving ? lanes : 0); @@ -1632,8 +1659,8 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) auto const &layout{Road.definition()}; auto const position{axis_end(layout, atend)}; // lanes heading for this end of the piece, and the ones leading away from it - auto const arriving{atend ? layout.forward : layout.backward}; - auto const leaving{atend ? layout.backward : layout.forward}; + auto const arriving{layout.lanes_at(atend ? 1 : 0, atend)}; + auto const leaving{layout.lanes_at(atend ? 1 : 0, false == atend)}; float lanewidth{0.f}; auto const evenlanes{lane_width(layout, atend ? 1.0 : 0.0, lanewidth)}; auto const gap{static_cast(layout.median_width(atend ? 1.0 : 0.0))}; @@ -1678,14 +1705,15 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) auto const otherlayout{other->definition()}; auto const otheratend{glm::distance(axis_end(otherlayout, true), position) < kSamePoint}; // lanes of the neighbour heading for the place they meet, and the ones leading away from it - auto const otherarriving{otheratend ? otherlayout.forward : otherlayout.backward}; - auto const otherleaving{otheratend ? otherlayout.backward : otherlayout.forward}; + auto const otherarriving{otherlayout.lanes_at(otheratend ? 1 : 0, otheratend)}; + auto const otherleaving{otherlayout.lanes_at(otheratend ? 1 : 0, false == otheratend)}; + auto const othergap{static_cast(otherlayout.median_width(otheratend ? 1.0 : 0.0))}; if (otherarriving == leaving && otherleaving == arriving) { // the same lanes: the neighbour is made as wide as the piece where they meet, and gets to that width along its length - auto const width{layout.width() * layout.taper[atend ? 1 : 0]}; - auto const othergap{static_cast(otherlayout.median_width(otheratend ? 1.0 : 0.0))}; - if (std::abs(width - otherlayout.width() * otherlayout.taper[otheratend ? 1 : 0]) < 0.001 && std::abs(gap - othergap) < 0.001f) + auto const width{layout.width(atend ? 1 : 0) * layout.taper[atend ? 1 : 0]}; + auto const otherwidth{otherlayout.width(otheratend ? 1 : 0)}; + if (std::abs(width - otherwidth * otherlayout.taper[otheratend ? 1 : 0]) < 0.001 && std::abs(gap - othergap) < 0.001f) continue; if (false == can_edit(*other, &reason)) { @@ -1693,7 +1721,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) continue; } auto state{otherlayout}; - state.taper[otheratend ? 1 : 0] = std::clamp(static_cast(width / otherlayout.width()), 0.25f, 4.f); + state.taper[otheratend ? 1 : 0] = std::clamp(static_cast(width / otherwidth), 0.25f, 4.f); // and it leads its two directions as far apart as the piece has them there if (std::abs(gap - othergap) >= 0.001f) { @@ -1710,80 +1738,67 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) continue; } - // other lanes: the end of the neighbour is given up for a junction of the two, where the lanes of one are led to the lanes of the other - if (false == evenlanes) + // other lanes: the neighbour is led from the lanes it has to the lanes of the piece. a long one gives up its end for that, + // which is made a piece of its own; a short one, or one which changes its lanes already, does it along its whole length + float otherlanewidth{0.f}; + if (false == evenlanes || false == lane_width(otherlayout, otheratend ? 1.0 : 0.0, otherlanewidth)) { - Notes += "Lanes of \"" + Road.name() + "\"" + unevenlanes; + Notes += "Lanes of \"" + (evenlanes ? other->name() : Road.name()) + "\" differ in width, and a road changes the number of its lanes only if they're all as wide. "; continue; } - road_cut cut; - std::vector consumed; - std::string error; - bool room{false}; - for (auto const length : {kTransitionLength, 0.5 * kTransitionLength}) - { - consumed.clear(); - if (follow(*other, otheratend ? 1.0 : 0.0, false == otheratend, length, cut, consumed, error)) - { - room = true; - break; - } - } - if (false == room) - { - Notes += "There's no room next to \"" + Road.name() + "\" for the lanes to change (" + error + "), so they don't lead on there. "; - continue; - } - if (cut.road == &Road) - { - Notes += "The road is too short for its lanes to change. "; - continue; - } - std::vector removed{consumed}; - if (cut.road != other) - removed.emplace_back(other); - // a cut at the very end the piece is kept from leaves it whole - auto const trimmed{cut.keepstart ? cut.t < 0.999 : cut.t > 0.001}; - bool allowed{false == trimmed || can_edit(*cut.road, &reason)}; - for (auto const *piece : removed) - allowed = allowed && can_edit(*piece, &reason); - junction_node::state junction; - junction_node::arm_data arms[2]; - if (false == allowed) + if (false == can_edit(*other, &reason)) { Notes += "The lanes next to \"" + Road.name() + "\" weren't fitted: " + reason + ". "; continue; } - if (false == cut_arm(cut, arms[0])) + auto *stretch{other}; + if (false == otherlayout.changing() && otherlayout.length() > 1.5 * kTransitionLength) { - Notes += "Lanes of \"" + cut.road->name() + "\"" + unevenlanes; + auto const lengths{stations(otherlayout)}; + auto *rest{split(*other, parameter_at(lengths, otheratend ? lengths.back() - kTransitionLength : kTransitionLength))}; + if (rest == nullptr) + { + Notes += "\"" + other->name() + "\" couldn't be cut for the lanes to change. "; + continue; + } + // the neighbour goes on record the way it was before the cut + if (std::none_of(Record.roads.begin(), Record.roads.end(), [other](std::pair const &Entry) { return Entry.first == other; })) + Record.roads.emplace_back(other, otherlayout); + Record.roads_created.emplace_back(rest); + // the part past the cut is the one at the end of the neighbour + if (otheratend) + stretch = rest; + } + auto state{stretch->definition()}; + // the end the stretch shares with the piece gets the lanes of the piece, the other one keeps what it has + auto const end{otheratend ? 1 : 0}; + int counts[2][2]{{state.lanes_at(0, true), state.lanes_at(0, false)}, {state.lanes_at(1, true), state.lanes_at(1, false)}}; + // the lanes of the piece leading away from where they meet are the ones of the stretch heading for it + counts[end][0] = (otheratend ? leaving : arriving); + counts[end][1] = (otheratend ? arriving : leaving); + if (false == state.lanes.empty()) + state.lanewidth = state.lanes.front().width; + state.lanes_between(counts[0][0], counts[0][1], counts[1][0], counts[1][1]); + if (state.lanes_at(end, true) != counts[end][0] || state.lanes_at(end, false) != counts[end][1]) + { + Notes += "The lanes of \"" + Road.name() + "\" can't be led on to the road next to it. "; continue; } - arms[1].position = position; - arms[1].direction = planar(layout.tangent(atend ? 1.0 : 0.0)) * (atend ? -1.0 : 1.0); - arms[1].incoming = arriving; - arms[1].outgoing = leaving; - arms[1].width = lanewidth; - arms[1].median = gap; - junction.arms.assign(std::begin(arms), std::end(arms)); - junction.centre = 0.5 * (arms[0].position + arms[1].position); - dress(junction, layout); - // it's a stretch of the road, with the speed limit the road has - junction.velocity = std::min(layout.velocity > 0.f ? layout.velocity : std::numeric_limits::max(), otherlayout.velocity > 0.f ? otherlayout.velocity : std::numeric_limits::max()); - if (junction.velocity == std::numeric_limits::max()) - junction.velocity = -1.f; - junction.normalize(); - - // room is made first, so nothing gets joined with what's about to go - remove(removed); - Record.roads_removed.insert(Record.roads_removed.end(), removed.begin(), removed.end()); - if (trimmed) + // and it's as wide there as the piece is, with its directions as far apart + state.taper[end] = std::clamp(lanewidth / state.lanes.front().width, 0.25f, 4.f); + if (std::abs(gap - othergap) >= 0.001f) { - remember(Record, cut.road); - apply(std::vector>{{cut.road, cut.keepstart ? part(cut.road->definition(), 0.0, cut.t) : part(cut.road->definition(), cut.t, 1.0)}}); + if (false == state.divided()) + { + state.median.type = layout.median.type; + state.median.material = layout.median.material; + state.median.width = {0.f, 0.f}; + } + state.median.width[end] = gap; } - auto const created{create(std::vector{junction})}; - Record.junctions_created.insert(Record.junctions_created.end(), created.begin(), created.end()); + if (stretch == other) + remember(Record, other); + apply(std::vector>{{stretch, state}}); } } @@ -1812,6 +1827,11 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const Error = "The road would leave at too sharp an angle"; return false; } + if (layout.changing()) + { + Error = "\"" + Road.name() + "\" changes its lanes along the way, which leaves no place for a junction. Lead the road out of a piece next to it"; + return false; + } float lanewidth{0.f}; if (false == lane_width(layout, T, lanewidth)) { @@ -1988,6 +2008,7 @@ std::vector editor_road::roundabout(glm::dvec3 const &Centre, if (layout.forward < 1) layout.forward = std::max(1, layout.backward); layout.backward = 0; + layout.missing = {}; layout.lanes.clear(); layout.changes.clear(); layout.taper = {1.f, 1.f}; diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp index df71db18..20553b01 100644 --- a/editor/editorRoad.hpp +++ b/editor/editorRoad.hpp @@ -134,8 +134,8 @@ class editor_road static std::vector> arms_at(glm::dvec3 const &Point); // brings what's next to the ends of a piece in line with the piece, after its lanes or its width were changed: // a junction gets its arm made for the lanes the piece has now, a neighbouring piece with the same lanes is made as wide - // as the piece where they meet, and one with other lanes gets a transition cut out of its end - a junction of two roads, - // which leads the lanes of one to the lanes of the other. what gets done is added to Record, what couldn't be done to Notes + // as the piece where they meet, and one with other lanes is led to the lanes of the piece: its end, or the whole of it if it's short, + // becomes a piece which gains or loses lanes along the way. what gets done is added to Record, what couldn't be done to Notes static void settle(road_node &Road, record &Record, std::string &Notes); // width shared by all lanes of the road at specified value of the curve parameter of its axis. returns: false if the lanes differ in width static bool lane_width(road_node::state const &State, double const T, float &Width); diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 5a6f8ba4..7082bfc8 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -401,7 +401,9 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc } simulation::Paths.InitTracks(); - // the junctions tie the lanes of the roads together, what's left loose after that gets closed by the roads + // the roads tie up their own lanes where they gain or lose some, the junctions tie the lanes of the roads together, + // what's left loose after that gets closed by the roads + simulation::Roads.InitLanes(); simulation::Junctions.InitJunctions(); simulation::Roads.InitRoads(); // crossings and traffic points go by the lanes, which are complete at this point @@ -536,11 +538,29 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch ( Scratchpad.location.offset.empty() ? glm::dvec3 { 0.0 } : glm::dvec3 { Scratchpad.location.offset.top() } ) ); - if( false == simulation::Junctions.insert( junction ) ) { - ErrorLog( "Bad scenario: duplicate junction name \"" + junction->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + road_node::state stretch; + if( junction->as_road( stretch ) ) { + // a junction of two roads is where a road changes its lanes, which used to take a junction and is done by a road now. + // it's loaded as the road it would be, and marked as changed so it's written as one when the scenery is saved + delete junction; + nodedata.type = "road"; + auto *road { new road_node( nodedata ) }; + road->define( stretch ); + if( false == simulation::Roads.insert( road ) ) { + ErrorLog( "Bad scenario: duplicate road name \"" + road->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), road ); + road->create_lanes(); + road->mark_dirty(); + scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } ); + } + else { + if( false == simulation::Junctions.insert( junction ) ) { + ErrorLog( "Bad scenario: duplicate junction name \"" + junction->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), junction ); + scene::Layers.track( junction, { sourcebegin, Input.TokenEnd() } ); } - 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 diff --git a/world/Road.cpp b/world/Road.cpp index f0e6193e..78c008ea 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -35,6 +35,8 @@ constexpr double dash_step{2.0}; // longest straight piece of a painted line // speed and reach of the paths made to turn the vehicles back at a loose end of the road constexpr float turn_velocity{10.f}; constexpr double turn_reach{6.0}; +// length of the paths left of the lanes at the ends of a piece which gains or loses lanes +constexpr double lane_lead{2.0}; glm::dvec3 const up{0.0, 1.0, 0.0}; @@ -148,6 +150,48 @@ class road_axis { return frame(parameter(Station)); } + // control vector of specified end of a part of the curve, relative to that end + glm::dvec3 control(int const End, double const From, double const To) const + { + if (From <= 0.0 && To >= 1.0) + { + return control(End); + } + // a part of a cubic curve is a cubic curve, with the tangents scaled to the part of the parameter range it takes + return End == 0 ? derivative(From) * ((To - From) / 3.0) : derivative(To) * (-(To - From) / 3.0); + } + // value of the curve parameter at specified distance from the start of the axis + double parameter(double const Station) const + { + if (Station <= 0.0) + { + return 0.0; + } + if (Station >= length()) + { + return 1.0; + } + // first of the sampled distances past the requested one; it's never the very first sample + 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]}; + auto const fraction{span > 0.0 ? (Station - m_stations[idx - 1]) / span : 0.0}; + return (static_cast(idx - 1) + fraction) / samples; + } + // part of the length of the axis which lies before specified value of the curve parameter + double fraction(double const T) const + { + if (T <= 0.0 || length() <= 0.0) + { + return 0.0; + } + if (T >= 1.0) + { + return 1.0; + } + auto const position{T * samples}; + auto const idx{std::min(samples - 1, static_cast(position))}; + return glm::mix(m_stations[idx], m_stations[idx + 1], position - idx) / length(); + } // length of pieces the road is cut into along the axis; follows the rules the tracks use double step() const { @@ -167,22 +211,6 @@ class road_axis auto const u{1.0 - T}; return 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]); } - double parameter(double const Station) const - { - if (Station <= 0.0) - { - return 0.0; - } - if (Station >= length()) - { - return 1.0; - } - // first of the sampled distances past the requested one; it's never the very first sample - 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]}; - auto const fraction{span > 0.0 ? (Station - m_stations[idx - 1]) / span : 0.0}; - return (static_cast(idx - 1) + fraction) / samples; - } std::array m_points; std::array m_rolls{}; @@ -191,6 +219,16 @@ class road_axis std::vector m_stations; }; +// lines of a road a corner of a cross-section can be measured from. on a piece which gains or loses lanes the edges +// and the line the directions meet at move across the axis along the way, each on its own +enum profile_anchor : int +{ + anchor_axis = 0, + anchor_left = 1, // left edge of the lanes + anchor_right = 2, // right edge of the lanes + anchor_middle = 3 // the line the two directions meet at +}; + // corner of the cross-section of something laid along the road struct profile_point { @@ -201,6 +239,8 @@ struct profile_point double spread{0.0}; // how much of the gap between the two directions of the road it's moved by: 0.5 for their left edge, -0.5 for the right one double grow{0.0}; // what the shift changes by along the axis, evenly double sink{0.0}; // what the height changes by along the axis, evenly + int anchor{anchor_axis}; // line of the road the corner is measured from, one of profile_anchor + double tile{0.0}; // if set the image is laid across the road by where the corner is: its coordinate grows by one for each that many metres from the axis }; // how far the taper of a road has got at specified part of its length. it starts and ends level, so the pieces of a road @@ -231,6 +271,8 @@ void loft(std::vector &Output, road_axis const &Axis, std::vector< return glm::vec3{glm::length2(result) < 1e-12 ? up : glm::normalize(result)}; }; std::vector previous(pointcount), current(pointcount); + std::vector previoustexture(pointcount), currenttexture(pointcount); + auto const anchored{std::any_of(Profile.begin(), Profile.end(), [](profile_point const &Point) { return Point.anchor != anchor_axis; })}; axis_frame previousframe; double previousstation{From}; @@ -241,10 +283,19 @@ void loft(std::vector &Output, road_axis const &Axis, std::vector< auto const fraction{Axis.length() > 0.0 ? station / Axis.length() : 0.0}; auto const scale{Taper[0] == Taper[1] ? static_cast(Taper[0]) : glm::mix(static_cast(Taper[0]), static_cast(Taper[1]), taper_progress(fraction))}; auto const gap{false == divided ? 0.0 : glm::mix(static_cast(median[0]), static_cast(median[1]), taper_progress(fraction))}; + // where the lines the corners are measured from are at this section + std::array lead{0.0, 0.0, 0.0, 0.0}; + if (anchored) + { + auto const half{0.5 * Road.breadth(fraction)}; + lead = {0.0, half, -half, Road.middle(fraction)}; + } for (std::size_t idx = 0; idx < pointcount; ++idx) { auto const &point{Profile[idx]}; - current[idx] = frame.position + frame.left * (point.offset * scale + point.shift + point.spread * gap + point.grow * fraction) + up * (point.height + point.sink * fraction); + auto const across{lead[point.anchor] + (point.offset * scale + point.shift + point.spread * gap + point.grow * fraction)}; + current[idx] = frame.position + frame.left * across + up * (point.height + point.sink * fraction); + currenttexture[idx] = (point.tile > 0.0 ? static_cast(point.texture + across / point.tile) : point.texture); } if (section > 0) { @@ -254,10 +305,10 @@ void loft(std::vector &Output, road_axis const &Axis, std::vector< { auto const normalstart{normal(previousframe.tangent, previous[idx] - previous[idx + 1])}; auto const normalend{normal(frame.tangent, current[idx] - current[idx + 1])}; - world_vertex const leftstart{previous[idx], normalstart, {Profile[idx].texture, texturestart}}; - world_vertex const rightstart{previous[idx + 1], normalstart, {Profile[idx + 1].texture, texturestart}}; - world_vertex const leftend{current[idx], normalend, {Profile[idx].texture, textureend}}; - world_vertex const rightend{current[idx + 1], normalend, {Profile[idx + 1].texture, textureend}}; + world_vertex const leftstart{previous[idx], normalstart, {previoustexture[idx], texturestart}}; + world_vertex const rightstart{previous[idx + 1], normalstart, {previoustexture[idx + 1], texturestart}}; + world_vertex const leftend{current[idx], normalend, {currenttexture[idx], textureend}}; + world_vertex const rightend{current[idx + 1], normalend, {currenttexture[idx + 1], textureend}}; Output.push_back(leftstart); Output.push_back(rightstart); Output.push_back(leftend); @@ -268,11 +319,63 @@ void loft(std::vector &Output, road_axis const &Axis, std::vector< } } previous.swap(current); + previoustexture.swap(currenttexture); previousframe = frame; previousstation = station; } } +// shape of a line running beside a part of the axis of a road, from one distance from the axis to another, positive being to the left. +// From, To: values of the curve parameter of the axis. the line is laid out the way the axis goes +segment_data offset_path(segment_data const &Axis, road_axis const &Centre, double const From, double const To, double const Fromoffset, double const Tooffset) +{ + auto const begin{Centre.frame(From)}; + auto const end{Centre.frame(To)}; + + segment_data path; + path.points[segment_data::point::start] = begin.position + begin.left * Fromoffset; + path.points[segment_data::point::end] = end.position + end.left * Tooffset; + if (Centre.curved() || Fromoffset != Tooffset) + { + // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors. + // the control vectors go along the axis at both ends, which is what makes the lane meet the next piece without a bend + path.points[segment_data::point::control1] = Centre.control(0, From, To) * std::max(0.05, 1.0 - Fromoffset * std::cos(begin.roll) * Centre.curvature(From)); + path.points[segment_data::point::control2] = Centre.control(1, From, To) * std::max(0.05, 1.0 - Tooffset * std::cos(end.roll) * Centre.curvature(To)); + // if the roll changes along the road its sides rise or fall relative to the axis + auto const climb{(Tooffset * std::sin(end.roll) - Fromoffset * std::sin(begin.roll)) / 3.0}; + path.points[segment_data::point::control1].y += climb; + path.points[segment_data::point::control2].y -= climb; + } + path.rolls = {From <= 0.0 ? Axis.rolls[0] : glm::mix(Axis.rolls[0], Axis.rolls[1], static_cast(From)), + To >= 1.0 ? Axis.rolls[1] : glm::mix(Axis.rolls[0], Axis.rolls[1], static_cast(To))}; + if (Axis.radius != 0.f) + { + // the lanes on the inner side of the bend have it tighter + auto const turn{Centre.curvature(0.5 * (From + To)) >= 0.0 ? 1.0 : -1.0}; + path.radius = static_cast(std::max(1.0, std::abs(Axis.radius) - turn * 0.5 * (Fromoffset + Tooffset))); + } + return path; +} + +// turns a line laid out along the axis of a road into the path of a lane. Against: the lane goes against the axis +segment_data lane_shape(segment_data Path, bool const Against) +{ + if (Against) + { + std::swap(Path.points[segment_data::point::start], Path.points[segment_data::point::end]); + std::swap(Path.points[segment_data::point::control1], Path.points[segment_data::point::control2]); + Path.rolls = {-Path.rolls[1], -Path.rolls[0]}; + } + if (Global.bRollFix) + { + // with this setting on a rolled path gets raised when it's set up, by an amount fit for a railway track. + // the surface of the road stays where it is, so the lane is lowered beforehand to make up for it + Path.points[segment_data::point::start].y -= std::abs(std::sin(glm::radians(static_cast(Path.rolls[0])))) * 0.75; + Path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast(Path.rolls[1])))) * 0.75; + } + return Path; +} + // corner of a cross-section which changes along the road struct section_corner { @@ -381,10 +484,31 @@ void road_node::state::normalize() { forward = std::clamp(forward, 0, 8); backward = std::clamp(backward, 0, 8); + for (int direction = 0; direction < 2; ++direction) + { + auto &count{direction == 0 ? forward : backward}; + for (auto &end : missing) + { + end[direction] = std::clamp(end[direction], 0, count); + } + // lanes the piece has at neither of its ends aren't there at all + auto const both{std::min(missing[0][direction], missing[1][direction])}; + count -= both; + missing[0][direction] -= both; + missing[1][direction] -= both; + } if (forward + backward == 0) { forward = 1; } + for (auto &end : missing) + { + // there has to be a lane at each end + if (forward - end[0] + backward - end[1] < 1) + { + (forward > 0 ? end[0] : end[1]) = (forward > 0 ? forward : backward) - 1; + } + } lanewidth = std::max(1.f, lanewidth); weight = std::clamp(weight, 0.f, 10000.f); for (auto &width : median.width) @@ -493,6 +617,69 @@ int road_node::state::lane_index(std::string const &Id) const return Id[0] == 'f' ? backward + number - 1 : backward - number; } +// true if the piece has other lanes at its end than at its start +bool road_node::state::changing() const +{ + return missing[0][0] != 0 || missing[0][1] != 0 || missing[1][0] != 0 || missing[1][1] != 0; +} + +// sets the lanes the piece has at the start and at the end of its axis +void road_node::state::lanes_between(int const Startforward, int const Startbackward, int const Endforward, int const Endbackward) +{ + auto const along{std::clamp(std::max(Startforward, Endforward), 0, 8)}; + auto const against{std::clamp(std::max(Startbackward, Endbackward), 0, 8)}; + if (along != forward || against != backward) + { + // a different set of lanes starts with the common width and speed limit, and the default rules for changing lanes + forward = along; + backward = against; + lanes.clear(); + changes.clear(); + } + missing[0] = {forward - std::max(0, Startforward), backward - std::max(0, Startbackward)}; + missing[1] = {forward - std::max(0, Endforward), backward - std::max(0, Endbackward)}; + normalize(); +} + +// number of lanes at an end of the axis: of the ones going along it, or of the ones going against it +int road_node::state::lanes_at(int const End, bool const Along) const +{ + return Along ? forward - missing[End][0] : backward - missing[End][1]; +} + +// true if the piece has the lane at specified end of the axis +bool road_node::state::lane_at(std::size_t const Lane, int const End) const +{ + // the lanes of each direction are counted outwards from where the directions meet, and it's the outermost ones a piece is short of + auto const against{static_cast(backward)}; + return Lane < against ? Lane >= static_cast(missing[End][1]) : Lane - against < static_cast(forward - missing[End][0]); +} + +// the same road with the lanes it has at specified end of the axis all the way +road_node::state road_node::state::section(int const End) const +{ + auto result{*this}; + result.lanes.clear(); + result.changes.clear(); + for (std::size_t lane = 0; lane < lanes.size(); ++lane) + { + if (false == lane_at(lane, End)) + { + continue; + } + // the lanes a piece has at an end lie side by side, so the one before this one is its neighbour there as well + if (false == result.lanes.empty() && lane - 1 < changes.size()) + { + result.changes.emplace_back(changes[lane - 1]); + } + result.lanes.emplace_back(lanes[lane]); + } + result.forward = lanes_at(End, true); + result.backward = lanes_at(End, false); + result.missing = {}; + return result; +} + // permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise int road_node::state::default_change(std::size_t const Boundary) const { @@ -516,6 +703,41 @@ double road_node::state::lane_offset(std::size_t const Lane) const return Lane < lanes.size() ? offset - 0.5 * lanes[Lane].width : offset; } +// the same at an end of the axis, where the road is centred on the axis with the lanes it has there +double road_node::state::lane_offset(std::size_t const Lane, int const End) const +{ + auto const against{static_cast(backward)}; + auto lane{Lane}; + if (Lane < lanes.size() && false == lane_at(Lane, End)) + { + // a lane the road is short of there comes out of the outermost lane its direction has at that end, or goes into it. + // with no such lane it starts, or ends, at the line the directions meet at + if (lanes_at(End, Lane >= against) < 1) + { + return divide(End); + } + lane = (Lane < against ? static_cast(missing[End][1]) : against + static_cast(lanes_at(End, true) - 1)); + } + auto offset{0.5 * width(End)}; + for (std::size_t idx = 0; idx < lane && idx < lanes.size(); ++idx) + { + if (lane_at(idx, End)) + { + offset -= lanes[idx].width; + } + } + return lane < lanes.size() ? offset - 0.5 * lanes[lane].width : offset; +} + +// distance of the middle of a lane from the axis at specified part of the length of the axis, with the taper and what's between the directions counted in +double road_node::state::lane_position(std::size_t const Lane, double const Fraction) const +{ + auto const side{Lane < static_cast(backward) ? 0.5 : -0.5}; + auto const beginoffset{lane_offset(Lane, 0) * taper[0] + side * median_width(0.0)}; + auto const endoffset{lane_offset(Lane, 1) * taper[1] + side * median_width(1.0)}; + return glm::mix(beginoffset, endoffset, taper_progress(Fraction)); +} + // combined width of the lanes, before the taper double road_node::state::width() const { @@ -527,6 +749,26 @@ double road_node::state::width() const return total; } +// the same for the lanes the piece has at an end of the axis +double road_node::state::width(int const End) const +{ + double total{0.0}; + for (std::size_t lane = 0; lane < lanes.size(); ++lane) + { + if (lane_at(lane, End)) + { + total += lanes[lane].width; + } + } + return total; +} + +// combined width of the lanes at specified part of the length of the axis, with the taper counted in +double road_node::state::breadth(double const Fraction) const +{ + return glm::mix(width(0) * taper[0], width(1) * taper[1], taper_progress(Fraction)); +} + // what the widths of the lanes are multiplied by at specified value of the curve parameter double road_node::state::scale(double const T) const { @@ -556,10 +798,30 @@ double road_node::state::divide() const return offset; } +// the same for the lanes the piece has at an end of the axis +double road_node::state::divide(int const End) const +{ + auto offset{0.5 * width(End)}; + for (std::size_t idx = 0; idx < static_cast(backward) && idx < lanes.size(); ++idx) + { + if (lane_at(idx, End)) + { + offset -= lanes[idx].width; + } + } + return offset; +} + +// the same at specified part of the length of the axis, with the taper counted in +double road_node::state::middle(double const Fraction) const +{ + return glm::mix(divide(0) * taper[0], divide(1) * taper[1], taper_progress(Fraction)); +} + // width of the whole roadway at specified value of the curve parameter double road_node::state::span(double const T) const { - return width() * scale(T) + median_width(T); + return (changing() ? breadth(T) : width() * scale(T)) + median_width(T); } // shape of the middle of a lane, laid out in the direction of travel @@ -567,49 +829,30 @@ segment_data road_node::state::lane_path(std::size_t const Lane) const { road_axis const centre{axis}; // on a road which gets wider or narrower the lane moves away from the axis or towards it along the way, - // and so it does where the two directions are led apart + // and so it does where the two directions are led apart, or where the road gains or loses lanes auto const side{Lane < static_cast(backward) ? 0.5 : -0.5}; - auto const beginoffset{lane_offset(Lane) * taper[0] + side * median_width(0.0)}; - auto const endoffset{lane_offset(Lane) * taper[1] + side * median_width(1.0)}; - auto const begin{centre.frame(0.0)}; - auto const end{centre.frame(1.0)}; + auto const beginoffset{lane_offset(Lane, 0) * taper[0] + side * median_width(0.0)}; + auto const endoffset{lane_offset(Lane, 1) * taper[1] + side * median_width(1.0)}; + return lane_shape(offset_path(axis, centre, 0.0, 1.0, beginoffset, endoffset), Lane < static_cast(backward)); +} - segment_data path; - path.points[segment_data::point::start] = begin.position + begin.left * beginoffset; - path.points[segment_data::point::end] = end.position + end.left * endoffset; - if (centre.curved() || beginoffset != endoffset) - { - // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors. - // the control vectors go along the axis at both ends, which is what makes the lane meet the next piece without a bend - path.points[segment_data::point::control1] = centre.control(0) * std::max(0.05, 1.0 - beginoffset * std::cos(begin.roll) * centre.curvature(0.0)); - path.points[segment_data::point::control2] = centre.control(1) * std::max(0.05, 1.0 - endoffset * std::cos(end.roll) * centre.curvature(1.0)); - // if the roll changes along the road its sides rise or fall relative to the axis - auto const climb{(endoffset * std::sin(end.roll) - beginoffset * std::sin(begin.roll)) / 3.0}; - path.points[segment_data::point::control1].y += climb; - path.points[segment_data::point::control2].y -= climb; - } - path.rolls = axis.rolls; - if (axis.radius != 0.f) - { - // the lanes on the inner side of the bend have it tighter - auto const turn{centre.curvature(0.5) >= 0.0 ? 1.0 : -1.0}; - path.radius = static_cast(std::max(1.0, std::abs(axis.radius) - turn * 0.5 * (beginoffset + endoffset))); - } - if (Lane < static_cast(backward)) - { - // the lane goes against the axis - std::swap(path.points[segment_data::point::start], path.points[segment_data::point::end]); - std::swap(path.points[segment_data::point::control1], path.points[segment_data::point::control2]); - path.rolls = {-axis.rolls[1], -axis.rolls[0]}; - } - if (Global.bRollFix) - { - // with this setting on a rolled path gets raised when it's set up, by an amount fit for a railway track. - // the surface of the road stays where it is, so the lane is lowered beforehand to make up for it - path.points[segment_data::point::start].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[0])))) * 0.75; - path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast(path.rolls[1])))) * 0.75; - } - return path; +// shape of the way from the middle of a lane to the middle of another over a part of the axis, laid out in the direction of travel +segment_data road_node::state::lane_path(std::size_t const Lane, std::size_t const Tolane, double const From, double const To) const +{ + road_axis const centre{axis}; + auto const against{Lane < static_cast(backward)}; + // the traffic going against the axis gets to the part by its far end + auto const fromoffset{lane_position(against ? Tolane : Lane, centre.fraction(From))}; + auto const tooffset{lane_position(against ? Lane : Tolane, centre.fraction(To))}; + return lane_shape(offset_path(axis, centre, From, To, fromoffset, tooffset), against); +} + +// part of the axis over which a piece which gains or loses lanes leads the lanes it has at one end to the lanes it has at the other +std::array road_node::state::change_range() const +{ + road_axis const centre{axis}; + auto const lead{std::min(lane_lead, 0.2 * centre.length())}; + return {centre.parameter(lead), centre.parameter(centre.length() - lead)}; } // length of the axis @@ -654,6 +897,8 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) }; std::vector laneproperties; std::vector changeproperties; + // lanes at the end of the axis, if these are other than the ones at its start + std::array endlanes{-1, -1}; auto token{Input.getToken()}; while (false == token.empty() && token != "endroad") @@ -664,6 +909,12 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) Input.getTokens(2); Input >> m_state.forward >> m_state.backward; } + else if (token == "lanesto") + { + // lanesto + Input.getTokens(2); + Input >> endlanes[0] >> endlanes[1]; + } else if (token == "width") { Input.getTokens(); @@ -848,6 +1099,29 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) ErrorLog("Bad road: " + label + " has no lanes, a single one is used instead"); } auto const environment{m_state.environment}; + if (endlanes[0] >= 0 || endlanes[1] >= 0) + { + if (std::max(0, endlanes[0]) + std::max(0, endlanes[1]) <= 0) + { + ErrorLog("Bad road: " + label + " has no lanes at its end, a single one is used instead"); + } + m_state.lanes_between(m_state.forward, m_state.backward, endlanes[0], endlanes[1]); + // the traffic going along the axis gets on the piece at its start, the one going against it at its end + int const gains[]{m_state.lanes_at(1, true) - m_state.lanes_at(0, true), m_state.lanes_at(0, false) - m_state.lanes_at(1, false)}; + int const leaving[]{m_state.lanes_at(1, true), m_state.lanes_at(0, false)}; + int const arriving[]{m_state.lanes_at(0, true), m_state.lanes_at(1, false)}; + for (int direction = 0; direction < 2; ++direction) + { + if (arriving[direction] > 0 && gains[direction] > 2) + { + ErrorLog("Bad road: " + label + " gains more than two lanes at once, the ones past the second can't be got on"); + } + if (arriving[direction] > 0 && leaving[direction] == 0) + { + ErrorLog("Bad road: " + label + " has lanes which lead nowhere, as their direction ends within it"); + } + } + } m_state.normalize(); if (environment != m_state.environment) { @@ -924,10 +1198,202 @@ void road_node::define(state const &State) // creates paths of the lanes and hands them over to the simulation void road_node::create_lanes() { - m_tracks.assign(m_state.lanes.size(), nullptr); - for (std::size_t lane = 0; lane < m_state.lanes.size(); ++lane) + auto const count{m_state.lanes.size()}; + m_tracks.clear(); + for (auto &tracks : m_lanetracks) { - m_tracks[lane] = create_track(m_name.empty() ? std::string{} : m_name + ":" + m_state.lane_id(lane), m_state.lane_path(lane), m_state.lanes[lane].width, m_state.lanes[lane].velocity); + tracks.assign(count, nullptr); + } + auto const range{m_state.change_range()}; + for (std::size_t lane = 0; lane < count; ++lane) + { + auto const along{lane >= static_cast(m_state.backward)}; + auto const name{m_name.empty() ? std::string{} : m_name + ":" + m_state.lane_id(lane)}; + auto const &data{m_state.lanes[lane]}; + if (m_state.missing[0][along ? 0 : 1] == 0 && m_state.missing[1][along ? 0 : 1] == 0) + { + // the direction keeps its lanes all the way, and so each of them is a single path + auto *track{create_track(name, m_state.lane_path(lane), data.width, data.velocity)}; + m_lanetracks[0][lane] = track; + m_lanetracks[1][lane] = track; + m_tracks.emplace_back(track); + continue; + } + // the direction gains or loses lanes along the way. a lane gets a short path at each end the piece has it at, which is what + // the neighbours and the junctions join; what leads from the ones at one end to the ones at the other is made by create_links() + for (int end = 0; end < 2; ++end) + { + if (false == m_state.lane_at(lane, end)) + { + continue; + } + // the traffic going along the axis gets on the piece at its start + auto const entry{(end == 0) == along}; + auto *track{create_track(name.empty() || entry ? name : name + ":out", m_state.lane_path(lane, lane, end == 0 ? 0.0 : range[1], end == 0 ? range[0] : 1.0), data.width, data.velocity)}; + m_lanetracks[end][lane] = track; + m_tracks.emplace_back(track); + } + } +} + +// makes the paths which lead the lanes the piece has at one end to the lanes it has at the other, where these differ +void road_node::create_links() +{ + if (false == m_links.empty() || false == m_state.changing() || m_lanetracks[0].size() != m_state.lanes.size()) + { + return; + } + auto const range{m_state.change_range()}; + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + auto const write_point = [&text](glm::dvec3 const &Point) { text << Point.x << ' ' << Point.y << ' ' << Point.z << ' '; }; + for (int direction = 0; direction < 2; ++direction) + { + if (m_state.missing[0][direction] == 0 && m_state.missing[1][direction] == 0) + { + continue; + } + auto const along{direction == 0}; + // ends of the axis the traffic of the direction gets on the piece and leaves it by + auto const entry{along ? 0 : 1}; + auto const exit{1 - entry}; + auto const arriving{m_state.lanes_at(entry, along)}; + auto const leaving{m_state.lanes_at(exit, along)}; + // place of a lane of the direction, counted from where the directions meet, in the left to right order + auto const index = [&](int const Number) { return static_cast(along ? m_state.backward + Number - 1 : m_state.backward - Number); }; + for (int number = 1; number <= arriving && leaving > 0; ++number) + { + auto const lane{index(number)}; + auto *head{m_lanetracks[entry][lane]}; + if (head == nullptr) + { + continue; + } + // a lane leads to itself; one the road loses leads to the outermost lane left, and the outermost lane of a road + // which gains lanes leads to these as well. a path has up to three ways out, so that's two lanes gained at most + std::vector targets{index(std::min(number, leaving))}; + for (int extra = arriving + 1; number == arriving && extra <= leaving && targets.size() < 3; ++extra) + { + targets.emplace_back(index(extra)); + } + std::vector tails; + std::vector ways; + for (auto const target : targets) + { + if (m_lanetracks[exit][target] != nullptr) + { + tails.emplace_back(m_lanetracks[exit][target]); + ways.emplace_back(m_state.lane_path(lane, target, range[0], range[1])); + } + } + if (tails.empty()) + { + continue; + } + m_ways.insert(m_ways.end(), ways.begin(), ways.end()); + auto const name{m_name.empty() ? std::string{} : m_name + ":" + m_state.lane_id(lane) + ":way"}; + auto const &data{m_state.lanes[lane]}; + if (tails.size() == 1) + { + auto *link{create_track(name, ways[0], data.width, data.velocity)}; + // tied by hand: lanes the road loses end where the one they lead to carries on, which is more than joining path ends can express + head->ConnectNextPrev(link, 0); + link->trNext = tails[0]; + link->iNextDirection = 0; + if (tails[0]->trPrev == nullptr) + { + tails[0]->trPrev = link; + tails[0]->iPrevDirection = 1; + } + m_links.emplace_back(link); + continue; + } + // a fork is a crossroads, which is what the drivers know how to pick a way through. the ways leave the lane + // the way it goes, and each joins its lane the way that one goes + // NOTE: a crossroads isn't set up the way a rolled path is, so its points are given as they are and it's level across + auto const raise = [](glm::dvec3 Point, float const Roll) { + if (Global.bRollFix) + { + Point.y += std::abs(std::sin(glm::radians(static_cast(Roll)))) * 0.75; + } + return Point; + }; + auto const start{raise(ways[0].points[segment_data::point::start], ways[0].rolls[0])}; + auto const startcontrol{ways[0].points[segment_data::point::control1]}; + std::vector ends; + std::vector endcontrols; + for (auto const &way : ways) + { + ends.emplace_back(raise(way.points[segment_data::point::end], way.rolls[1])); + endcontrols.emplace_back(way.points[segment_data::point::control2]); + } + text.str(""); + text << "cross " << glm::distance(start, ends[0]) << ' ' << data.width << ' ' << m_state.friction << ' ' << m_state.sounddistance << ' ' << m_state.quality << " 0 " << m_state.environment + << " unvis "; + // first path: from the lane to itself + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[0]); + write_point(ends[0]); + text << "0 0 "; + if (ways.size() == 2) + { + // second path of a crossroads of three roads starts where the first one does + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[1]); + write_point(ends[1]); + text << "0 0 "; + } + else + { + // second path of a crossroads of four roads runs between its third and fourth point + write_point(ends[1]); + text << "0 "; + write_point(endcontrols[1]); + write_point(endcontrols[2]); + write_point(ends[2]); + text << "0 0 "; + } + if (data.velocity > 0.f) + { + text << "velocity " << data.velocity << ' '; + } + text << "endtrack"; + auto *link{create_track(name, text.str())}; + if (link->SwitchExtension != nullptr) + { + auto &extension{*link->SwitchExtension}; + // the points of the crossroads are stored as: first, second, fourth, third + extension.pPrevs[0] = head; + extension.iPrevDirection[0] = 1; + extension.pNexts[0] = tails[0]; + extension.iNextDirection[0] = 0; + extension.pPrevs[1] = (tails.size() == 3 ? tails[2] : tails[1]); + extension.iPrevDirection[1] = 0; + extension.pNexts[1] = (tails.size() == 3 ? tails[1] : nullptr); + extension.iNextDirection[1] = 0; + link->Switch(0); + head->trNext = link; + head->iNextDirection = 0; + // the drivers take each of the lanes as readily as any other + link->m_routeweights = {100.f, 100.f, tails.size() == 3 ? 100.f : 0.f}; + // the lanes get to know the path leading to them, for whatever goes backwards and for those who look who's coming + int const ends2[]{1, 2}; + int const ends3[]{1, 3, 2}; + for (std::size_t idx = 0; idx < tails.size(); ++idx) + { + if (tails[idx]->trPrev == nullptr) + { + tails[idx]->trPrev = link; + tails[idx]->iPrevDirection = (tails.size() == 3 ? ends3[idx] : ends2[idx]); + } + } + } + m_links.emplace_back(link); + } } } @@ -935,7 +1401,7 @@ void road_node::create_lanes() void road_node::close_ends() { // NOTE: without the link a vehicle reaching the end would be sent back on the lane it came by, against its direction - if (m_tracks.size() != m_state.lanes.size()) + if (m_lanetracks[0].size() != m_state.lanes.size()) { return; } @@ -944,12 +1410,12 @@ void road_node::close_ends() { // the lanes going along the axis leave the road at its end, the ones going against it leave at its start auto const outwards{end == 1 ? axis.frame(1.0).tangent : -axis.frame(0.0).tangent}; - for (int pair = 1; pair <= std::min(m_state.forward, m_state.backward); ++pair) + for (int pair = 1; pair <= std::min(m_state.lanes_at(end, true), m_state.lanes_at(end, false)); ++pair) { auto const forward{static_cast(m_state.backward + pair - 1)}; auto const backward{static_cast(m_state.backward - pair)}; - auto *exit{m_tracks[end == 1 ? forward : backward]}; - auto *entry{m_tracks[end == 1 ? backward : forward]}; + auto *exit{m_lanetracks[end][end == 1 ? forward : backward]}; + auto *entry{m_lanetracks[end][end == 1 ? backward : forward]}; if (exit == nullptr || entry == nullptr || exit->trNext != nullptr || entry->trPrev != nullptr) { continue; @@ -974,6 +1440,13 @@ std::vector road_node::release_lanes() { std::vector released; released.swap(m_tracks); + released.insert(released.end(), m_links.begin(), m_links.end()); + m_links.clear(); + m_ways.clear(); + for (auto &tracks : m_lanetracks) + { + tracks.clear(); + } released.erase(std::remove(released.begin(), released.end(), nullptr), released.end()); return released; } @@ -989,7 +1462,7 @@ std::vector road_node::release_turns() // true if there's a vehicle on any path of the road bool road_node::occupied() const { - for (auto const *tracks : {&m_tracks, &m_turns}) + for (auto const *tracks : {&m_tracks, &m_links, &m_turns}) { for (auto const *track : *tracks) { @@ -1039,26 +1512,42 @@ std::vector road_node::create_shapes() const auto const widest{static_cast(std::max(road.median.width[0], road.median.width[1]))}; auto const height{static_cast(road.kerbheight)}; auto const usable = [](std::string const &Material) { return false == Material.empty() && Material != "none"; }; + // on a piece which gains or loses lanes the edges and the line the directions meet at move across the axis along the way, + // so what's laid along them is measured from them. elsewhere they keep their distance from the axis, and it's measured from the axis + auto const changing{road.changing()}; + // distance of one of these lines from the axis, on a road as wide as its lanes say + auto const distance = [&](int const Line) { return Line == anchor_left ? halfwidth : Line == anchor_right ? -halfwidth : Line == anchor_middle ? divide : 0.0; }; + // where the line the directions meet at is, for specified distance from the start of the axis + auto const meeting = [&](double const Station) { + return changing ? road.middle(Station / length) : divide * glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(Station / length)); + }; // surface. the image is centered on the axis, and repeated if the road is wider if (road.surface != "none") { auto const tile{static_cast(texture_ratio(road.surface)) * texturelength}; - auto const corner = [&](double const Offset, double const Spread) { return profile_point{Offset, 0.0, static_cast(0.5 + (Offset + Spread * widest) / tile), 0.0, Spread}; }; + auto const corner = [&](int const Line, double const Spread) { + if (changing) + { + return profile_point{0.0, 0.0, 0.5f, 0.0, Spread, 0.0, 0.0, Line, tile}; + } + auto const offset{distance(Line)}; + return profile_point{offset, 0.0, static_cast(0.5 + (offset + Spread * widest) / tile), 0.0, Spread}; + }; auto &surface{vertices(road.surface, lighting_data{})}; if (false == divided) { - loft(surface, axis, {corner(halfwidth, 0.0), corner(-halfwidth, 0.0)}, 0.0, length, step, texturelength, road); + loft(surface, axis, {corner(anchor_left, 0.0), corner(anchor_right, 0.0)}, 0.0, length, step, texturelength, road); } else { - loft(surface, axis, {corner(halfwidth, 0.5), corner(divide, 0.5)}, 0.0, length, step, texturelength, road); + loft(surface, axis, {corner(anchor_left, 0.5), corner(anchor_middle, 0.5)}, 0.0, length, step, texturelength, road); if (road.median.type != median_type::gap) { // the surface carries on between the roadways; an island stands on it, so there's something under its rounded ends - loft(surface, axis, {corner(divide, 0.5), corner(divide, -0.5)}, 0.0, length, step, texturelength, road); + loft(surface, axis, {corner(anchor_middle, 0.5), corner(anchor_middle, -0.5)}, 0.0, length, step, texturelength, road); } - loft(surface, axis, {corner(divide, -0.5), corner(-halfwidth, -0.5)}, 0.0, length, step, texturelength, road); + loft(surface, axis, {corner(anchor_middle, -0.5), corner(anchor_right, -0.5)}, 0.0, length, step, texturelength, road); } } @@ -1087,7 +1576,7 @@ std::vector road_node::create_shapes() const stations.emplace_back(length - nose[1] * (1.0 - std::sin(quarter * idx / nosesteps))); } // middle of the island and half of its width, for specified distance from the start of the axis - auto const middle = [&](double const Station) { return divide * glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(Station / length)); }; + auto const &middle{meeting}; auto const half = [&](double const Station) { auto result{0.5 * glm::mix(static_cast(road.median.width[0]), static_cast(road.median.width[1]), taper_progress(Station / length))}; if (Station < nose[0]) @@ -1169,7 +1658,8 @@ std::vector road_node::create_shapes() const auto const emit = [&](std::string const &Material, std::vector Profile) { for (auto &point : Profile) { - point.offset = direction * halfwidth; + point.offset = (changing ? 0.0 : direction * halfwidth); + point.anchor = (changing ? (side == 0 ? anchor_left : anchor_right) : anchor_axis); point.spread = (divided ? direction * 0.5 : 0.0); point.shift *= direction; point.grow *= direction; @@ -1187,7 +1677,7 @@ std::vector road_node::create_shapes() const auto const tile{static_cast(texture_ratio(Material)) * texturelength}; for (int end = 0; end < 2; ++end) { - auto const edge{direction * (halfwidth * road.taper[end] + (divided ? 0.5 * road.median.width[end] : 0.0))}; + auto const edge{direction * (0.5 * road.width(end) * road.taper[end] + (divided ? 0.5 * road.median.width[end] : 0.0))}; cap(vertices(Material, lighting_data{}), axis.at(end == 0 ? 0.0 : length), {{edge + direction * From, 0.0, 0.f}, {edge + direction * From, height, 0.f}, {edge + direction * To, height, 0.f}, {edge + direction * To, 0.0, 0.f}}, end == 1, tile); } @@ -1258,30 +1748,79 @@ std::vector road_node::create_shapes() const paint.ambient = paint.diffuse; auto &lines{vertices("colored", paint)}; - // Offset: where the line runs on a road as wide as its lanes say, Shift: how far from there it's kept whatever the width, - // Spread: which of the roadways it goes with, where the road has two - auto const solid = [&](double const Offset, double const Shift, double const Spread) { - std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread}}; - loft(lines, axis, line, 0.0, length, std::min(step, dash_step), texturelength, road); + // Line, Offset: where the line runs on a road as wide as its lanes say, as a distance from one of the lines of the road, + // Shift: how far from there it's kept whatever the width, Spread: which of the roadways it goes with, where the road has two, + // From, To: part of the piece it's painted on + auto const solid = [&](int const Line, double const Offset, double const Shift, double const Spread, double const From, double const To) { + std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread, 0.0, 0.0, Line}, + {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread, 0.0, 0.0, Line}}; + loft(lines, axis, line, From, To, std::min(step, dash_step), texturelength, road); }; // the dashes are fitted so the gaps at both ends of the road are a half of the regular one, which makes them match the next piece - auto const dashed = [&](double const Offset, double const Shift, double const Spread) { + auto const dashed = [&](int const Line, double const Offset, double const Shift, double const Spread, double const From, double const To) { auto const count{std::max(1, static_cast(std::lround(length / dash_period)))}; auto const period{length / count}; - std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread}}; + std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread, 0.0, 0.0, Line}, + {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread, 0.0, 0.0, Line}}; for (int dash = 0; dash < count; ++dash) { auto const from{(dash + 1.0 / 3.0) * period}; + if (from < From || from + period / 3.0 > To) + { + continue; + } loft(lines, axis, line, from, from + period / 3.0, dash_step, texturelength, road); } }; + // part of the piece the line between two lanes is painted on. a lane the road gains or loses along the piece + // gets its line where there's room for a half of it + auto const stretch = [&](std::size_t const Boundary) { + auto const against{static_cast(road.backward)}; + std::pair result{0.0, length}; + if (false == changing || Boundary + 1 == against) + { + return result; + } + // the one of the two which is further from where the directions meet + auto const outer{Boundary + 1 < against ? Boundary : Boundary + 1}; + if (road.lane_at(outer, 0) && road.lane_at(outer, 1)) + { + return result; + } + // what the direction needs to be as wide as for the line, on a road as wide as its lanes say + auto needed{0.5 * road.lanes[outer].width}; + for (auto lane{outer < against ? outer + 1 : against}; lane < (outer < against ? against : outer); ++lane) + { + needed += road.lanes[lane].width; + } + int const samples{64}; + auto const room = [&](int const Sample) { + auto const fraction{static_cast(Sample) / samples}; + auto const half{0.5 * road.breadth(fraction)}; + auto const middle{road.middle(fraction)}; + auto const scale{glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(fraction))}; + return (outer < against ? half - middle : half + middle) >= needed * scale; + }; + auto first{0}; + while (first <= samples && false == room(first)) + { + ++first; + } + auto last{samples}; + while (last >= first && false == room(last)) + { + --last; + } + result = {length * first / samples, length * last / samples}; + return result; + }; // edges of the surface auto const inset{edge_inset + 0.5 * line_width}; - if (halfwidth * std::min(road.taper[0], road.taper[1]) - inset > 0.5) + if (0.5 * std::min(road.width(0) * road.taper[0], road.width(1) * road.taper[1]) - inset > 0.5) { - solid(halfwidth, -inset, divided ? 0.5 : 0.0); - solid(-halfwidth, inset, divided ? -0.5 : 0.0); + solid(changing ? anchor_left : anchor_axis, changing ? 0.0 : halfwidth, -inset, divided ? 0.5 : 0.0, 0.0, length); + solid(changing ? anchor_right : anchor_axis, changing ? 0.0 : -halfwidth, inset, divided ? -0.5 : 0.0, 0.0, length); } // lines between the lanes. a lane can be left across a dashed line, or across a pair of lines if the dashed one is on its side auto offset{halfwidth}; @@ -1290,11 +1829,19 @@ std::vector road_node::create_shapes() const offset -= road.lanes[boundary].width; auto const opposite{road.backward > 0 && boundary + 1 == static_cast(road.backward)}; auto const pair{0.5 * (line_width + line_spacing)}; + // the lanes keep their widths, and so the lines between them keep their distance from where the directions meet + auto const from{changing ? anchor_middle : anchor_axis}; + auto const place{changing ? offset - divide : offset}; + auto const range{stretch(boundary)}; + if (changing && range.second - range.first < 1.0) + { + continue; + } if (opposite && divided) { // each of the two roadways gets its edge line - solid(offset, inset, 0.5); - solid(offset, -inset, -0.5); + solid(from, place, inset, 0.5, range.first, range.second); + solid(from, place, -inset, -0.5, range.first, range.second); continue; } auto const spread{false == divided ? 0.0 : boundary + 1 < static_cast(road.backward) ? 0.5 : -0.5}; @@ -1302,19 +1849,19 @@ std::vector road_node::create_shapes() const { case change_both: { - dashed(offset, 0.0, spread); + dashed(from, place, 0.0, spread, range.first, range.second); break; } case change_toright: { - dashed(offset, pair, spread); - solid(offset, -pair, spread); + dashed(from, place, pair, spread, range.first, range.second); + solid(from, place, -pair, spread, range.first, range.second); break; } case change_toleft: { - solid(offset, pair, spread); - dashed(offset, -pair, spread); + solid(from, place, pair, spread, range.first, range.second); + dashed(from, place, -pair, spread, range.first, range.second); break; } default: @@ -1322,12 +1869,12 @@ std::vector road_node::create_shapes() const if (opposite) { // traffic going opposite ways is kept apart with a double line - solid(offset, pair, spread); - solid(offset, -pair, spread); + solid(from, place, pair, spread, range.first, range.second); + solid(from, place, -pair, spread, range.first, range.second); } else { - solid(offset, 0.0, spread); + solid(from, place, 0.0, spread, range.first, range.second); } break; } @@ -1344,8 +1891,7 @@ std::vector road_node::create_shapes() const auto const edge = [&](double const Station, double const Side) { auto const station{std::clamp(Station, 0.0, length)}; auto const frame{axis.at(station)}; - auto const scale{glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(station / length))}; - return frame.position + frame.left * (divide * scale + Side * std::max(0.0, 0.5 * gap_at(station) - 2.0 * inset)) + up * line_lift; + return frame.position + frame.left * (meeting(station) + Side * std::max(0.0, 0.5 * gap_at(station) - 2.0 * inset)) + up * line_lift; }; // the stripes are spread evenly over the piece, and none of them reaches past its ends auto const count{std::max(1, static_cast(std::lround(length / period)))}; @@ -1529,6 +2075,27 @@ double lane_grade(TTrack const &Lane, bool const Atend) return run > 1e-6 ? direction.y / run : 0.0; } +// what a driver calls the way of a fork which leads to specified lane: the number of the path of the crossroads it's on, +// negative if it's taken against the path. 0 if the fork doesn't lead to the lane +int fork_code(TTrack const &Fork, TTrack const *Lane) +{ + if (Fork.SwitchExtension == nullptr || Lane == nullptr) + { + return 0; + } + auto const &extension{*Fork.SwitchExtension}; + if (extension.pNexts[0] == Lane) + { + return 1; + } + if (extension.pNexts[1] != nullptr) + { + // three ways out + return extension.pNexts[1] == Lane ? 5 : extension.pPrevs[1] == Lane ? -4 : 0; + } + return extension.pPrevs[1] == Lane ? -2 : 0; +} + } // namespace // puts provided shapes in the section of the scene holding specified point @@ -1997,6 +2564,60 @@ void junction_node::define(state const &State) m_area.radius = -1.f; } +// a junction of two roads is a stretch where a road changes its lanes, which a road can do by itself +bool junction_node::as_road(road_node::state &Road) const +{ + auto const &arms{m_state.arms}; + if (arms.size() != 2) + { + return false; + } + auto const &from{arms[0]}; + auto const &to{arms[1]}; + auto const span{glm::distance(from.position, to.position)}; + if (span < 0.1) + { + return false; + } + road_node::state road; + // the axis is the way from the first road to the second, which leaves and joins them the way they go + road.axis.points[segment_data::point::start] = from.position; + road.axis.points[segment_data::point::control1] = glm::dvec3{0.0} - flat(from.direction) * (0.39 * span); + road.axis.points[segment_data::point::control2] = glm::dvec3{0.0} - flat(to.direction) * (0.39 * span); + road.axis.points[segment_data::point::end] = to.position; + // at the first road the lanes leading into the junction go the way the axis does, at the second one it's the lanes leading out of it + road.lanewidth = from.width; + road.velocity = m_state.velocity; + road.lanes_between(from.incoming, from.outgoing, to.outgoing, to.incoming); + road.taper = {1.f, from.width > 0.f ? to.width / from.width : 1.f}; + road.median.type = m_state.median; + road.median.material = m_state.medianmaterial; + road.median.width = {from.median, to.median}; + road.surface = m_state.surface; + road.texturelength = m_state.texturelength; + road.markings = m_state.markings; + road.sides = {m_state.side, m_state.side}; + road.kerbs = {m_state.kerbs, m_state.kerbs}; + road.kerbwidth = m_state.kerbwidth; + road.kerbmaterial = m_state.kerbmaterial; + road.kerbheight = m_state.kerbheight; + road.slope = m_state.slope; + // the bank of the corner which starts at the first road runs along the right edge. the one which starts at the second road + // runs along the left edge, the other way + road.banks[1] = from.bank; + road.banks[0] = to.bank; + std::swap(road.banks[0].width[0], road.banks[0].width[1]); + std::swap(road.banks[0].drop[0], road.banks[0].drop[1]); + road.bankmaterial = m_state.bankmaterial; + road.friction = m_state.friction; + road.sounddistance = m_state.sounddistance; + road.quality = m_state.quality; + road.environment = m_state.environment; + road.normalize(); + Road = road; + return true; +} + // makes paths leading through the junction, between the lanes of the roads attached to it at the moment void junction_node::create_links() { @@ -2016,7 +2637,7 @@ void junction_node::create_links() lanes[arm].outgoing.assign(static_cast(arms[arm].outgoing), nullptr); for (auto const *road : simulation::Roads.sequence()) { - if (road == nullptr || road->m_editorremoved || road->tracks().size() != road->definition().lanes.size()) + if (road == nullptr || road->m_editorremoved) { continue; } @@ -2031,18 +2652,21 @@ void junction_node::create_links() // the lanes heading for this end of the road lead into the junction, the others lead out of it for (int lane = 1; lane <= arms[arm].incoming; ++lane) { + // the road may have other lanes at its other end; it's the ones it has here which count auto const index{layout.lane_index((atend ? "f" : "b") + std::to_string(lane))}; - if (index >= 0 && road->tracks()[index] != nullptr && glm::distance(glm::dvec3{road->tracks()[index]->CurrentSegment()->FastGetPoint_1()}, m_state.lane_point(arm, true, lane)) < junction_fit) + auto *track{index >= 0 ? road->lane_track(static_cast(index), atend ? 1 : 0) : nullptr}; + if (track != nullptr && glm::distance(glm::dvec3{track->CurrentSegment()->FastGetPoint_1()}, m_state.lane_point(arm, true, lane)) < junction_fit) { - lanes[arm].incoming[lane - 1] = road->tracks()[index]; + lanes[arm].incoming[lane - 1] = track; } } for (int lane = 1; lane <= arms[arm].outgoing; ++lane) { auto const index{layout.lane_index((atend ? "b" : "f") + std::to_string(lane))}; - if (index >= 0 && road->tracks()[index] != nullptr && glm::distance(glm::dvec3{road->tracks()[index]->CurrentSegment()->FastGetPoint_0()}, m_state.lane_point(arm, false, lane)) < junction_fit) + auto *track{index >= 0 ? road->lane_track(static_cast(index), atend ? 1 : 0) : nullptr}; + if (track != nullptr && glm::distance(glm::dvec3{track->CurrentSegment()->FastGetPoint_0()}, m_state.lane_point(arm, false, lane)) < junction_fit) { - lanes[arm].outgoing[lane - 1] = road->tracks()[index]; + lanes[arm].outgoing[lane - 1] = track; } } } @@ -2339,15 +2963,39 @@ void junction_node::create_gates() auto &gate{m_gates[index]}; // lanes the vehicles heading for the junction are on: the one leading in, and the same lane of the pieces before it gate.approach.emplace_back(gate.entry); + gate.wishes.emplace_back(0); auto reach{glm::distance(glm::dvec3{gate.entry->CurrentSegment()->FastGetPoint_0()}, glm::dvec3{gate.entry->CurrentSegment()->FastGetPoint_1()})}; for (auto *lane{gate.entry}; reach < gate_reach && gate.approach.size() < 16;) { auto *previous{lane->trPrev}; - if (previous == nullptr || previous->trNext != lane || dynamic_cast(previous->m_road) == nullptr || previous->m_paths.size() != 1) + // lanes a road loses join the one which is left: the vehicles on the paths leading them here are coming this way too + if (auto const *owner{dynamic_cast(lane->m_road)}; owner != nullptr) + { + for (auto *link : owner->links()) + { + if (link != previous && link->trNext == lane && link->m_paths.size() == 1) + { + gate.approach.emplace_back(link); + gate.wishes.emplace_back(0); + } + } + } + auto wish{0}; + if (previous != nullptr && previous->m_paths.size() > 1 && dynamic_cast(previous->m_road) != nullptr) + { + // a path on which a road forks into more lanes: of the vehicles on it the ones which picked the way leading here count + wish = fork_code(*previous, lane); + if (wish == 0) + { + break; + } + } + else if (previous == nullptr || previous->trNext != lane || dynamic_cast(previous->m_road) == nullptr || previous->m_paths.size() != 1) { break; } gate.approach.emplace_back(previous); + gate.wishes.emplace_back(wish); reach += glm::distance(glm::dvec3{previous->CurrentSegment()->FastGetPoint_0()}, glm::dvec3{previous->CurrentSegment()->FastGetPoint_1()}); lane = previous; } @@ -2457,14 +3105,25 @@ void junction_node::update(double const Deltatime) { TDynamicObject *first{nullptr}; auto nearest{std::numeric_limits::max()}; - for (auto const *lane : gate.approach) + for (std::size_t idx = 0; idx < gate.approach.size(); ++idx) { + auto *lane{gate.approach[idx]}; + auto const wish{idx < gate.wishes.size() ? gate.wishes[idx] : 0}; for (auto *vehicle : lane->Dynamics) { if (vehicle == nullptr) { continue; } + if (wish != 0) + { + // on a fork: the ones which picked another lane aren't coming this way + auto const picked{vehicle->RouteWish(lane)}; + if (picked != 0 && picked != wish) + { + continue; + } + } auto const position{vehicle->GetPosition()}; auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{gate.line.x, gate.line.z})}; if (distance < nearest) @@ -3375,7 +4034,13 @@ TTrack *road_node::create_track(std::string const &Name, segment_data const &Pat } text << "endtrack"; - cParser parser(text.str(), cParser::buffer_TEXT); + return create_track(Name, text.str()); +} + +// creates a path for the vehicles from its definition and registers it with the simulation +TTrack *road_node::create_track(std::string const &Name, std::string const &Definition) +{ + cParser parser(Definition, cParser::buffer_TEXT); scene::node_data nodedata; nodedata.range_max = -1.0; nodedata.name = Name; @@ -3435,7 +4100,12 @@ void road_node::export_as_text_(std::ostream &Output) const << road.axis.radius << ' '; Output.precision(precision); // lanes - Output << "lanes " << road.forward << ' ' << road.backward << ' ' << "width " << road.lanewidth << ' '; + Output << "lanes " << road.lanes_at(0, true) << ' ' << road.lanes_at(0, false) << ' '; + if (road.changing()) + { + Output << "lanesto " << road.lanes_at(1, true) << ' ' << road.lanes_at(1, false) << ' '; + } + Output << "width " << road.lanewidth << ' '; if (road.taper[0] != 1.f || road.taper[1] != 1.f) { Output << "taper " << road.taper[0] << ' ' << road.taper[1] << ' '; @@ -3517,6 +4187,19 @@ void road_node::export_as_text_(std::ostream &Output) const } // legacy style initialization, to be performed when the tracks are already joined +void road_table::InitLanes() +{ + // the pieces which gain or lose lanes lead the ones they have at one end to the ones they have at the other + for (auto *road : m_items) + { + if (road != nullptr) + { + road->create_links(); + } + } +} + +// legacy style initialization, to be performed when the junctions are done void road_table::InitRoads() { for (auto *road : m_items) diff --git a/world/Road.h b/world/Road.h index de83bf4d..930f3595 100644 --- a/world/Road.h +++ b/world/Road.h @@ -58,7 +58,8 @@ class owned_shapes // properties, past the ones of the lanes and the look: median , // medianround , // kerbs , bank , -// bankmaterial , weight , roundabout +// bankmaterial , weight , roundabout, +// lanesto : lanes the piece has at the end of its axis, where these aren't the ones it starts with class road_node : public scene::basic_node { @@ -151,9 +152,23 @@ class road_node : public scene::basic_node std::array banks; // left, right std::string bankmaterial{"none"}; // what a bank is covered with, other than one of a shoulder, which goes with the shoulder bool roundabout{false}; // the piece is a part of a roundabout: traffic on it goes ahead of the traffic joining it + // lanes the piece is short of at an end of its axis: [end][0: the ones going along the axis, 1: the ones going against it]. + // these are the outermost lanes of the direction, which the road gains or loses along the piece; + // forward and backward say how many lanes there are where the piece has the most of them + std::array, 2> missing{}; // brings the content to a usable form: values within their limits, an entry for each lane and for each pair of neighbours void normalize(); + // true if the piece has other lanes at its end than at its start + bool changing() const; + // sets the lanes the piece has at the start and at the end of its axis + void lanes_between(int const Startforward, int const Startbackward, int const Endforward, int const Endbackward); + // number of lanes at an end of the axis (0: the start, 1: the end): of the ones going along it, or of the ones going against it + int lanes_at(int const End, bool const Along) const; + // true if the piece has the lane at specified end of the axis + bool lane_at(std::size_t const Lane, int const End) const; + // the same road with the lanes it has at specified end of the axis all the way + state section(int const End) const; // identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet std::string lane_id(std::size_t const Lane) const; // number of a lane in the left to right order, -1 if there's no lane with such identifier @@ -162,8 +177,17 @@ class road_node : public scene::basic_node int default_change(std::size_t const Boundary) const; // distance of the middle of a lane from the axis, positive to the left when facing along the axis double lane_offset(std::size_t const Lane) const; + // the same at an end of the axis, where the road is centred on the axis with the lanes it has there. + // a lane the road is short of at that end is where the lane it comes out of, or goes into, is + double lane_offset(std::size_t const Lane, int const End) const; + // distance of the middle of a lane from the axis at specified part of the length of the axis, with the taper and what's between the directions counted in + double lane_position(std::size_t const Lane, double const Fraction) const; // combined width of the lanes, before the taper double width() const; + // the same for the lanes the piece has at an end of the axis + double width(int const End) const; + // combined width of the lanes at specified part of the length of the axis, with the taper counted in + double breadth(double const Fraction) const; // what the widths of the lanes are multiplied by at specified value of the curve parameter double scale(double const T) const; // true if the two directions of the road are kept apart anywhere along it @@ -172,10 +196,20 @@ class road_node : public scene::basic_node double median_width(double const T) const; // distance from the axis of the line the two directions meet at, on a road which doesn't keep them apart double divide() const; + // the same for the lanes the piece has at an end of the axis + double divide(int const End) const; + // the same at specified part of the length of the axis, with the taper counted in + double middle(double const Fraction) const; // width of the whole roadway at specified value of the curve parameter: the lanes and what's between the directions double span(double const T) const; // shape of the middle of a lane, laid out in the direction of travel segment_data lane_path(std::size_t const Lane) const; + // shape of the way from the middle of a lane to the middle of another over a part of the axis, laid out in the direction of travel. + // From, To: values of the curve parameter of the axis, in the order of the axis + segment_data lane_path(std::size_t const Lane, std::size_t const Tolane, double const From, double const To) const; + // part of the axis, as values of its curve parameter, over which a piece which gains or loses lanes leads the lanes it has + // at one end to the lanes it has at the other. before it and past it there's a short path for each lane the piece has at that end + std::array change_range() const; // length of the axis double length() const; // position on the axis and direction of the axis, for specified value of the curve parameter @@ -196,17 +230,36 @@ class road_node : public scene::basic_node void define(state const &State); // creates paths of the lanes and hands them over to the simulation void create_lanes(); + // makes the paths which lead the lanes the piece has at one end to the lanes it has at the other, where these differ. + // to be called once the lanes are joined with the lanes of the neighbours + void create_links(); // links lanes of opposite directions at an end of the road which leads nowhere, so the vehicles can turn back void close_ends(); // gives up the paths of the lanes, or the ones made to close the loose ends. the paths belong to the path table, // the caller is expected to take them out of use std::vector release_lanes(); std::vector release_turns(); - // paths of the lanes, in the order of the lanes, and the ones made to close the loose ends + // paths of the lanes, and the ones made to close the loose ends. the former go in the order of the lanes; + // on a piece which gains or loses lanes these are the short paths left of each such lane at the ends of the piece std::vector const &tracks() const { return m_tracks; } + // path of a lane at an end of the axis (0: the start, 1: the end); nullptr if the piece doesn't have the lane there + TTrack *lane_track(std::size_t const Lane, int const End) const + { + return Lane < m_lanetracks[End].size() ? m_lanetracks[End][Lane] : nullptr; + } + // paths leading from the lanes at one end to the lanes at the other, on a piece which gains or loses lanes + std::vector const &links() const + { + return m_links; + } + // shapes of the ways these make, for display + std::vector const &ways() const + { + return m_ways; + } std::vector const &turns() const { return m_turns; @@ -243,9 +296,14 @@ class road_node : public scene::basic_node void export_as_text_(std::ostream &Output) const override; // creates a path for the vehicles and registers it with the simulation TTrack *create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity); + // the same for a path put together by the caller, the way the scenery defines one + TTrack *create_track(std::string const &Name, std::string const &Definition); // members state m_state; std::vector m_tracks; + std::array, 2> m_lanetracks; // path of each lane at the start and at the end of the axis + std::vector m_links; + std::vector m_ways; std::vector m_turns; bool m_merged{false}; owned_shapes m_shapes; // geometry put in the scene by show() @@ -263,7 +321,8 @@ class road_node : public scene::basic_node // armmedian , kerbs , // cornerbank (at that road, then at the next one), bankmaterial , // crosswalk , median -// a junction of two roads is drawn as a stretch of road, on which the lanes of one are led to the lanes of the other +// a junction of two roads is drawn as a stretch of road, on which the lanes of one are led to the lanes of the other. +// that's what a road which changed its lanes used to take; such a junction found in a scenery is loaded as a piece of road instead, see as_road() // the arms don't have to be level with each other: the surface is spanned between their ends and the centre. // the junction also tells the vehicles when to wait: the ones which have to give way wait for the ones with the right of way, // and where neither has it, for the ones coming from their right, or from the opposite side when turning left across their way @@ -324,6 +383,9 @@ class junction_node : public scene::basic_node TTrack *link{nullptr}; // path, or crossroads of paths, leading on from it std::vector ways; std::vector approach; // lanes the vehicles heading for the junction are looked for on, nearest first + // for each of these: the way a vehicle on it has to have picked to be heading for the junction; 0 if there's no choice there. + // it's set for a path on which a road forks into more lanes + std::vector wishes; glm::dvec3 line{0.0}; // where the lane ends road_order *order{nullptr}; // what's going on at the moment @@ -385,6 +447,9 @@ class junction_node : public scene::basic_node return m_state; } void define(state const &State); + // a junction of two roads is a stretch where a road changes its lanes, which a road can do by itself. + // Road: receives the piece of road to take the place of the junction. returns: false if the junction isn't of that kind + bool as_road(road_node::state &Road) const; // makes paths leading through the junction, between the lanes of the roads attached to it at the moment void create_links(); // gives up the paths leading through the junction; the caller is expected to take them out of use @@ -452,7 +517,10 @@ class road_table : public basic_table { public: - // legacy style initialization, to be performed when the tracks are already joined + // legacy style initialization, to be performed when the tracks are already joined: ties up the lanes of the pieces + // which gain or lose them. the junctions go by these, so it comes ahead of their initialization + void InitLanes(); + // the same, to be performed once the junctions are done: closes the ends of the roads which lead nowhere void InitRoads(); // generates geometry of the roads and puts it in the scene void create_geometry(scene::scratch_data &Scratchpad); diff --git a/world/RoadPoint.cpp b/world/RoadPoint.cpp index 3663a7d3..bc4cea25 100644 --- a/world/RoadPoint.cpp +++ b/world/RoadPoint.cpp @@ -545,6 +545,29 @@ void roadpoint_node::bind() m_lanes.emplace_back(lane); } } + // and from the paths which lead the lanes of a piece which gains or loses them from one of its ends to the other + for (auto *link : road->links()) + { + if (link == nullptr) + { + continue; + } + for (auto const &way : link->m_paths) + { + if (out_of_reach(way, m_state.position, m_state.radius)) + { + continue; + } + path_curve const curve{way}; + 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(link); + break; + } + } + } } break; } @@ -559,10 +582,16 @@ void roadpoint_node::bind_crossing() auto const *crossed{road_under(m_state.position, where)}; if (crossed != nullptr) { - auto const &lanes{crossed->tracks()}; - for (std::size_t idx = 0; idx < lanes.size(); ++idx) + auto const &layout{crossed->definition()}; + if (layout.changing()) { - auto *lane{lanes[idx]}; + // NOTE: on such a piece a lane is a string of paths, and may fork; the vehicles are stopped ahead of all that + ErrorLog("Bad road point: crossing \"" + m_name + "\" lies on a piece of road which gains or loses lanes, the vehicles are stopped before that piece"); + } + for (std::size_t idx = 0; idx < layout.lanes.size(); ++idx) + { + // the path the traffic of the lane gets on the piece by; on a piece which keeps its lanes it's the lane itself + auto *lane{crossed->lane_track(idx, idx >= static_cast(layout.backward) ? 0 : 1)}; if (lane == nullptr || lane->m_paths.empty()) { continue;