mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 16:14:52 +02:00
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
2086 lines
78 KiB
C++
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;
|
|
}
|