17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 16:14:52 +02:00
Files
maszyna/editor/editorRoad.cpp
maj00r ff1710d9ea feat(editor): labels for a layman, light at the start; whole sceneries fitted to the terrain model
Labels:
- every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the
  sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track,
  grade), sides told from directions, degrees and per mille as signs, messages built from one format string
- plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header
- lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped
- roads: the notes and errors naming a road translated; the build options and the layout fold the texture length,
  kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the
  lanes start folded

Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once:
- fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone
  past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way
  is one point of intersection, a curve with no room is as sharp as the room lets it be
- the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what
  the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run
- apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into
  pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the
  ends of the route are brought to its height
- Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
2026-10-19 15:27:50 +02:00

2086 lines
78 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "editor/editorRoad.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorTrack.hpp"
#include "world/Track.h"
#include "scene/scene.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include "utilities/translation.h"
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <set>
namespace
{
double const kSamePoint{0.25}; // ends of two pieces this close are taken for joined
double const kPieceLength{50.0}; // longest piece the planner makes
double const kPieceTurn{glm::radians(30.0)}; // largest turn of a single piece
double const kShortestRoad{2.0};
double const kTightestRadius{6.0};
double const kCornerRoom{4.0}; // length of an arm of a junction past the point where its edge meets the edge of the next one, taken by the rounded corner
double const kSharpestBranch{glm::radians(30.0)}; // smallest angle between two roads leaving a junction
double const kWidestCorner{glm::radians(120.0)}; // arms further apart than this don't have to make room for each other
double const kTransitionLength{30.0}; // length of the road taken by a change of its lanes
void add_unique(std::vector<road_node *> &Roads, road_node *Road)
{
if (Road != nullptr && std::find(Roads.begin(), Roads.end(), Road) == Roads.end())
Roads.emplace_back(Road);
}
glm::dvec3 axis_end(road_node::state const &State, bool const Atend)
{
return State.axis.points[Atend ? segment_data::point::end : segment_data::point::start];
}
// roads other than the specified one, with an end at specified point
std::vector<road_node *> roads_at(glm::dvec3 const &Point, road_node const *Exclude)
{
std::vector<road_node *> result;
for (auto *road : simulation::Roads.sequence())
{
if (road == nullptr || road == Exclude || road->m_editorremoved)
continue;
for (auto const atend : {false, true})
{
if (glm::distance(axis_end(road->definition(), atend), Point) < kSamePoint)
{
result.emplace_back(road);
break;
}
}
}
return result;
}
// junctions with an arm at specified point
std::vector<junction_node *> junctions_at(glm::dvec3 const &Point)
{
std::vector<junction_node *> result;
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
for (auto const &arm : junction->definition().arms)
{
if (glm::distance(arm.position, Point) < kSamePoint)
{
result.emplace_back(junction);
break;
}
}
}
return result;
}
void add_unique(std::vector<junction_node *> &Junctions, junction_node *Junction)
{
if (Junction != nullptr && std::find(Junctions.begin(), Junctions.end(), Junction) == Junctions.end())
Junctions.emplace_back(Junction);
}
// part of a path between two values of its curve parameter
segment_data subpath(segment_data const &Path, double const From, double const To)
{
segment_data part{Path};
auto const &p0{Path.points[segment_data::point::start]};
auto const &p3{Path.points[segment_data::point::end]};
part.rolls = {static_cast<float>(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * From), static_cast<float>(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * To)};
if (Path.points[segment_data::point::control1] == glm::dvec3{0.0} && Path.points[segment_data::point::control2] == glm::dvec3{0.0})
{
part.points[segment_data::point::start] = glm::mix(p0, p3, From);
part.points[segment_data::point::end] = glm::mix(p0, p3, To);
return part;
}
auto const p1{p0 + Path.points[segment_data::point::control1]};
auto const p2{p3 + Path.points[segment_data::point::control2]};
auto const point = [&](double const T) {
auto const u{1.0 - T};
return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3;
};
auto const derivative = [&](double const T) {
auto const u{1.0 - T};
return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2);
};
// a part of a cubic curve is a cubic curve, with the tangents scaled to the part of the parameter range it takes
part.points[segment_data::point::start] = point(From);
part.points[segment_data::point::end] = point(To);
part.points[segment_data::point::control1] = derivative(From) * ((To - From) / 3.0);
part.points[segment_data::point::control2] = derivative(To) * (-(To - From) / 3.0);
return part;
}
// true if there's a vehicle on any of the paths turning the traffic back at the loose ends of the road
bool turning(road_node const &Road)
{
for (auto const *turn : Road.turns())
if (turn != nullptr && false == turn->Dynamics.empty())
return true;
return false;
}
// definition of a part of a piece, between two values of the curve parameter of its axis
road_node::state part(road_node::state const &State, double const From, double const To)
{
auto result{State};
result.axis = subpath(State.axis, From, To);
result.taper = {static_cast<float>(State.scale(From)), static_cast<float>(State.scale(To))};
if (State.divided())
result.median.width = {static_cast<float>(State.median_width(From)), static_cast<float>(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]};
result.banks[side].width = {glm::mix(bank.width[0], bank.width[1], static_cast<float>(From)), glm::mix(bank.width[0], bank.width[1], static_cast<float>(To))};
result.banks[side].drop = {glm::mix(bank.drop[0], bank.drop[1], static_cast<float>(From)), glm::mix(bank.drop[0], bank.drop[1], static_cast<float>(To))};
}
return result;
}
// points of an end of a road something can be joined to: the end of its axis, and where it keeps its two directions apart,
// the middle of each of its roadways
std::vector<glm::dvec3> end_points(road_node::state const &State, bool const Atend)
{
std::vector<glm::dvec3> result{axis_end(State, Atend)};
auto const t{Atend ? 1.0 : 0.0};
auto const gap{State.median_width(t)};
if (gap <= 0.0)
return result;
auto const tangent{State.tangent(t)};
glm::dvec3 left{tangent.z, 0.0, -tangent.x};
if (glm::length(left) < 1e-9)
return result;
left = glm::normalize(left);
auto const halfwidth{0.5 * State.width(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));
return result;
}
// points of a path as a bezier curve, and a point of that curve. unlike the methods of a road definition
// these don't measure the whole axis for each call, which matters for what's done every frame
std::array<glm::dvec3, 4> curve_points(segment_data const &Path)
{
auto const &p0{Path.points[segment_data::point::start]};
auto const &p3{Path.points[segment_data::point::end]};
if (Path.points[segment_data::point::control1] == glm::dvec3{0.0} && Path.points[segment_data::point::control2] == glm::dvec3{0.0})
return {p0, p0 + (p3 - p0) / 3.0, p3 - (p3 - p0) / 3.0, p3};
return {p0, p0 + Path.points[segment_data::point::control1], p3 + Path.points[segment_data::point::control2], p3};
}
glm::dvec3 curve_point(std::array<glm::dvec3, 4> const &Points, double const T)
{
auto const u{1.0 - T};
return u * u * u * Points[0] + 3.0 * u * u * T * Points[1] + 3.0 * u * T * T * Points[2] + T * T * T * Points[3];
}
int const kStations{64};
// distances along a path for evenly spaced values of its curve parameter
std::vector<double> stations(road_node::state const &State)
{
std::vector<double> result(kStations + 1, 0.0);
auto const points{curve_points(State.axis)};
auto previous{points[0]};
for (int i = 1; i <= kStations; ++i)
{
auto const current{curve_point(points, static_cast<double>(i) / kStations)};
result[i] = result[i - 1] + glm::distance(previous, current);
previous = current;
}
return result;
}
// adds to a record what a piece or a junction is like, ahead of changing it. the first entry stays, as it's what taking the lot back has to end with
void remember(editor_road::record &Record, road_node *Road)
{
if (std::none_of(Record.roads.begin(), Record.roads.end(), [Road](std::pair<road_node *, road_node::state> const &Entry) { return Entry.first == Road; }))
Record.roads.emplace_back(Road, Road->definition());
}
void remember(editor_road::record &Record, junction_node *Junction)
{
if (std::none_of(Record.junctions.begin(), Record.junctions.end(), [Junction](std::pair<junction_node *, junction_node::state> const &Entry) { return Entry.first == Junction; }))
Record.junctions.emplace_back(Junction, Junction->definition());
}
// distance along a path for specified value of its curve parameter, and the other way around
double station_at(std::vector<double> const &Stations, double const T)
{
auto const position{std::clamp(T, 0.0, 1.0) * kStations};
auto const index{std::min(kStations - 1, static_cast<int>(position))};
return glm::mix(Stations[index], Stations[index + 1], position - index);
}
double parameter_at(std::vector<double> const &Stations, double const Station)
{
if (Station <= 0.0)
return 0.0;
if (Station >= Stations.back())
return 1.0;
auto const index{static_cast<int>(std::distance(Stations.begin(), std::upper_bound(Stations.begin(), Stations.end(), Station)))};
auto const span{Stations[index] - Stations[index - 1]};
return (index - 1 + (span > 0.0 ? (Station - Stations[index - 1]) / span : 0.0)) / kStations;
}
// direction seen from above
glm::dvec2 planar(glm::dvec3 const &Direction)
{
glm::dvec2 const result{Direction.x, Direction.z};
return glm::length(result) > 1e-9 ? glm::normalize(result) : glm::dvec2{0.0, 1.0};
}
// where a junction made in the middle of a road ends on one of its sides
struct road_cut
{
road_node *road{nullptr};
double t{0.0};
bool keepstart{false}; // what stays of the piece: from its start up to the cut, or from the cut to its end
};
// follows a road from a point of a piece for specified distance, to find where a junction made at that point ends.
// Consumed: receives the pieces past the first one which lie within the distance whole
bool follow(road_node &Road, double const T, bool const Forward, double const Distance, road_cut &Cut, std::vector<road_node *> &Consumed, std::string &Error)
{
auto *piece{&Road};
auto from{T};
auto forward{Forward};
auto remaining{Distance};
for (int guard = 0; guard < 64; ++guard)
{
auto const &state{piece->definition()};
auto const lengths{stations(state)};
auto const here{station_at(lengths, from)};
auto const available{forward ? lengths.back() - here : here};
if (remaining <= 0.0 || available >= remaining + kShortestRoad)
{
// the cut is put here; with nothing left to go the piece is kept whole
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
remaining = std::max(0.0, remaining - available);
if (piece != &Road)
Consumed.emplace_back(piece);
auto const end{axis_end(state, forward)};
if (false == junctions_at(end).empty())
{
Error = "Too close to a junction";
return false;
}
auto const next{roads_at(end, piece)};
if (next.size() != 1)
{
Error = (next.empty() ? "Too close to the end of the road" : "Too close to a place where roads meet");
return false;
}
if (next.front() == &Road || std::find(Consumed.begin(), Consumed.end(), next.front()) != Consumed.end())
{
Error = "The road is too short for a junction";
return false;
}
piece = next.front();
forward = (glm::distance(axis_end(piece->definition(), false), end) < kSamePoint);
from = (forward ? 0.0 : 1.0);
}
Error = "The road is cut into too many pieces here";
return false;
}
// arm of a junction for the part of a road left on one side of it. returns: false if the lanes there differ in width
bool cut_arm(road_cut const &Cut, junction_node::arm_data &Arm)
{
auto const &state{Cut.road->definition()};
Arm.position = state.point(Cut.t);
// the way out of the junction is the way the kept part of the piece goes from the cut
Arm.direction = planar(state.tangent(Cut.t)) * (Cut.keepstart ? -1.0 : 1.0);
// lanes heading for the cut lead into the junction. 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<float>(state.median_width(Cut.t));
return editor_road::lane_width(state, Cut.t, Arm.width);
}
// how far from the centre of a junction the edges of two of its roads meet, measured along the first one.
// Halfwidth, Otherhalfwidth: of the two roads, Angle: between the ways they leave
double edge_reach(double const Halfwidth, double const Otherhalfwidth, double const Angle)
{
return (Otherhalfwidth + Halfwidth * std::cos(Angle)) / std::sin(Angle);
}
// the look of a junction goes after the road it's made on
void dress(junction_node::state &Junction, road_node::state const &Road)
{
Junction.surface = Road.surface;
Junction.texturelength = Road.texturelength;
Junction.markings = Road.markings;
// one kind of side goes around the whole junction; the right side of the road is the one facing the corner a road leaves by
Junction.side = (Road.sides[1].type != road_node::side_type::none ? Road.sides[1] : Road.sides[0]);
Junction.kerbheight = Road.kerbheight;
Junction.kerbs = (Road.kerbs[0] || Road.kerbs[1]);
Junction.kerbwidth = Road.kerbwidth;
Junction.kerbmaterial = Road.kerbmaterial;
Junction.bankmaterial = Road.bankmaterial;
Junction.median = Road.median.type;
Junction.medianmaterial = Road.median.material;
Junction.slope = Road.slope;
Junction.friction = Road.friction;
Junction.sounddistance = Road.sounddistance;
Junction.quality = Road.quality;
Junction.environment = Road.environment;
}
void retire(std::vector<TTrack *> const &Tracks)
{
for (auto *track : Tracks)
if (track != nullptr)
editor_track::retire(*track);
}
// value of the curve parameter for the point of a curve nearest to specified point, seen from above
double nearest_on(std::array<glm::dvec3, 4> const &Points, glm::dvec3 const &Point)
{
auto const distance = [&](double const T) {
auto const position{curve_point(Points, T)};
return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z});
};
double best{0.0};
for (int i = 1; i <= 64; ++i)
if (distance(i / 64.0) < distance(best))
best = i / 64.0;
double step{1.0 / 64.0};
for (int i = 0; i < 24; ++i)
{
step *= 0.5;
if (best - step >= 0.0 && distance(best - step) < distance(best))
best -= step;
else if (best + step <= 1.0 && distance(best + step) < distance(best))
best += step;
}
return best;
}
double planar_distance(glm::dvec3 const &Left, glm::dvec3 const &Right)
{
return glm::distance(glm::dvec2{Left.x, Left.z}, glm::dvec2{Right.x, Right.z});
}
// true if two lines cross when seen from above. Along, Otheralong: receive how far along each of them the crossing is, 0 to 1
bool lines_cross(glm::dvec3 const &Start, glm::dvec3 const &End, glm::dvec3 const &Otherstart, glm::dvec3 const &Otherend, double &Along, double &Otheralong)
{
glm::dvec2 const line{End.x - Start.x, End.z - Start.z};
glm::dvec2 const other{Otherend.x - Otherstart.x, Otherend.z - Otherstart.z};
auto const denominator{line.x * other.y - line.y * other.x};
if (std::abs(denominator) < 1e-12)
return false;
glm::dvec2 const offset{Otherstart.x - Start.x, Otherstart.z - Start.z};
Along = (offset.x * other.y - offset.y * other.x) / denominator;
Otheralong = (offset.x * line.y - offset.y * line.x) / denominator;
return Along >= 0.0 && Along <= 1.0 && Otheralong >= 0.0 && Otheralong <= 1.0;
}
// the same piece of a road laid out from its other end: what it looks like and where its lanes lead stays as it was
road_node::state flipped(road_node::state const &State)
{
auto result{State};
std::swap(result.axis.points[segment_data::point::start], result.axis.points[segment_data::point::end]);
std::swap(result.axis.points[segment_data::point::control1], result.axis.points[segment_data::point::control2]);
result.axis.rolls = {-State.axis.rolls[1], -State.axis.rolls[0]};
std::swap(result.forward, result.backward);
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());
for (auto &change : result.changes)
change = ((change & road_node::change_toright) != 0 ? road_node::change_toleft : 0) | ((change & road_node::change_toleft) != 0 ? road_node::change_toright : 0);
std::swap(result.sides[0], result.sides[1]);
std::swap(result.taper[0], result.taper[1]);
std::swap(result.median.width[0], result.median.width[1]);
std::swap(result.median.round[0], result.median.round[1]);
std::swap(result.kerbs[0], result.kerbs[1]);
std::swap(result.banks[0], result.banks[1]);
for (auto &bank : result.banks)
{
std::swap(bank.width[0], bank.width[1]);
std::swap(bank.drop[0], bank.drop[1]);
}
return result;
}
// smallest radius of the bend made by a bezier curve, seen from above; 0 if the curve is practically straight
double tightest_radius(segment_data const &Path)
{
auto const &p0{Path.points[segment_data::point::start]};
auto const &p3{Path.points[segment_data::point::end]};
auto const p1{p0 + Path.points[segment_data::point::control1]};
auto const p2{p3 + Path.points[segment_data::point::control2]};
double curvature{0.0};
for (int i = 0; i <= 16; ++i)
{
auto const t{i / 16.0};
auto const u{1.0 - t};
auto const first{3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2)};
auto const second{6.0 * u * (p2 - 2.0 * p1 + p0) + 6.0 * t * (p3 - 2.0 * p2 + p1)};
auto const speedsquared{first.x * first.x + first.z * first.z};
if (speedsquared < 1e-9)
continue;
curvature = std::max(curvature, std::abs(second.x * first.z - second.z * first.x) / (speedsquared * std::sqrt(speedsquared)));
}
return curvature > 1e-5 ? 1.0 / curvature : 0.0;
}
}
bool editor_road::available(std::string *Reason)
{
if (Global.NvRenderer)
{
if (Reason != nullptr)
*Reason = "Geometry of the roads can't be rebuilt with the experimental renderer";
return false;
}
if (simulation::Region == nullptr)
{
if (Reason != nullptr)
*Reason = "No scenery is loaded";
return false;
}
return true;
}
bool editor_road::can_edit(road_node const &Road, std::string *Reason)
{
std::string reason;
if (false == available(&reason))
{
// reason was filled by the call
}
else if (Road.merged())
{
reason = "the scenery wasn't opened for editing (-edit), geometry of the roads it came with can't be taken apart";
}
else if (false == scene::Layers.editable(&Road, &reason))
{
// reason was filled by the call
}
else if (Road.occupied())
{
reason = "there are vehicles on it";
}
else
{
// the paths made to turn the vehicles back at the loose ends of the neighbours go away too
for (auto const *neighbour : neighbours(Road))
for (auto const *turn : neighbour->turns())
if (turn != nullptr && false == turn->Dynamics.empty())
reason = "there are vehicles turning back next to it";
}
if (reason.empty())
{
// so do the ways through the junctions it leads to
for (auto const atend : {false, true})
for (auto const *junction : junctions_at(axis_end(Road.definition(), atend)))
if (junction->occupied())
reason = "there are vehicles on the junction it leads to";
}
if (false == reason.empty() && Reason != nullptr)
*Reason = reason;
return reason.empty();
}
bool editor_road::can_edit(junction_node const &Junction, std::string *Reason)
{
std::string reason;
if (false == available(&reason))
{
// reason was filled by the call
}
else if (Junction.merged())
{
reason = "the scenery wasn't opened for editing (-edit), geometry of the junctions it came with can't be taken apart";
}
else if (false == scene::Layers.editable(&Junction, &reason))
{
// reason was filled by the call
}
else if (Junction.occupied())
{
reason = "there are vehicles on it";
}
else
{
// the roads attached to it get their loose ends closed anew
for (auto const &arm : Junction.definition().arms)
for (auto const *road : roads_at(arm.position, nullptr))
if (turning(*road))
reason = "there are vehicles turning back on the roads it leads to";
}
if (false == reason.empty() && Reason != nullptr)
*Reason = reason;
return reason.empty();
}
bool editor_road::can_join(loose_end const &End, std::string *Reason)
{
// what gets attached takes the place of the paths made for the end as it was
std::string reason;
if (End.road != nullptr && turning(*End.road))
reason = "there are vehicles turning back on the road";
if (End.junction != nullptr)
{
if (End.junction->occupied())
reason = "there are vehicles on the junction";
for (auto const &arm : End.junction->definition().arms)
for (auto const *road : roads_at(arm.position, nullptr))
if (turning(*road))
reason = "there are vehicles turning back on the roads the junction leads to";
}
if (false == reason.empty() && Reason != nullptr)
*Reason = reason;
return reason.empty();
}
std::vector<road_node *> editor_road::neighbours(road_node const &Road)
{
std::vector<road_node *> result;
if (Road.m_editorremoved)
return result;
for (auto const atend : {false, true})
{
for (auto *road : roads_at(axis_end(Road.definition(), atend), &Road))
add_unique(result, road);
// a road which keeps its directions apart can have a road joined to each of its roadways, and be one for the road joined
auto const points{end_points(Road.definition(), atend)};
for (auto *road : simulation::Roads.sequence())
{
if (road == nullptr || road == &Road || road->m_editorremoved)
continue;
for (auto const otheratend : {false, true})
{
auto const otherpoints{end_points(road->definition(), otheratend)};
if (points.size() == 1 && otherpoints.size() == 1)
continue;
for (auto const &point : points)
for (auto const &otherpoint : otherpoints)
if (glm::distance(point, otherpoint) < kSamePoint)
add_unique(result, road);
}
}
}
return result;
}
template <class Change_>
void editor_road::refresh(std::vector<road_node *> const &Roads, std::vector<junction_node *> const &Junctions, Change_ Change)
{
// the pieces and junctions next to the changed ones keep their shape, but what they're joined with changes.
// this takes down the paths made for the joints, so they can be made again once the change is done
std::vector<road_node *> affected;
std::vector<junction_node *> crossings;
// level crossings and traffic points hold on to the lanes, which are about to be replaced
simulation::Roadpoints.unbind();
auto const gather = [&]() {
std::vector<road_node *> roads{affected};
std::vector<junction_node *> junctions{crossings};
for (auto *road : Roads)
{
add_unique(roads, road);
if (road->m_editorremoved)
continue;
for (auto *neighbour : neighbours(*road))
add_unique(roads, neighbour);
for (auto const atend : {false, true})
for (auto *junction : junctions_at(axis_end(road->definition(), atend)))
add_unique(junctions, junction);
}
for (auto *junction : Junctions)
add_unique(junctions, junction);
// a junction changes what the roads attached to it do at their ends
for (auto *junction : junctions)
if (false == junction->m_editorremoved)
for (auto const &arm : junction->definition().arms)
for (auto *road : roads_at(arm.position, nullptr))
add_unique(roads, road);
for (auto *road : roads)
{
if (std::find(affected.begin(), affected.end(), road) == affected.end())
{
retire(road->release_turns());
affected.emplace_back(road);
}
}
for (auto *junction : junctions)
{
if (std::find(crossings.begin(), crossings.end(), junction) == crossings.end())
{
retire(junction->release_links());
crossings.emplace_back(junction);
}
}
};
gather();
for (auto *road : Roads)
{
road->hide();
retire(road->release_lanes());
}
for (auto *junction : Junctions)
junction->hide();
Change();
// the change could have moved things next to other ones
gather();
std::vector<TTrack *> lanes;
for (auto *road : Roads)
{
if (road->m_editorremoved)
continue;
road->create_lanes();
road->show();
lanes.insert(lanes.end(), road->tracks().begin(), road->tracks().end());
}
// joins the lanes with the lanes of the neighbours, by the ends they share
editor_track::commit(lanes);
// 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 : Roads)
if (false == road->m_editorremoved)
road->create_links();
for (auto *junction : Junctions)
if (false == junction->m_editorremoved)
junction->show();
// the junctions take the lanes which end at them, what's left loose after that is closed by the roads
for (auto *junction : crossings)
if (false == junction->m_editorremoved)
junction->create_links();
for (auto *road : affected)
if (false == road->m_editorremoved)
road->close_ends();
simulation::Roadpoints.bind();
}
std::vector<road_node *> editor_road::create(std::vector<road_node::state> const &States)
{
static int counter{0};
std::vector<road_node *> created;
if (false == available())
return created;
for (auto const &state : States)
{
scene::node_data data;
data.type = "road";
data.range_max = -1.0;
data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing
// the name is what the lanes are known by to the vehicles put on them, so each piece needs one of its own
do
{
data.name = "road_" + std::to_string(++counter);
} while (simulation::Roads.find(data.name) != nullptr);
auto *road{new road_node(data)};
road->define(state);
road->mark_dirty();
simulation::Roads.insert(road);
scene::Layers.count(road->layer(), scene::layer_item::track);
created.emplace_back(road);
}
refresh(created, {}, []() {});
return created;
}
void editor_road::apply(std::vector<std::pair<road_node *, road_node::state>> const &Changes)
{
std::vector<road_node *> roads;
for (auto const &change : Changes)
add_unique(roads, change.first);
if (roads.empty())
return;
refresh(roads, {}, [&]() {
for (auto const &change : Changes)
{
change.first->define(change.second);
change.first->mark_dirty();
}
});
}
void editor_road::remove(std::vector<road_node *> const &Roads)
{
std::vector<road_node *> roads;
for (auto *road : Roads)
if (road != nullptr && false == road->m_editorremoved)
add_unique(roads, road);
if (roads.empty())
return;
refresh(roads, {}, [&]() {
for (auto *road : roads)
{
road->m_editorremoved = true;
scene::Layers.count(road->layer(), scene::layer_item::track, -1);
}
});
}
void editor_road::revive(std::vector<road_node *> const &Roads)
{
std::vector<road_node *> roads;
for (auto *road : Roads)
if (road != nullptr && road->m_editorremoved)
add_unique(roads, road);
if (roads.empty())
return;
refresh(roads, {}, [&]() {
for (auto *road : roads)
{
road->m_editorremoved = false;
road->mark_dirty();
scene::Layers.count(road->layer(), scene::layer_item::track);
}
});
}
void editor_road::apply(std::vector<std::pair<junction_node *, junction_node::state>> const &Changes)
{
std::vector<junction_node *> junctions;
for (auto const &change : Changes)
add_unique(junctions, change.first);
if (junctions.empty())
return;
refresh({}, junctions, [&]() {
for (auto const &change : Changes)
{
if (change.first == nullptr)
continue;
change.first->define(change.second);
change.first->mark_dirty();
}
});
}
std::vector<junction_node *> editor_road::create(std::vector<junction_node::state> const &States)
{
static int counter{0};
std::vector<junction_node *> created;
if (false == available())
return created;
for (auto const &state : States)
{
scene::node_data data;
data.type = "junction";
data.range_max = -1.0;
data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing
do
{
data.name = "junction_" + std::to_string(++counter);
} while (simulation::Junctions.find(data.name) != nullptr);
auto *junction{new junction_node(data)};
junction->define(state);
junction->mark_dirty();
simulation::Junctions.insert(junction);
scene::Layers.count(junction->layer(), scene::layer_item::track);
created.emplace_back(junction);
}
refresh({}, created, []() {});
return created;
}
void editor_road::remove(std::vector<junction_node *> const &Junctions)
{
std::vector<junction_node *> junctions;
for (auto *junction : Junctions)
if (junction != nullptr && false == junction->m_editorremoved)
add_unique(junctions, junction);
if (junctions.empty())
return;
refresh({}, junctions, [&]() {
for (auto *junction : junctions)
{
junction->m_editorremoved = true;
scene::Layers.count(junction->layer(), scene::layer_item::track, -1);
}
});
}
void editor_road::revive(std::vector<junction_node *> const &Junctions)
{
std::vector<junction_node *> junctions;
for (auto *junction : Junctions)
if (junction != nullptr && junction->m_editorremoved)
add_unique(junctions, junction);
if (junctions.empty())
return;
refresh({}, junctions, [&]() {
for (auto *junction : junctions)
{
junction->m_editorremoved = false;
junction->mark_dirty();
scene::Layers.count(junction->layer(), scene::layer_item::track);
}
});
}
bool editor_road::can_edit(roadpoint_node const &Point, std::string *Reason)
{
std::string reason;
auto editable{true};
if (simulation::Region == nullptr)
{
reason = "No scenery is loaded";
editable = false;
}
else
{
editable = scene::Layers.editable(&Point, &reason);
}
if (false == editable && Reason != nullptr)
*Reason = reason;
return editable;
}
std::vector<roadpoint_node *> editor_road::create(std::vector<roadpoint_node::state> const &States)
{
static int counter{0};
std::vector<roadpoint_node *> created;
if (simulation::Region == nullptr)
return created;
for (auto const &state : States)
{
scene::node_data data;
data.type = roadpoint_node::keyword(state.kind);
data.range_max = -1.0;
data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing
// the vehicles of a spawn point are named after it, so each point needs a name of its own
do
{
data.name = data.type + "_" + std::to_string(++counter);
} while (simulation::Roadpoints.find(data.name) != nullptr);
auto *point{new roadpoint_node(data)};
point->define(state);
point->mark_dirty();
simulation::Roadpoints.insert(point);
scene::Layers.count(point->layer(), scene::layer_item::track);
point->bind();
point->show();
created.emplace_back(point);
}
return created;
}
void editor_road::apply(std::vector<std::pair<roadpoint_node *, roadpoint_node::state>> const &Changes)
{
for (auto const &change : Changes)
{
auto *point{change.first};
if (point == nullptr)
continue;
point->hide();
point->unbind();
point->define(change.second);
point->mark_dirty();
point->bind();
point->show();
}
}
void editor_road::remove(std::vector<roadpoint_node *> const &Points)
{
for (auto *point : Points)
{
if (point == nullptr || point->m_editorremoved)
continue;
point->hide();
// a crossing which goes lets the vehicles it was holding drive on
point->unbind();
point->m_editorremoved = true;
scene::Layers.count(point->layer(), scene::layer_item::track, -1);
}
}
void editor_road::revive(std::vector<roadpoint_node *> const &Points)
{
for (auto *point : Points)
{
if (point == nullptr || false == point->m_editorremoved)
continue;
point->m_editorremoved = false;
point->mark_dirty();
scene::Layers.count(point->layer(), scene::layer_item::track);
point->bind();
point->show();
}
}
roadpoint_node *editor_road::nearest_point(glm::dvec3 const &Point, double const Radius)
{
roadpoint_node *result{nullptr};
auto best{Radius};
for (auto *point : simulation::Roadpoints.sequence())
{
if (point == nullptr || point->m_editorremoved)
continue;
auto const &position{point->definition().position};
auto const distance{planar_distance(position, Point)};
if (distance > best || std::abs(position.y - Point.y) > 3.0)
continue;
best = distance;
result = point;
}
return result;
}
bool editor_road::nearest_lane(glm::dvec3 const &Point, double const Reach, glm::dvec3 &Position, glm::dvec3 &Direction)
{
auto best{Reach};
auto found{false};
for (auto const *road : simulation::Roads.sequence())
{
if (road == nullptr || road->m_editorremoved)
continue;
auto const &state{road->definition()};
auto const axis{curve_points(state.axis)};
// the axis is no longer than the lines joining its points, which is good enough to tell the road is nowhere near
if (glm::distance(road->location(), Point) >
0.5 * (glm::distance(axis[0], axis[1]) + glm::distance(axis[1], axis[2]) + glm::distance(axis[2], axis[3])) + 0.5 * std::max(state.span(0.0), state.span(1.0)) + Reach + 10.0)
continue;
for (auto const *lane : road->tracks())
{
if (lane == nullptr || lane->m_paths.empty())
continue;
auto const points{curve_points(lane->m_paths.front())};
auto const t{nearest_on(points, Point)};
auto const position{curve_point(points, t)};
auto const distance{planar_distance(position, Point)};
// lanes passing over or under the point don't count
if (distance > best || std::abs(position.y - Point.y) > 3.0)
continue;
best = distance;
Position = position;
// the lanes are laid out the way the traffic goes
Direction = curve_point(points, std::min(1.0, t + 0.01)) - curve_point(points, std::max(0.0, t - 0.01));
found = true;
}
}
return found;
}
bool editor_road::rails_across(road_node const &Road, glm::dvec3 const &Near, double const Reach, glm::dvec3 &Middle, double &Halfspan)
{
int const pieces{32}; // lines the axis is taken for
int const railpieces{16}; // and each of the rails
auto const axis{curve_points(Road.definition().axis)};
std::vector<glm::dvec3> line;
for (int i = 0; i <= pieces; ++i)
line.emplace_back(curve_point(axis, static_cast<double>(i) / pieces));
// values of the curve parameter of the axis the rails cross it at
auto first{2.0};
auto last{-1.0};
for (auto const *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0)
continue;
for (auto const &path : track->m_paths)
{
auto const rail{curve_points(path)};
// the path is no longer than the lines joining its points, which is good enough to tell it's nowhere near
if (planar_distance(rail[0], Near) > glm::distance(rail[0], rail[1]) + glm::distance(rail[1], rail[2]) + glm::distance(rail[2], rail[3]) + Reach)
continue;
auto previous{rail[0]};
for (int j = 1; j <= railpieces; ++j)
{
auto const next{curve_point(rail, static_cast<double>(j) / railpieces)};
for (int i = 0; i < pieces; ++i)
{
double along{0.0};
double railalong{0.0};
if (false == lines_cross(line[i], line[i + 1], previous, next, along, railalong))
continue;
auto const point{glm::mix(line[i], line[i + 1], along)};
// rails passing over or under the road don't make a level crossing
if (planar_distance(point, Near) > Reach || std::abs(glm::mix(previous.y, next.y, railalong) - point.y) > 3.0)
continue;
auto const t{(i + along) / pieces};
first = std::min(first, t);
last = std::max(last, t);
}
previous = next;
}
}
}
if (last < first)
return false;
Middle = curve_point(axis, 0.5 * (first + last));
Halfspan = std::max(planar_distance(curve_point(axis, first), Middle), planar_distance(curve_point(axis, last), Middle));
return true;
}
bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string &Error)
{
if (false == junctions_at(Joint).empty())
{
Error = "the point is where a road meets a junction. Delete the junction or the road instead";
return false;
}
auto const ends{ends_at(Joint)};
if (ends.empty())
{
Error = "there's no point of a road there";
return false;
}
if (ends.size() > 2 || (ends.size() == 2 && ends[0].road == ends[1].road))
{
Error = "more than two ends of roads meet at the point";
return false;
}
std::string reason;
for (auto const &end : ends)
{
if (false == can_edit(*end.road, &reason))
{
Error = format(STR_C("\"%s\" can't be changed: %s"), end.road->name().c_str(), STR(reason).c_str());
return false;
}
}
if (ends.size() == 1)
{
// the point a road ends at: the last piece goes
remove(std::vector<road_node *>{ends.front().road});
Record.roads_removed.emplace_back(ends.front().road);
return true;
}
// the piece which leads to the point stays, and takes the place of the two
auto kept{ends[0]};
auto dropped{ends[1]};
if (false == kept.atend && dropped.atend)
std::swap(kept, dropped);
auto before{kept.road->definition()};
auto after{dropped.road->definition()};
if (false == kept.atend)
before = flipped(before);
if (dropped.atend)
after = flipped(after);
if (before.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";
return false;
}
auto const lengths{std::make_pair(before.length(), after.length())};
auto const whole{lengths.first + lengths.second};
if (lengths.first < 0.01 || lengths.second < 0.01 || before.width() <= 0.0)
{
Error = "the pieces at the point are too short";
return false;
}
auto const start{curve_points(before.axis)};
auto const end{curve_points(after.axis)};
auto merged{before};
merged.axis.points[segment_data::point::end] = end[3];
merged.axis.rolls[1] = after.axis.rolls[1];
// the far ends keep the way they go, with their control points moved out to make up for the ones which go with the point.
// a piece cut in two has the control points at the cut as far from it as the parts of the piece each of the two took,
// so going by these brings such a piece back to what it was
auto const reach{std::make_pair(glm::distance(start[2], start[3]), glm::distance(end[0], end[1]))};
auto const share{std::clamp(reach.first + reach.second > 1e-6 ? reach.first / (reach.first + reach.second) : lengths.first / whole, 0.05, 0.95)};
auto control1{(start[1] - start[0]) / share};
auto control2{(end[2] - end[3]) / (1.0 - share)};
auto const straight{end[3] - start[0]};
auto const along = [&straight](glm::dvec3 const &Direction) { return glm::length(glm::cross(glm::normalize(Direction), glm::normalize(straight))) < 1e-4; };
if (glm::length(straight) > 0.01 && along(control1) && along(-control2) && glm::dot(control1, straight) > 0.0 && glm::dot(control2, straight) < 0.0)
{
// two pieces of a straight make a straight
control1 = glm::dvec3{0.0};
control2 = glm::dvec3{0.0};
}
merged.axis.points[segment_data::point::control1] = control1;
merged.axis.points[segment_data::point::control2] = control2;
// the road is as wide at its far ends as it was
merged.taper[1] = static_cast<float>(after.taper[1] * after.width() / before.width());
if (after.divided() || before.divided())
{
if (false == before.divided())
{
merged.median = after.median;
merged.median.width[0] = 0.f;
}
merged.median.width[1] = (after.divided() ? after.median.width[1] : 0.f);
}
merged.median.round[1] = after.median.round[1];
for (std::size_t side = 0; side < merged.banks.size(); ++side)
{
if (merged.banks[side].set && after.banks[side].set)
{
merged.banks[side].width[1] = after.banks[side].width[1];
merged.banks[side].drop[1] = after.banks[side].drop[1];
}
}
remember(Record, kept.road);
remove(std::vector<road_node *>{dropped.road});
Record.roads_removed.emplace_back(dropped.road);
apply(std::vector<std::pair<road_node *, road_node::state>>{{kept.road, merged}});
return true;
}
road_node *editor_road::split(road_node &Road, double const T)
{
// 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};
auto const &path{Road.definition().axis};
auto const p0{path.points[segment_data::point::start]};
auto const p3{path.points[segment_data::point::end]};
if (path.points[segment_data::point::control1] == glm::dvec3{0.0} && path.points[segment_data::point::control2] == glm::dvec3{0.0})
{
auto const middle{glm::mix(p0, p3, T)};
first.axis.points[segment_data::point::end] = middle;
second.axis.points[segment_data::point::start] = middle;
}
else
{
auto const p1{p0 + path.points[segment_data::point::control1]};
auto const p2{p3 + path.points[segment_data::point::control2]};
auto const p01{glm::mix(p0, p1, T)};
auto const p12{glm::mix(p1, p2, T)};
auto const p23{glm::mix(p2, p3, T)};
auto const p012{glm::mix(p01, p12, T)};
auto const p123{glm::mix(p12, p23, T)};
auto const middle{glm::mix(p012, p123, T)};
first.axis.points[segment_data::point::control1] = p01 - p0;
first.axis.points[segment_data::point::control2] = p012 - middle;
first.axis.points[segment_data::point::end] = middle;
second.axis.points[segment_data::point::start] = middle;
second.axis.points[segment_data::point::control1] = p123 - middle;
second.axis.points[segment_data::point::control2] = p23 - p3;
}
auto const roll{static_cast<float>(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * T)};
first.axis.rolls[1] = roll;
second.axis.rolls[0] = roll;
// the width at the cut is what it was there
auto const scale{static_cast<float>(Road.definition().scale(T))};
first.taper[1] = scale;
second.taper[0] = scale;
// and so is what's between its directions, and what leads from it to the ground
if (Road.definition().divided())
{
auto const gap{static_cast<float>(Road.definition().median_width(T))};
first.median.width[1] = gap;
second.median.width[0] = gap;
}
first.median.round[1] = 0.f;
second.median.round[0] = 0.f;
for (std::size_t side = 0; side < first.banks.size(); ++side)
{
auto const &bank{Road.definition().banks[side]};
first.banks[side].width[1] = second.banks[side].width[0] = glm::mix(bank.width[0], bank.width[1], static_cast<float>(T));
first.banks[side].drop[1] = second.banks[side].drop[0] = glm::mix(bank.drop[0], bank.drop[1], static_cast<float>(T));
}
apply(std::vector<std::pair<road_node *, road_node::state>>{{&Road, first}});
auto const created{create(std::vector<road_node::state>{second})};
return created.empty() ? nullptr : created.front();
}
std::vector<road_node *> editor_road::chain(road_node &Road)
{
std::vector<road_node *> 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 && false == next.front()->definition().changing())
add_unique(result, next.front());
}
}
return result;
}
double editor_road::nearest_parameter(road_node::state const &State, glm::dvec3 const &Point)
{
return nearest_on(curve_points(State.axis), Point);
}
road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin)
{
road_node *result{nullptr};
double best{std::numeric_limits<double>::max()};
for (auto *road : simulation::Roads.sequence())
{
if (road == nullptr || road->m_editorremoved)
continue;
auto const &state{road->definition()};
auto const reach{0.5 * std::max(state.span(0.0), state.span(1.0)) + std::max(state.sides[0].width, state.sides[1].width) + Margin};
// the axis is no longer than the lines joining its points, which is good enough to tell the road is nowhere near
auto const points{curve_points(state.axis)};
if (glm::distance(road->location(), Point) > 0.5 * (glm::distance(points[0], points[1]) + glm::distance(points[1], points[2]) + glm::distance(points[2], points[3])) + reach + 10.0)
continue;
auto const position{curve_point(points, nearest_parameter(state, Point))};
auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})};
// roads passing over or under the point don't count
if (distance > reach || std::abs(position.y - Point.y) > 3.0)
continue;
if (distance < best)
{
best = distance;
result = road;
}
}
return result;
}
junction_node *editor_road::nearest_junction(glm::dvec3 const &Point)
{
junction_node *result{nullptr};
double best{std::numeric_limits<double>::max()};
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
auto const &state{junction->definition()};
// the junction takes the ground up to the ends of the roads it's made for
double reach{0.0};
for (auto const &arm : state.arms)
reach = std::max(reach, glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{state.centre.x, state.centre.z}));
auto const distance{glm::distance(glm::dvec2{state.centre.x, state.centre.z}, glm::dvec2{Point.x, Point.z})};
if (distance > reach || std::abs(state.centre.y - Point.y) > 3.0 || distance > best)
continue;
best = distance;
result = junction;
}
return result;
}
editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double const Radius, bool const Junctions)
{
loose_end result;
double best{Radius};
for (auto *road : simulation::Roads.sequence())
{
if (road == nullptr || road->m_editorremoved)
continue;
auto const &state{road->definition()};
for (auto const atend : {false, true})
{
auto const position{axis_end(state, atend)};
auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})};
if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty() || false == junctions_at(position).empty())
continue;
best = distance;
result = loose_end{};
result.road = road;
result.atend = atend;
result.position = position;
result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0));
}
// a road which keeps its two directions apart can be carried on as two roads, one for each of its roadways
for (auto const atend : {false, true})
{
auto const points{end_points(state, atend)};
float lanewidth{0.f};
if (points.size() < 3 || state.median_width(atend ? 1.0 : 0.0) < 0.5 || false == lane_width(state, atend ? 1.0 : 0.0, lanewidth))
continue;
// the whole end has to be loose
if (false == roads_at(points[0], road).empty() || false == junctions_at(points[0]).empty())
continue;
for (int half = 1; half <= 2; ++half)
{
auto const &position{points[half]};
auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})};
if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty())
continue;
best = distance;
result = loose_end{};
result.road = road;
result.atend = atend;
result.position = position;
result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0));
// the first of the points is the roadway of the lanes going against the axis
result.half = (half == 1 ? -1 : 1);
auto const lanes{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);
result.backward = (leaving ? 0 : lanes);
result.width = lanewidth;
}
}
}
if (false == Junctions)
return result;
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
for (auto const &arm : junction->definition().arms)
{
auto const distance{glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{Point.x, Point.z})};
if (distance > best || std::abs(arm.position.y - Point.y) > 3.0 || false == roads_at(arm.position, nullptr).empty())
continue;
best = distance;
result = loose_end{};
result.junction = junction;
result.position = arm.position;
result.outwards = glm::dvec3{arm.direction.x, 0.0, arm.direction.y};
// a road leading away from the junction has the lanes going out of it as the ones going along its axis
result.forward = arm.outgoing;
result.backward = arm.incoming;
result.width = arm.width;
}
}
return result;
}
std::vector<segment_data> editor_road::plan(glm::dvec3 const &Start, glm::dvec2 const *Direction, glm::dvec3 const &Target, glm::dvec2 const *Enddirection, bool const Straight, std::string &Error)
{
std::vector<segment_data> pieces;
glm::dvec2 const chord{Target.x - Start.x, Target.z - Start.z};
auto const span{glm::length(chord)};
if (span < kShortestRoad)
{
Error = "Too short";
return pieces;
}
// straight road, cut into pieces of reasonable length
auto const straight = [&](glm::dvec3 const &End) {
auto const count{std::max(1, static_cast<int>(std::ceil(glm::distance(Start, End) / kPieceLength)))};
for (int i = 0; i < count; ++i)
{
segment_data piece;
piece.points[segment_data::point::start] = glm::mix(Start, End, static_cast<double>(i) / count);
piece.points[segment_data::point::end] = glm::mix(Start, End, static_cast<double>(i + 1) / count);
pieces.emplace_back(piece);
}
};
if (Direction != nullptr && Enddirection != nullptr)
{
// both ends are tied, which leaves a single curve with the tangents set
if (glm::dot(*Direction, chord) < 0.1 * span || glm::dot(*Enddirection, chord) < 0.1 * span)
{
Error = "The road would have to turn back to meet the end";
return pieces;
}
auto const rise{(Target.y - Start.y) / 3.0};
segment_data piece;
piece.points[segment_data::point::start] = Start;
piece.points[segment_data::point::end] = Target;
piece.points[segment_data::point::control1] = glm::dvec3{Direction->x * span / 3.0, rise, Direction->y * span / 3.0};
piece.points[segment_data::point::control2] = glm::dvec3{-Enddirection->x * span / 3.0, -rise, -Enddirection->y * span / 3.0};
auto const radius{tightest_radius(piece)};
if (radius == 0.0)
{
pieces.clear();
straight(Target);
return pieces;
}
if (radius < kTightestRadius)
{
Error = "The bend is too tight";
return pieces;
}
piece.radius = static_cast<float>(radius);
pieces.emplace_back(piece);
return pieces;
}
if (Direction == nullptr && Enddirection != nullptr)
{
// only the far end is tied: the same road as the one leading back from there, turned around
glm::dvec2 const back{-*Enddirection};
auto reversed{plan(Target, &back, Start, nullptr, Straight, Error)};
for (auto &piece : reversed)
{
std::swap(piece.points[segment_data::point::start], piece.points[segment_data::point::end]);
std::swap(piece.points[segment_data::point::control1], piece.points[segment_data::point::control2]);
}
std::reverse(reversed.begin(), reversed.end());
return reversed;
}
if (Direction == nullptr)
{
straight(Target);
return pieces;
}
// the road leaves the start in set direction, and reaches the target with an arc
auto const forward{*Direction};
glm::dvec2 const left{forward.y, -forward.x};
auto const along{glm::dot(chord, forward)};
auto const side{glm::dot(chord, left)};
if (Straight || std::abs(side) < 0.02 * span)
{
if (along < kShortestRoad)
{
Error = "The target is behind the start";
return pieces;
}
straight(glm::dvec3{Start.x + forward.x * along, Target.y, Start.z + forward.y * along});
return pieces;
}
auto const turn{2.0 * std::atan2(side, along)}; // positive to the left
auto const radius{span * span / (2.0 * side)}; // likewise
if (std::abs(turn) > glm::radians(170.0))
{
Error = "The road would have to turn back to reach the target";
return pieces;
}
if (std::abs(radius) < kTightestRadius)
{
Error = "The bend is too tight";
return pieces;
}
glm::dvec2 const start{Start.x, Start.z};
auto const centre{start + left * radius};
auto const count{std::max(1, static_cast<int>(std::ceil(std::max(std::abs(turn) / kPieceTurn, std::abs(radius * turn) / kPieceLength))))};
auto const handle{4.0 / 3.0 * std::tan(std::abs(turn) / count / 4.0) * std::abs(radius)};
auto const rise{(Target.y - Start.y) / count};
for (int i = 0; i < count; ++i)
{
auto const angle0{turn * i / count};
auto const angle1{turn * (i + 1) / count};
auto const point0{centre - left * (radius * std::cos(angle0)) + forward * (radius * std::sin(angle0))};
auto const point1{centre - left * (radius * std::cos(angle1)) + forward * (radius * std::sin(angle1))};
auto const tangent0{forward * std::cos(angle0) + left * std::sin(angle0)};
auto const tangent1{forward * std::cos(angle1) + left * std::sin(angle1)};
segment_data piece;
piece.points[segment_data::point::start] = glm::dvec3{point0.x, Start.y + rise * i, point0.y};
piece.points[segment_data::point::end] = glm::dvec3{point1.x, Start.y + rise * (i + 1), point1.y};
piece.points[segment_data::point::control1] = glm::dvec3{tangent0.x * handle, rise / 3.0, tangent0.y * handle};
piece.points[segment_data::point::control2] = glm::dvec3{-tangent1.x * handle, -rise / 3.0, -tangent1.y * handle};
piece.radius = static_cast<float>(std::abs(radius));
pieces.emplace_back(piece);
}
// the arc is worked out from the start, make sure it ends exactly at the target
pieces.back().points[segment_data::point::end] = Target;
return pieces;
}
double editor_road::planar_length(segment_data const &Path)
{
auto const whole{subpath(Path, 0.0, 1.0)};
auto const &p0{whole.points[segment_data::point::start]};
auto const &p3{whole.points[segment_data::point::end]};
if (whole.points[segment_data::point::control1] == glm::dvec3{0.0} && whole.points[segment_data::point::control2] == glm::dvec3{0.0})
return glm::distance(glm::dvec2{p0.x, p0.z}, glm::dvec2{p3.x, p3.z});
auto const p1{p0 + whole.points[segment_data::point::control1]};
auto const p2{p3 + whole.points[segment_data::point::control2]};
double length{0.0};
glm::dvec2 previous{p0.x, p0.z};
for (int i = 1; i <= 32; ++i)
{
auto const t{i / 32.0};
auto const u{1.0 - t};
auto const point{u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3};
glm::dvec2 const current{point.x, point.z};
length += glm::distance(previous, current);
previous = current;
}
return length;
}
double editor_road::end_grade(segment_data const &Path)
{
auto const &control{Path.points[segment_data::point::control2]};
if (control != glm::dvec3{0.0})
{
auto const run{glm::length(glm::dvec2{control.x, control.z})};
return run > 1e-6 ? -control.y / run : 0.0;
}
auto const length{planar_length(Path)};
return length > 1e-6 ? (Path.points[segment_data::point::end].y - Path.points[segment_data::point::start].y) / length : 0.0;
}
std::vector<segment_data> editor_road::refine(std::vector<segment_data> const &Pieces, double const Length)
{
std::vector<segment_data> result;
for (auto const &piece : Pieces)
{
auto const count{std::max(1, static_cast<int>(std::ceil(planar_length(piece) / std::max(1.0, Length) - 0.01)))};
for (int i = 0; i < count; ++i)
result.emplace_back(count == 1 ? piece : subpath(piece, static_cast<double>(i) / count, static_cast<double>(i + 1) / count));
}
return result;
}
void editor_road::profile(std::vector<segment_data> &Pieces, std::vector<double> Heights, double const *Startgrade, double const *Endgrade)
{
auto const count{Pieces.size()};
if (count == 0 || Heights.size() != count + 1)
return;
std::vector<double> lengths(count);
for (std::size_t i = 0; i < count; ++i)
lengths[i] = std::max(0.01, planar_length(Pieces[i]));
// the heights in between are evened out, so the road doesn't ride every dip. it's never lowered though,
// as that would let the ground the heights were taken from come through the surface
{
auto const sampled{Heights};
for (std::size_t i = 1; i < count; ++i)
Heights[i] = std::max(sampled[i], 0.25 * sampled[i - 1] + 0.5 * sampled[i] + 0.25 * sampled[i + 1]);
}
std::vector<double> slopes(count);
for (std::size_t i = 0; i < count; ++i)
slopes[i] = (Heights[i + 1] - Heights[i]) / lengths[i];
// slope at each joint is shared by the pieces on both its sides, which is what rounds the profile
std::vector<double> grades(count + 1);
grades.front() = (Startgrade != nullptr ? *Startgrade : slopes.front());
grades.back() = (Endgrade != nullptr ? *Endgrade : slopes.back());
for (std::size_t i = 1; i < count; ++i)
{
auto const before{slopes[i - 1]};
auto const after{slopes[i]};
if (before * after <= 0.0)
{
// a crest or a bottom
grades[i] = 0.0;
continue;
}
auto grade{(before * lengths[i] + after * lengths[i - 1]) / (lengths[i - 1] + lengths[i])};
// kept from overshooting the heights the road is led through
auto const limit{3.0 * std::min(std::abs(before), std::abs(after))};
grades[i] = std::clamp(grade, -limit, limit);
}
for (std::size_t i = 0; i < count; ++i)
{
auto &piece{Pieces[i]};
auto &start{piece.points[segment_data::point::start]};
auto &end{piece.points[segment_data::point::end]};
auto &control1{piece.points[segment_data::point::control1]};
auto &control2{piece.points[segment_data::point::control2]};
start.y = Heights[i];
end.y = Heights[i + 1];
if (control1 == glm::dvec3{0.0} && control2 == glm::dvec3{0.0})
{
if (std::abs(grades[i] - slopes[i]) < 1e-4 && std::abs(grades[i + 1] - slopes[i]) < 1e-4)
continue; // even slope, the piece stays a plain straight
// a straight needs its control points spelled out to bend up or down
control1 = (end - start) / 3.0;
control2 = (start - end) / 3.0;
}
control1.y = grades[i] * glm::length(glm::dvec2{control1.x, control1.z});
control2.y = -grades[i + 1] * glm::length(glm::dvec2{control2.x, control2.z});
}
}
bool editor_road::nearest_joint(glm::dvec3 const &Point, double const Radius, glm::dvec3 &Joint)
{
auto best{Radius};
bool found{false};
for (auto const *road : simulation::Roads.sequence())
{
if (road == nullptr || road->m_editorremoved)
continue;
for (auto const atend : {false, true})
{
auto const position{axis_end(road->definition(), atend)};
auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})};
if (distance > best || std::abs(position.y - Point.y) > 3.0)
continue;
best = distance;
Joint = position;
found = true;
}
}
return found;
}
std::vector<editor_road::piece_end> editor_road::ends_at(glm::dvec3 const &Point)
{
std::vector<piece_end> result;
for (auto *road : simulation::Roads.sequence())
{
if (road == nullptr || road->m_editorremoved)
continue;
for (auto const atend : {false, true})
if (glm::distance(axis_end(road->definition(), atend), Point) < kSamePoint)
result.push_back({road, atend});
}
return result;
}
std::vector<std::pair<junction_node *, std::size_t>> editor_road::arms_at(glm::dvec3 const &Point)
{
std::vector<std::pair<junction_node *, std::size_t>> result;
for (auto *junction : simulation::Junctions.sequence())
{
if (junction == nullptr || junction->m_editorremoved)
continue;
auto const &arms{junction->definition().arms};
for (std::size_t arm = 0; arm < arms.size(); ++arm)
if (glm::distance(arms[arm].position, Point) < kSamePoint)
result.emplace_back(junction, arm);
}
return result;
}
void editor_road::settle(road_node &Road, record &Record, std::string &Notes)
{
for (auto const atend : {false, true})
{
if (Road.m_editorremoved)
return;
auto const &layout{Road.definition()};
auto const position{axis_end(layout, atend)};
// lanes heading for this end of the piece, and the ones leading away from it
auto const arriving{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<float>(layout.median_width(atend ? 1.0 : 0.0))};
std::string reason;
auto const junctions{arms_at(position)};
if (false == junctions.empty())
{
// a junction is made for the lanes of the road at each arm
for (auto const &entry : junctions)
{
auto *junction{entry.first};
auto state{junction->definition()};
auto &arm{state.arms[entry.second]};
if (arm.incoming == arriving && arm.outgoing == leaving && std::abs(arm.width - lanewidth) < 0.001f && std::abs(arm.median - gap) < 0.001f)
continue;
if (false == evenlanes)
{
Notes += format(STR_C("Lanes of \"%s\" differ in width, and a junction takes roads with lanes of the same width"), Road.name().c_str()) + ". ";
continue;
}
if (false == can_edit(*junction, &reason))
{
Notes += format(STR_C("\"%s\" wasn't fitted: %s"), junction->name().c_str(), STR(reason).c_str()) + ". ";
continue;
}
arm.incoming = arriving;
arm.outgoing = leaving;
arm.width = lanewidth;
arm.median = gap;
remember(Record, junction);
apply(std::vector<std::pair<junction_node *, junction_node::state>>{{junction, state}});
}
continue;
}
auto const neighbours{roads_at(position, &Road)};
if (neighbours.size() != 1)
continue;
auto *other{neighbours.front()};
auto const otherlayout{other->definition()};
auto const otheratend{glm::distance(axis_end(otherlayout, true), position) < kSamePoint};
// lanes of the neighbour heading for the place they meet, and the ones leading away from it
auto const otherarriving{otherlayout.lanes_at(otheratend ? 1 : 0, otheratend)};
auto const otherleaving{otherlayout.lanes_at(otheratend ? 1 : 0, false == otheratend)};
auto const othergap{static_cast<float>(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(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))
{
Notes += format(STR_C("\"%s\" wasn't fitted: %s"), other->name().c_str(), STR(reason).c_str()) + ". ";
continue;
}
auto state{otherlayout};
state.taper[otheratend ? 1 : 0] = std::clamp(static_cast<float>(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)
{
if (false == otherlayout.divided())
{
state.median.type = layout.median.type;
state.median.material = layout.median.material;
state.median.width = {0.f, 0.f};
}
state.median.width[otheratend ? 1 : 0] = gap;
}
remember(Record, other);
apply(std::vector<std::pair<road_node *, road_node::state>>{{other, state}});
continue;
}
// 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 += format(STR_C("Lanes of \"%s\" differ in width, and a road changes the number of its lanes only if they're all as wide"), (evenlanes ? other->name() : Road.name()).c_str()) + ". ";
continue;
}
if (false == can_edit(*other, &reason))
{
Notes += format(STR_C("The lanes next to \"%s\" weren't fitted: %s"), Road.name().c_str(), STR(reason).c_str()) + ". ";
continue;
}
auto *stretch{other};
if (false == otherlayout.changing() && otherlayout.length() > 1.5 * kTransitionLength)
{
auto const lengths{stations(otherlayout)};
auto *rest{split(*other, parameter_at(lengths, otheratend ? lengths.back() - kTransitionLength : kTransitionLength))};
if (rest == nullptr)
{
Notes += format(STR_C("\"%s\" couldn't be cut for the lanes to change"), other->name().c_str()) + ". ";
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<road_node *, road_node::state> 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 += format(STR_C("The lanes of \"%s\" can't be led on to the road next to it"), Road.name().c_str()) + ". ";
continue;
}
// 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)
{
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;
}
if (stretch == other)
remember(Record, other);
apply(std::vector<std::pair<road_node *, road_node::state>>{{stretch, state}});
}
}
bool editor_road::lane_width(road_node::state const &State, double const T, float &Width)
{
if (State.lanes.empty())
return false;
for (auto const &lane : State.lanes)
if (std::abs(lane.width - State.lanes.front().width) > 0.005f)
return false;
Width = static_cast<float>(State.lanes.front().width * State.scale(T));
return true;
}
bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const &Direction, int const Incoming, int const Outgoing, float const Width, branch &Result, std::string &Error)
{
Result = branch{};
auto const &layout{Road.definition()};
auto const centre{layout.point(T)};
auto const along{planar(layout.tangent(T))};
auto const direction{glm::length(Direction) > 1e-9 ? glm::normalize(Direction) : glm::dvec2{0.0, 1.0}};
// the sharper the angle the road leaves at, the further from the centre the edges of the roads part
auto const angle{std::acos(std::clamp(std::abs(glm::dot(direction, along)), 0.0, 1.0))};
if (angle < kSharpestBranch)
{
Error = "The road would leave at too sharp an angle";
return false;
}
if (layout.changing())
{
Error = format(STR_C("\"%s\" changes its lanes along the way, which leaves no place for a junction. Lead the road out of a piece next to it"), Road.name().c_str());
return false;
}
float lanewidth{0.f};
if (false == lane_width(layout, T, lanewidth))
{
Error = format(STR_C("Lanes of \"%s\" differ in width, and a junction takes roads with lanes of the same width"), Road.name().c_str());
return false;
}
auto const halfwidth{0.5 * (layout.forward + layout.backward) * lanewidth + 0.5 * layout.median_width(T)};
auto const branchhalfwidth{0.5 * (Incoming + Outgoing) * Width};
auto const reach{edge_reach(halfwidth, branchhalfwidth, angle) + kCornerRoom};
auto const branchreach{edge_reach(branchhalfwidth, halfwidth, angle) + kCornerRoom};
// where the road is cut on both sides of the centre
road_cut cuts[2];
std::vector<road_node *> consumed;
if (false == follow(Road, T, false, reach, cuts[0], consumed, Error) || false == follow(Road, T, true, reach, cuts[1], consumed, Error))
return false;
if (cuts[0].road == cuts[1].road && cuts[0].road != &Road)
{
Error = "The road is too short for a junction";
return false;
}
// what's left of the pieces
if (cuts[0].road == &Road && cuts[1].road == &Road)
{
Result.changes.emplace_back(&Road, part(layout, 0.0, cuts[0].t));
Result.added.emplace_back(part(layout, cuts[1].t, 1.0));
}
else if (cuts[0].road != &Road && cuts[1].road != &Road)
{
Result.removed.emplace_back(&Road);
}
for (auto const &cut : cuts)
{
if (cut.road == &Road && cuts[0].road == cuts[1].road)
continue;
// a cut at the very end the piece is kept from leaves it whole
if (cut.keepstart ? cut.t < 0.999 : cut.t > 0.001)
Result.changes.emplace_back(cut.road, cut.keepstart ? part(cut.road->definition(), 0.0, cut.t) : part(cut.road->definition(), cut.t, 1.0));
}
for (auto *piece : consumed)
Result.removed.emplace_back(piece);
std::string reason;
for (auto const &change : Result.changes)
{
if (false == can_edit(*change.first, &reason))
{
Error = format(STR_C("\"%s\" can't be changed: %s"), change.first->name().c_str(), STR(reason).c_str());
return false;
}
}
for (auto const *piece : Result.removed)
{
if (false == can_edit(*piece, &reason))
{
Error = format(STR_C("\"%s\" can't be changed: %s"), piece->name().c_str(), STR(reason).c_str());
return false;
}
}
// the junction: the road on both its sides, and the one leaving
auto &junction{Result.layout};
junction.centre = centre;
for (auto const &cut : cuts)
{
junction_node::arm_data arm;
if (false == cut_arm(cut, arm))
{
Error = format(STR_C("Lanes of \"%s\" differ in width, and a junction takes roads with lanes of the same width"), cut.road->name().c_str());
return false;
}
junction.arms.emplace_back(arm);
}
junction_node::arm_data arm;
arm.position = centre + glm::dvec3{direction.x, 0.0, direction.y} * branchreach;
arm.direction = direction;
arm.incoming = Incoming;
arm.outgoing = Outgoing;
arm.width = Width;
junction.arms.emplace_back(arm);
Result.arm = junction.arms.size() - 1;
dress(junction, layout);
if (layout.roundabout)
{
// the traffic going around goes ahead of the traffic joining it
for (auto &entry : junction.arms)
entry.priority = junction_node::right_of_way::priority;
junction.arms.back().priority = junction_node::right_of_way::yield;
}
junction.normalize();
return true;
}
bool editor_road::branch_from(junction_node &Junction, glm::dvec2 const &Direction, int const Incoming, int const Outgoing, float const Width, branch &Result, std::string &Error)
{
Result = branch{};
auto const &layout{Junction.definition()};
if (layout.arms.size() >= 4)
{
Error = "The junction has four roads already";
return false;
}
std::string reason;
if (false == can_edit(Junction, &reason))
{
Error = format(STR_C("\"%s\" can't be changed: %s"), Junction.name().c_str(), STR(reason).c_str());
return false;
}
auto const direction{glm::length(Direction) > 1e-9 ? glm::normalize(Direction) : glm::dvec2{0.0, 1.0}};
auto const halfwidth{0.5 * (Incoming + Outgoing) * Width};
// the roads which are there stay where they are, so the new one has to fit between them
auto reach{kCornerRoom};
for (std::size_t idx = 0; idx < layout.arms.size(); ++idx)
{
auto const &other{layout.arms[idx]};
auto const angle{std::acos(std::clamp(glm::dot(direction, other.direction), -1.0, 1.0))};
if (angle < kSharpestBranch)
{
Error = "The road would leave too close to another road of the junction";
return false;
}
if (angle > kWidestCorner)
continue;
auto const otherhalfwidth{0.5 * layout.arm_width(idx)};
auto const otherreach{glm::distance(glm::dvec2{other.position.x, other.position.z}, glm::dvec2{layout.centre.x, layout.centre.z})};
if (edge_reach(otherhalfwidth, halfwidth, angle) > otherreach - 0.5)
{
Error = "There's no room for the road between the roads the junction has; lead it out at a wider angle to them";
return false;
}
reach = std::max(reach, edge_reach(halfwidth, otherhalfwidth, angle) + kCornerRoom);
}
Result.junction = &Junction;
Result.layout = layout;
junction_node::arm_data arm;
arm.position = layout.centre + glm::dvec3{direction.x, 0.0, direction.y} * reach;
arm.direction = direction;
arm.incoming = Incoming;
arm.outgoing = Outgoing;
arm.width = Width;
Result.layout.arms.emplace_back(arm);
Result.arm = Result.layout.arms.size() - 1;
Result.layout.normalize();
return true;
}
junction_node *editor_road::carry_out(branch const &Branch, record &Record)
{
if (Branch.junction != nullptr)
{
remember(Record, Branch.junction);
apply(std::vector<std::pair<junction_node *, junction_node::state>>{{Branch.junction, Branch.layout}});
return Branch.junction;
}
// room is made first, so nothing gets joined with what's about to go
remove(Branch.removed);
Record.roads_removed.insert(Record.roads_removed.end(), Branch.removed.begin(), Branch.removed.end());
for (auto const &change : Branch.changes)
remember(Record, change.first);
apply(Branch.changes);
auto const added{create(Branch.added)};
Record.roads_created.insert(Record.roads_created.end(), added.begin(), added.end());
auto const junctions{create(std::vector<junction_node::state>{Branch.layout})};
if (junctions.empty())
return nullptr;
Record.junctions_created.emplace_back(junctions.front());
return junctions.front();
}
std::vector<road_node::state> editor_road::roundabout(glm::dvec3 const &Centre, double const Radius, road_node::state const &Layout, std::string const &Island)
{
std::vector<road_node::state> pieces;
auto layout{Layout};
// a single direction, going around with the middle on its left
if (layout.forward < 1)
layout.forward = std::max(1, layout.backward);
layout.backward = 0;
layout.missing = {};
layout.lanes.clear();
layout.changes.clear();
layout.taper = {1.f, 1.f};
layout.median.width = {0.f, 0.f};
layout.roundabout = true;
layout.normalize();
auto const halfwidth{0.5 * layout.width()};
if (Radius - halfwidth < 1.0)
return pieces;
if (false == Island.empty() && Island != "none")
{
// the island takes all that's inside the road
layout.sides[0].type = road_node::side_type::sidewalk;
layout.sides[0].width = static_cast<float>(Radius - halfwidth);
layout.sides[0].material = Island;
}
else
{
layout.sides[0] = road_node::side_data{};
}
layout.banks[0] = road_node::bank_data{};
// arcs short enough for a curve of this kind to pass for a circle
int const count{Radius > 40.0 ? 8 : 4};
auto const angle{6.283185307179586 / count};
auto const reach{4.0 / 3.0 * std::tan(0.25 * angle) * Radius};
auto const point = [&](double const Angle) { return Centre + glm::dvec3{std::cos(Angle), 0.0, std::sin(Angle)} * Radius; };
// the way the road goes at a point, with the angles going down
auto const heading = [](double const Angle) { return glm::dvec3{std::sin(Angle), 0.0, -std::cos(Angle)}; };
for (int index = 0; index < count; ++index)
{
auto const from{-angle * index};
auto const to{-angle * (index + 1)};
auto state{layout};
state.axis = segment_data{};
state.axis.points[segment_data::point::start] = point(from);
state.axis.points[segment_data::point::control1] = heading(from) * reach;
state.axis.points[segment_data::point::control2] = -heading(to) * reach;
state.axis.points[segment_data::point::end] = point(to);
state.axis.radius = static_cast<float>(Radius);
pieces.emplace_back(state);
}
// the last piece ends exactly where the first one starts
pieces.back().axis.points[segment_data::point::end] = pieces.front().axis.points[segment_data::point::start];
return pieces;
}
std::vector<std::string> const &editor_road::materials()
{
static std::vector<std::string> names;
static bool scanned{false};
if (scanned)
return names;
scanned = true;
std::set<std::string> found;
std::error_code error;
for (std::filesystem::directory_iterator file{Global.asCurrentTexturePath, std::filesystem::directory_options::skip_permission_denied, error}, end; file != end; file.increment(error))
{
if (error || false == file->is_regular_file(error))
continue;
auto extension{file->path().extension().string()};
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (extension != ".mat" && extension != ".dds" && extension != ".tga" && extension != ".png" && extension != ".bmp" && extension != ".jpg" && extension != ".ktx" && extension != ".tex")
continue;
auto name{file->path().stem().string()};
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
// names with spaces can't be put in a scenery file
if (name.empty() || name.find(' ') != std::string::npos)
continue;
found.emplace(name);
}
names.assign(found.begin(), found.end());
return names;
}