17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 05:09:20 +02:00

feat(roads): level crossings, spawn and removal points for road vehicles, deleting points of a road

This commit is contained in:
2026-10-04 00:31:44 +02:00
parent d37dff11e1
commit 3ce9102da0
12 changed files with 2369 additions and 33 deletions

View File

@@ -96,6 +96,7 @@ set(SOURCES
"utilities/Timer.cpp"
"world/Track.cpp"
"world/Road.cpp"
"world/RoadPoint.cpp"
"world/Traction.cpp"
"world/TractionPower.cpp"
"vehicle/Train.cpp"

View File

@@ -500,9 +500,10 @@ class editor_mode : public application_mode
{
bool window{false}; // the road window is open, the left mouse button belongs to its tools
bool lanes{true}; // lanes of the roads are drawn as trajectories
int tool{0}; // 0: select, 1: build
int tool{0}; // 0: select, 1: build, 2: place
road_node *selected{nullptr};
junction_node *junction{nullptr}; // selected junction
roadpoint_node *marker{nullptr}; // selected level crossing or traffic point
std::vector<glm::dvec3> points; // selected points where the pieces meet or end
road_node::state settings; // layout shown in the window: of the selected piece, or of the pieces about to be built
bool whole{true}; // layout changes go to every piece of the road the selected one belongs to
@@ -522,6 +523,17 @@ class editor_mode : public application_mode
bool follow{true}; // built roads are led over the ground between the clicked points
float crossingspeed{30.f}; // speed limit on the junctions being made
float heightstep{0.1f}; // how much the selected points are raised or lowered at a time
// level crossings and traffic points
int placekind{0}; // what the place tool puts down: 0: level crossing, 1: spawn point, 2: removal point
roadpoint_node::state placing[3]; // what the next point of each kind is going to be like
float stopmargin{5.f}; // distance from the outermost track of a new crossing to the places the vehicles stop at
roadpoint_node::state target; // the point a click of the place tool would make at the moment
bool hastarget{false};
glm::dvec3 targetdirection{0.0}; // the way the traffic goes there
std::string targetnote; // what's worth knowing about that point, or why there's none
glm::dvec3 aimed{0.0}; // where the cursor was when the target was worked out, and for what kind of point
int aimedkind{-1};
char vehicles[4096]{}; // vehicles to add to the set of a spawn point, written the way they're put in a scenery
road_plan plan; // what a click would make at the moment, worked out once a frame for display
glm::dvec3 hover{0.0}; // point of a road or a junction under the cursor a road can be led out of
bool hashover{false};
@@ -550,6 +562,16 @@ class editor_mode : public application_mode
void road_point_width(float const Width);
void road_point_heights(std::vector<double> const &Heights);
bool road_points_apply(std::vector<std::pair<road_node *, road_node::state>> const &Changes, std::vector<std::pair<junction_node *, junction_node::state>> const &Junctions);
// takes the selected points out of their roads
bool road_points_delete();
// level crossings and traffic points
void roadpoint_select(roadpoint_node *Point);
// works out the point a click of the place tool would make. Fresh: even if the cursor is where it was the last time
void roadpoint_aim(bool const Fresh);
void roadpoint_place();
void roadpoint_apply(roadpoint_node &Point, roadpoint_node::state const &State);
// Point: the point the controls are for, nullptr for the one about to be made
bool render_roadpoint_layout(roadpoint_node::state &State, roadpoint_node const *Point);
// Fresh: the ground which wasn't looked up yet is gathered on the spot, instead of a bit of it each frame
road_plan road_preview(bool const Fresh);
// Firstgrade, Lastgrade: slopes the road has to start and end with, nullptr for none

View File

@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/renderer.h"
#include "world/Track.h"
#include "world/Road.h"
#include "world/RoadPoint.h"
#include "simulation/simulation.h"
#include "imgui/imgui.h"
@@ -133,6 +134,10 @@ double const kSnapRadius{4.0}; // an end of a road this close to the cursor is w
double const kPointRadius{2.5}; // a point where pieces meet this close to the click is what gets selected, instead of the piece
double const kPointRange{400.0}; // the points are drawn for the roads this close to the camera
double const kSideMargin{1.0}; // how far past the edge of a road the cursor still counts as being on it
double const kMarkerRadius{2.0}; // a level crossing or a traffic point this close to the click is what gets selected
double const kRailReach{40.0}; // rails crossing the road this close to the cursor are taken for a single level crossing
double const kLaneReach{2.5}; // a lane with its middle this close to the cursor is the one a traffic point is put on
double const kFullTurn{6.283185307179586};
double const kJoinClearance{15.0}; // a road isn't led into the side of another this close to the point it's led from
double const kReach{3000.0}; // clicks landing further than this are taken for misses
double const kLaneRange{600.0}; // lanes are drawn for the roads this close to the camera
@@ -151,6 +156,19 @@ ImU32 const kSelectedColor{IM_COL32(40, 220, 255, 230)};
ImU32 const kPreviewColor{IM_COL32(255, 210, 60, 255)};
ImU32 const kRefusedColor{IM_COL32(240, 60, 60, 255)};
ImU32 const kSnapColor{IM_COL32(255, 60, 255, 255)};
ImU32 const kCrossingColor{IM_COL32(255, 220, 60, 230)};
ImU32 const kSpawnColor{IM_COL32(90, 230, 120, 230)};
ImU32 const kDespawnColor{IM_COL32(255, 110, 110, 230)};
roadpoint_node::kind_type point_kind(int const Index)
{
return Index == 1 ? roadpoint_node::kind_type::spawn : Index == 2 ? roadpoint_node::kind_type::despawn : roadpoint_node::kind_type::crossing;
}
char const *point_label(roadpoint_node::kind_type const Kind)
{
return Kind == roadpoint_node::kind_type::spawn ? "spawn point" : Kind == roadpoint_node::kind_type::despawn ? "removal point" : "level crossing";
}
// gathers new definitions for the pieces which meet or end at provided points.
// Change: receives a definition, which end of it is at the point, and the number of the point
@@ -209,6 +227,8 @@ void editor_mode::update_road_tool()
tool.selected = nullptr;
if (tool.junction != nullptr && tool.junction->m_editorremoved)
tool.junction = nullptr;
if (tool.marker != nullptr && tool.marker->m_editorremoved)
tool.marker = nullptr;
if ((tool.branchroad != nullptr && tool.branchroad->m_editorremoved) || (tool.branchjunction != nullptr && tool.branchjunction->m_editorremoved))
{
// what the road was to be led out of is gone
@@ -220,6 +240,11 @@ void editor_mode::update_road_tool()
tool.points.erase(std::remove_if(tool.points.begin(), tool.points.end(), [](glm::dvec3 const &Point) { return editor_road::ends_at(Point).empty(); }), tool.points.end());
tool.plan = road_plan{};
tool.hashover = false;
if (false == tool.window || tool.tool != 2)
{
tool.hastarget = false;
tool.aimedkind = -1;
}
if (false == tool.window)
{
tool.chain = false;
@@ -230,6 +255,14 @@ void editor_mode::update_road_tool()
if (glm::distance(tool.mouse, camera) > kReach)
{
tool.plan.error = "The cursor isn't over anything near enough";
tool.hastarget = false;
tool.aimedkind = -1;
return;
}
if (tool.tool == 2)
{
// the point a click would make at the moment, to be drawn over the view
roadpoint_aim(false);
return;
}
if (tool.tool != 1)
@@ -289,6 +322,280 @@ void editor_mode::junction_select(junction_node *Junction)
copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material);
}
void editor_mode::roadpoint_select(roadpoint_node *Point)
{
auto &tool{m_roadtool};
tool.marker = Point;
if (Point == nullptr)
return;
std::string reason;
tool.status = "Selected: " + Point->name();
if (false == editor_road::can_edit(*Point, &reason))
tool.status += ". It can't be changed: " + reason;
}
// works out the point a click of the place tool would make with the cursor where it is
void editor_mode::roadpoint_aim(bool const Fresh)
{
auto &tool{m_roadtool};
if (false == Fresh && tool.aimedkind == tool.placekind && glm::distance(tool.aimed, tool.mouse) < 0.1)
{
// the cursor didn't move, so neither did what it points at
return;
}
tool.aimed = tool.mouse;
tool.aimedkind = tool.placekind;
auto const kind{point_kind(tool.placekind)};
tool.target = tool.placing[tool.placekind];
tool.target.kind = kind;
tool.hastarget = false;
tool.targetnote.clear();
tool.targetdirection = glm::dvec3{0.0};
if (kind == roadpoint_node::kind_type::crossing)
{
auto const *road{editor_road::nearest(tool.mouse, kSideMargin)};
if (road == nullptr)
{
tool.targetnote = "Point at the place a road crosses the rails";
return;
}
glm::dvec3 middle{0.0};
double halfspan{0.0};
if (editor_road::rails_across(*road, tool.mouse, kRailReach, middle, halfspan))
{
// the crossing goes halfway between the outermost tracks. the distance is rounded up, so it reads well in the scenery file
tool.target.position = middle;
tool.target.clearance = static_cast<float>(std::ceil((halfspan + tool.stopmargin) * 2.0) * 0.5);
}
else
{
tool.target.position = road->definition().point(editor_road::nearest_parameter(road->definition(), tool.mouse));
tool.target.clearance = std::max(tool.stopmargin, 2.0f);
tool.targetnote = "No rails cross the road here; a crossing put here won't close until some do";
}
tool.hastarget = true;
return;
}
if (false == editor_road::nearest_lane(tool.mouse, kLaneReach, tool.target.position, tool.targetdirection))
{
tool.targetnote = "Point at a lane of a road";
return;
}
tool.hastarget = true;
}
// puts down the point the place tool is set for, as a step which can be taken back
void editor_mode::roadpoint_place()
{
auto &tool{m_roadtool};
roadpoint_aim(true);
if (false == tool.hastarget)
{
tool.status = tool.targetnote;
return;
}
auto const created{editor_road::create(std::vector<roadpoint_node::state>{tool.target})};
if (created.empty())
{
tool.status = "No scenery is loaded";
return;
}
auto const *point{created.front()};
auto const &state{point->definition()};
editor_road::record record;
record.points_created = created;
push_road_snapshot(std::move(record));
char text[64];
switch (state.kind)
{
case roadpoint_node::kind_type::crossing:
{
std::snprintf(text, sizeof(text), "%.1f", state.clearance);
tool.status = "Level crossing made: " + std::to_string(point->stops().size()) + " lanes stop " + text + " m from its middle, ";
tool.status += std::to_string(point->rails()) + " tracks are watched for trains.";
break;
}
case roadpoint_node::kind_type::spawn:
{
tool.status = (state.vehicles.empty() ? std::string{"Spawn point made, with no vehicles: nothing appears there until it's given some (Select, click the point)."} :
"Spawn point made, with " + std::to_string(state.vehicles.size() * state.copies) + " vehicles. They appear while the simulation runs.");
break;
}
case roadpoint_node::kind_type::despawn:
{
tool.status = "Removal point made, it takes vehicles from " + std::to_string(point->lanes().size()) + (point->lanes().size() == 1 ? " lane." : " lanes.");
break;
}
}
if (false == tool.targetnote.empty())
tool.status += " " + tool.targetnote + ".";
}
// replaces the definition of a level crossing or a traffic point, as a step which can be taken back
void editor_mode::roadpoint_apply(roadpoint_node &Point, roadpoint_node::state const &State)
{
auto &tool{m_roadtool};
std::string reason;
if (false == editor_road::can_edit(Point, &reason))
{
tool.status = "\"" + Point.name() + "\" can't be changed: " + reason;
return;
}
editor_road::record record;
record.points.emplace_back(&Point, Point.definition());
editor_road::apply(std::vector<std::pair<roadpoint_node *, roadpoint_node::state>>{{&Point, State}});
push_road_snapshot(std::move(record));
tool.status = "Changed";
}
// draws controls for a level crossing or a traffic point. returns: true if something was changed
bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoint_node const *Point)
{
auto &tool{m_roadtool};
bool changed{false};
// values typed for a point which is there already are taken when confirmed, so the point isn't made anew with each digit
auto const flags{Point != nullptr ? ImGuiInputTextFlags_EnterReturnsTrue : ImGuiInputTextFlags_None};
auto const number = [flags](char const *Label, float &Value, float const Step, char const *Format) {
ImGui::SetNextItemWidth(120.0f);
return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, flags);
};
ImVec4 const warning{1.0f, 0.6f, 0.3f, 1.0f};
switch (State.kind)
{
case roadpoint_node::kind_type::crossing:
{
if (Point != nullptr)
{
ImGui::Text("%d lanes stop here, %d tracks are watched for trains", static_cast<int>(Point->stops().size()), static_cast<int>(Point->rails()));
if (Point->rails() == 0)
ImGui::TextColored(warning, "No rails pass through it, so it never closes");
// the crossing is tied to what's there when it's made or changed, and when the roads change
if (ImGui::Button("Look for the rails again"))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "For when the tracks were laid or changed after the crossing was made.");
if (number("Stop this far from the middle [m]", State.clearance, 0.5f, "%.1f"))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the middle of the crossing.\n"
"The tracks passing within that distance from the middle are the ones the crossing is closed for.");
}
else
{
if (number("Stop this far from the outermost track [m]", tool.stopmargin, 0.5f, "%.1f"))
tool.stopmargin = std::clamp(tool.stopmargin, 2.0f, 50.0f);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the axis of the first track they'd get to.");
}
if (number("Closes with a train this far away [m]", State.warning, 50.0f, "%.0f"))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Measured along the tracks, both ways, with both ways of each switch counted.\n"
"A rail vehicle within that distance which gets nearer closes the crossing, and so does one right at it.\n"
"It opens a few seconds after the last one is gone.");
if (ImGui::Checkbox("Paint the stop lines", &State.stoplines))
changed = true;
break;
}
case roadpoint_node::kind_type::spawn:
{
if (Point != nullptr)
{
if (Point->lanes().empty())
ImGui::TextColored(warning, "It isn't on a lane, so nothing appears here");
else if (Point->vehicles() == 0)
ImGui::TextDisabled("The vehicles are made when the simulation runs");
else
ImGui::Text("%d vehicles made, %d of them wait for their turn", static_cast<int>(Point->vehicles()), static_cast<int>(Point->waiting()));
}
if (number("A vehicle every [s]", State.interval, 1.0f, "%.0f"))
changed = true;
if (number("Uneven by [0-1]", State.variation, 0.1f, "%.2f"))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Part of the time between vehicles which is drawn at random each time.\n"
"0: they come like clockwork, 1: anything from one right after another to twice the time.");
if (number("Speed [km/h]", State.velocity, 5.0f, "%.0f"))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "The speed the vehicles appear with. From there on the drivers go by the speed limits of the lanes.");
ImGui::SetNextItemWidth(120.0f);
if (ImGui::InputInt("Copies of each vehicle", &State.copies, 1, 1, flags))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "How many of each vehicle of the set are made. A vehicle can't be in two places at once,\n"
"so this is how many of a kind can be on the road together. All of them are loaded along with the scenery,\n"
"and a vehicle taken by a removal point goes back to wait for its next turn.");
// the set
ImGui::Separator();
ImGui::Text("Vehicles of the set: %d", static_cast<int>(State.vehicles.size()));
int removed{-1};
for (std::size_t index = 0; index < State.vehicles.size(); ++index)
{
auto const &vehicle{State.vehicles[index]};
ImGui::PushID(static_cast<int>(index));
if (ImGui::SmallButton("x"))
removed = static_cast<int>(index);
ImGui::SameLine();
if (vehicle.load > 0)
ImGui::Text("%s / %s, %s, %d %s", vehicle.folder.c_str(), vehicle.type.c_str(), vehicle.skin.c_str(), vehicle.load, vehicle.loadtype.c_str());
else
ImGui::Text("%s / %s, %s", vehicle.folder.c_str(), vehicle.type.c_str(), vehicle.skin.c_str());
ImGui::PopID();
}
if (removed >= 0)
{
State.vehicles.erase(State.vehicles.begin() + removed);
changed = true;
}
if (State.vehicles.empty())
ImGui::TextColored(warning, "No vehicles, so nothing appears here");
ImGui::InputTextMultiline("##vehicles", tool.vehicles, sizeof(tool.vehicles), ImVec2(-1.0f, ImGui::GetTextLineHeight() * 5.0f));
if (ImGui::Button("Add to the set"))
{
auto const parsed{roadpoint_node::parse_vehicles(tool.vehicles)};
if (parsed.empty())
{
tool.status = "No vehicle found in the text";
}
else
{
State.vehicles.insert(State.vehicles.end(), parsed.begin(), parsed.end());
tool.vehicles[0] = 0;
changed = true;
}
}
ImGui::SameLine();
if (ImGui::Button("Empty the set") && false == State.vehicles.empty())
{
State.vehicles.clear();
changed = true;
}
ImGui::TextDisabled("Paste the vehicles the way they're put in a scenery:");
ImGui::TextDisabled("node -1 0 none dynamic <folder> <skin> <type> <path>");
ImGui::TextDisabled(" <offset> <driver> <velocity> <load> enddynamic");
ImGui::TextDisabled("or as <folder> <skin> <type>, one vehicle a line.");
ImGui::TextDisabled("The path, the offset, the driver and the velocity don't matter.");
break;
}
case roadpoint_node::kind_type::despawn:
{
if (Point != nullptr)
ImGui::Text("Takes vehicles from %d %s", static_cast<int>(Point->lanes().size()), Point->lanes().size() == 1 ? "lane" : "lanes");
if (number("Takes vehicles this close [m]", State.radius, 0.5f, "%.1f"))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Road vehicles on the lanes passing within this distance from the point are taken off the road when they get here,\n"
"except for the one the user drives. Vehicles which came from a spawn point go back to wait for their next turn.\n"
"The distance it starts with takes in a single lane.");
break;
}
}
if (changed && Point != nullptr)
State.normalize();
return changed;
}
// replaces the definition of a junction, as a step which can be taken back
void editor_mode::junction_apply(junction_node &Junction, junction_node::state const &State)
{
@@ -636,8 +943,23 @@ void editor_mode::road_click()
return;
}
tool.mouse = ground;
if (tool.tool == 2)
{
roadpoint_place();
return;
}
if (tool.tool == 0)
{
if (auto *marker{editor_road::nearest_point(ground, kMarkerRadius)}; marker != nullptr)
{
// a level crossing or a traffic point
tool.points.clear();
tool.junction = nullptr;
road_select(nullptr);
roadpoint_select(marker);
return;
}
tool.marker = nullptr;
glm::dvec3 joint;
if (editor_road::nearest_joint(ground, kPointRadius, joint))
{
@@ -883,13 +1205,29 @@ void editor_mode::road_apply()
bool editor_mode::road_delete()
{
auto &tool{m_roadtool};
if (false == tool.window || (tool.selected == nullptr && tool.junction == nullptr))
{
// with points selected the key isn't meant for whatever else is selected in the editor
return tool.window && false == tool.points.empty();
}
if (false == tool.window)
return false;
std::string reason;
editor_road::record record;
if (tool.marker != nullptr)
{
auto *point{tool.marker};
if (false == editor_road::can_edit(*point, &reason))
{
tool.status = "Can't delete: " + reason;
return true;
}
editor_road::remove(std::vector<roadpoint_node *>{point});
record.points_removed.emplace_back(point);
push_road_snapshot(std::move(record));
tool.marker = nullptr;
tool.status = "\"" + point->name() + "\" deleted, Ctrl+Z brings it back";
return true;
}
if (false == tool.points.empty())
return road_points_delete();
if (tool.selected == nullptr && tool.junction == nullptr)
return false;
if (tool.junction != nullptr)
{
auto *junction{tool.junction};
@@ -919,6 +1257,30 @@ bool editor_mode::road_delete()
return true;
}
// takes the selected points out of their roads
bool editor_mode::road_points_delete()
{
auto &tool{m_roadtool};
editor_road::record record;
std::string notes;
int taken{0};
for (auto const &point : tool.points)
{
std::string error;
if (editor_road::dissolve(point, record, error))
++taken;
else if (notes.find(error) == std::string::npos)
notes += " A point was left: " + error + ".";
}
push_road_snapshot(std::move(record));
// the points which stayed are still selected, so it's clear which ones these are
tool.points.erase(std::remove_if(tool.points.begin(), tool.points.end(), [](glm::dvec3 const &Point) { return editor_road::ends_at(Point).empty(); }), tool.points.end());
tool.status = (taken == 0 ? std::string{"Nothing deleted."} : taken == 1 ? std::string{"Point deleted, Ctrl+Z brings it back."} :
std::to_string(taken) + " points deleted, Ctrl+Z brings them back.");
tool.status += notes;
return true;
}
bool editor_mode::road_split()
{
auto &tool{m_roadtool};
@@ -1049,8 +1411,14 @@ void editor_mode::push_road_snapshot(editor_road::record Record)
node = Record.junctions.front().first;
else if (false == Record.junctions_created.empty())
node = Record.junctions_created.front();
else
else if (false == Record.junctions_removed.empty())
node = Record.junctions_removed.front();
else if (false == Record.points.empty())
node = Record.points.front().first;
else if (false == Record.points_created.empty())
node = Record.points_created.front();
else
node = Record.points_removed.front();
EditorSnapshot snap;
snap.action = EditorSnapshot::Action::RoadEdit;
snap.node_name = node->name();
@@ -1072,6 +1440,8 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec
entry.second = entry.first->definition();
for (auto &entry : current.roads.junctions)
entry.second = entry.first->definition();
for (auto &entry : current.roads.points)
entry.second = entry.first->definition();
Opposite.push_back(std::move(current));
// things are taken out before the rest is changed and put in after it, so nothing gets joined with what's on its way out
@@ -1079,23 +1449,28 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec
{
editor_road::remove(record.roads_created);
editor_road::remove(record.junctions_created);
editor_road::remove(record.points_created);
}
else
{
editor_road::remove(record.roads_removed);
editor_road::remove(record.junctions_removed);
editor_road::remove(record.points_removed);
}
editor_road::apply(record.roads);
editor_road::apply(record.junctions);
editor_road::apply(record.points);
if (Undo)
{
editor_road::revive(record.roads_removed);
editor_road::revive(record.junctions_removed);
editor_road::revive(record.points_removed);
}
else
{
editor_road::revive(record.roads_created);
editor_road::revive(record.junctions_created);
editor_road::revive(record.points_created);
}
auto &tool{m_roadtool};
@@ -1104,6 +1479,9 @@ void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vec
road_select(tool.selected->m_editorremoved ? nullptr : tool.selected);
if (tool.junction != nullptr)
junction_select(tool.junction->m_editorremoved ? nullptr : tool.junction);
if (tool.marker != nullptr && tool.marker->m_editorremoved)
tool.marker = nullptr;
tool.aimedkind = -1;
}
// a field for the name of a material, with a list of the ones in the texture folder to pick from. returns: true if the material was changed
@@ -1333,6 +1711,10 @@ void editor_mode::render_road_window()
ImGui::RadioButton("Select", &tool.tool, 0);
ImGui::SameLine();
ImGui::RadioButton("Build", &tool.tool, 1);
ImGui::SameLine();
ImGui::RadioButton("Place", &tool.tool, 2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Level crossings, and the points where vehicles appear on the roads or are taken off them");
if (tool.tool != previoustool)
{
road_cancel();
@@ -1340,6 +1722,7 @@ void editor_mode::render_road_window()
{
tool.junction = nullptr;
tool.points.clear();
tool.marker = nullptr;
// the fields for the names of materials show the layout for the roads to be built again
road_select(nullptr);
}
@@ -1379,6 +1762,45 @@ void editor_mode::render_road_window()
// what's set here is what the next pieces get
render_road_layout(tool.settings);
}
else if (tool.tool == 2)
{
ImGui::RadioButton("Level crossing", &tool.placekind, 0);
ImGui::SameLine();
ImGui::RadioButton("Spawn point", &tool.placekind, 1);
ImGui::SameLine();
ImGui::RadioButton("Removal point", &tool.placekind, 2);
tool.placekind = std::clamp(tool.placekind, 0, 2);
switch (tool.placekind)
{
case 0:
ImGui::TextDisabled("LMB on a road where the rails cross it. The vehicles stop ahead");
ImGui::TextDisabled("of the rails for as long as a train is coming.");
break;
case 1:
ImGui::TextDisabled("LMB on a lane. The vehicles of the set appear there one by one");
ImGui::TextDisabled("and drive on the way the lane goes.");
break;
default:
ImGui::TextDisabled("LMB on a lane. The vehicles which get there are taken off the road;");
ImGui::TextDisabled("put one where the road leaves the scenery.");
break;
}
ImGui::Separator();
// what's set here is what the next points get
auto &placing{tool.placing[tool.placekind]};
placing.kind = point_kind(tool.placekind);
render_roadpoint_layout(placing, nullptr);
}
else if (tool.marker != nullptr)
{
auto &point{*tool.marker};
auto state{point.definition()};
ImGui::Text("%s, %s", point.name().c_str(), point_label(state.kind));
if (render_roadpoint_layout(state, &point))
roadpoint_apply(point, state);
if (ImGui::Button("Delete (Del)"))
road_delete();
}
else if (false == tool.points.empty())
{
// the road at the selected points
@@ -1428,6 +1850,12 @@ void editor_mode::render_road_window()
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Puts each point %.2f m over the ground under it; the height above the ground is set in the Build tool.", tool.offset);
}
if (ImGui::Button(tool.points.size() == 1 ? "Delete the point (Del)" : "Delete the points (Del)"))
road_points_delete();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Takes the point out of the road: the pieces on its two sides become one piece,\n"
"led from the far end of one to the far end of the other the way the road goes there.\n"
"A point the road ends at takes the last piece of the road with it.");
}
else if (tool.junction != nullptr)
{
@@ -1456,7 +1884,8 @@ void editor_mode::render_road_window()
}
else
{
ImGui::TextDisabled("LMB: select a road piece, a junction, or a point where pieces meet");
ImGui::TextDisabled("LMB: select a road piece, a junction, a point where pieces meet,");
ImGui::TextDisabled("a level crossing or a traffic point");
}
if (false == tool.status.empty())
{
@@ -1587,6 +2016,77 @@ void editor_mode::draw_road_overlay() const
drawpoint(arm.position, Color, 5.0f, true);
};
// a circle lying on the ground
auto const drawring = [&](glm::dvec3 const &Centre, double const Radius, ImU32 const Color) {
int const pieces{24};
glm::dvec3 previous{Centre + glm::dvec3{Radius, 0.0, 0.0}};
for (int i = 1; i <= pieces; ++i)
{
auto const angle{kFullTurn * i / pieces};
glm::dvec3 const next{Centre + glm::dvec3{Radius * std::cos(angle), 0.0, Radius * std::sin(angle)}};
projection.line(drawlist, previous, next, Color, 2.0f);
previous = next;
}
};
auto const drawlabel = [&](glm::dvec3 const &Point, ImU32 const Color, char const *Text) {
ImVec2 screen;
if (false == projection.room(drawlist) || false == projection.project(Point, screen) || projection.outside(screen, screen))
return;
drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 8.0f), Color, Text);
};
// a level crossing or a traffic point. Point: the node it's drawn for, nullptr for one which isn't made yet
auto const drawmarker = [&](roadpoint_node::state const &State, roadpoint_node const *Point, ImU32 const Color) {
drawpoint(State.position, Color, 6.0f, true);
auto const labelled{glm::distance(State.position, camera) < 200.0};
switch (State.kind)
{
case roadpoint_node::kind_type::crossing:
{
// the tracks within the circle are the ones it's closed for, the lines are where the vehicles stop
drawring(State.position, State.clearance, Color);
auto const closed{Point != nullptr && Point->closed()};
if (Point != nullptr)
{
for (auto const &stop : Point->stops())
{
glm::dvec3 across{stop.direction.z, 0.0, -stop.direction.x};
if (glm::length(across) < 1e-6)
continue;
across = glm::normalize(across) * (0.5 * stop.width);
projection.line(drawlist, stop.position - across, stop.position + across, closed ? kRefusedColor : Color, 4.0f);
}
}
if (labelled)
drawlabel(State.position, Color, closed ? "crossing, closed" : "crossing");
break;
}
case roadpoint_node::kind_type::spawn:
{
drawpoint(State.position, Color, 11.0f, false);
if (labelled)
drawlabel(State.position, Color, "spawn");
break;
}
case roadpoint_node::kind_type::despawn:
{
drawring(State.position, State.radius, Color);
if (labelled)
drawlabel(State.position, Color, "removal");
break;
}
}
};
for (auto const *point : simulation::Roadpoints.sequence())
{
if (point == nullptr || point->m_editorremoved || glm::distance(point->location(), camera) > kPointRange)
continue;
auto const kind{point->definition().kind};
drawmarker(point->definition(), point,
point == tool.marker ? kSelectedColor : kind == roadpoint_node::kind_type::spawn ? kSpawnColor : kind == roadpoint_node::kind_type::despawn ? kDespawnColor : kCrossingColor);
if (point == tool.marker)
drawpoint(point->definition().position, kSelectedColor, 15.0f, false);
}
if (tool.selected != nullptr && false == tool.selected->m_editorremoved)
{
auto const &state{tool.selected->definition()};
@@ -1649,6 +2149,30 @@ void editor_mode::draw_road_overlay() const
}
}
}
else if (tool.tool == 2)
{
if (tool.hastarget)
{
drawmarker(tool.target, nullptr, kPreviewColor);
if (glm::length(tool.targetdirection) > 1e-6)
{
// the way the traffic goes on the lane
auto const direction{glm::normalize(tool.targetdirection)};
glm::dvec3 const across{direction.z, 0.0, -direction.x};
auto const tip{tool.target.position + direction * 5.0};
projection.line(drawlist, tool.target.position, tip, kPreviewColor, 2.0f);
projection.line(drawlist, tip, tip - direction * 1.2 + across * 0.8, kPreviewColor, 2.0f);
projection.line(drawlist, tip, tip - direction * 1.2 - across * 0.8, kPreviewColor, 2.0f);
}
hint = std::string{"LMB: put a "} + point_label(tool.target.kind) + " here";
if (false == tool.targetnote.empty())
hint += " " + tool.targetnote;
}
else
{
hint = (tool.targetnote.empty() ? std::string{"LMB: put a "} + point_label(point_kind(tool.placekind)) + " on a road" : tool.targetnote);
}
}
else
{
// points where the pieces meet or end, to pick from
@@ -1664,10 +2188,11 @@ void editor_mode::draw_road_overlay() const
drawpoint(point, kSelectedColor, 6.0f, true);
drawpoint(point, kSelectedColor, 11.0f, false);
}
hint = false == tool.points.empty() ? "LMB: select a point Shift+LMB: add a point or take it out Ctrl+Z: undo" :
tool.selected != nullptr ? "LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo" :
tool.junction != nullptr ? "LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo" :
"LMB: select a piece, a junction or a point (white dot)";
hint = tool.marker != nullptr ? "LMB: select a piece, a junction, a point or a marker Del: delete Ctrl+Z: undo" :
false == tool.points.empty() ? "LMB: select a point Shift+LMB: add a point or take it out Del: delete the points Ctrl+Z: undo" :
tool.selected != nullptr ? "LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo" :
tool.junction != nullptr ? "LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo" :
"LMB: select a piece, a junction, a point (white dot) or a marker";
}
// shown above the place the track tools put their hints at
ImGuiIO const &io = ImGui::GetIO();

View File

@@ -317,6 +317,66 @@ void retire(std::vector<TTrack *> const &Tracks)
editor_track::retire(*track);
}
// value of the curve parameter for the point of a curve nearest to specified point, seen from above
double nearest_on(std::array<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);
// the lanes are listed left to right for someone facing along the axis, who's now looking the other way
std::reverse(result.lanes.begin(), result.lanes.end());
std::reverse(result.changes.begin(), result.changes.end());
for (auto &change : result.changes)
change = ((change & road_node::change_toright) != 0 ? road_node::change_toleft : 0) | ((change & road_node::change_toleft) != 0 ? road_node::change_toright : 0);
std::swap(result.sides[0], result.sides[1]);
std::swap(result.taper[0], result.taper[1]);
return result;
}
// smallest radius of the bend made by a bezier curve, seen from above; 0 if the curve is practically straight
double tightest_radius(segment_data const &Path)
{
@@ -468,6 +528,8 @@ void editor_road::refresh(std::vector<road_node *> const &Roads, std::vector<jun
// 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};
@@ -541,6 +603,7 @@ void editor_road::refresh(std::vector<road_node *> const &Roads, std::vector<jun
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)
@@ -702,6 +765,288 @@ void editor_road::revive(std::vector<junction_node *> const &Junctions)
});
}
bool editor_road::can_edit(roadpoint_node const &Point, std::string *Reason)
{
std::string reason;
auto editable{true};
if (simulation::Region == nullptr)
{
reason = "No scenery is loaded";
editable = false;
}
else
{
editable = scene::Layers.editable(&Point, &reason);
}
if (false == editable && Reason != nullptr)
*Reason = reason;
return editable;
}
std::vector<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 * state.width() * std::max(state.taper[0], state.taper[1]) + Reach + 10.0)
continue;
for (auto const *lane : road->tracks())
{
if (lane == nullptr || lane->m_paths.empty())
continue;
auto const points{curve_points(lane->m_paths.front())};
auto const t{nearest_on(points, Point)};
auto const position{curve_point(points, t)};
auto const distance{planar_distance(position, Point)};
// lanes passing over or under the point don't count
if (distance > best || std::abs(position.y - Point.y) > 3.0)
continue;
best = distance;
Position = position;
// the lanes are laid out the way the traffic goes
Direction = curve_point(points, std::min(1.0, t + 0.01)) - curve_point(points, std::max(0.0, t - 0.01));
found = true;
}
}
return found;
}
bool editor_road::rails_across(road_node const &Road, glm::dvec3 const &Near, double const Reach, glm::dvec3 &Middle, double &Halfspan)
{
int const pieces{32}; // lines the axis is taken for
int const railpieces{16}; // and each of the rails
auto const axis{curve_points(Road.definition().axis)};
std::vector<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 = "\"" + end.road->name() + "\" can't be changed: " + reason;
return false;
}
}
if (ends.size() == 1)
{
// the point a road ends at: the last piece goes
remove(std::vector<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.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());
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)
{
if (T <= 0.001 || T >= 0.999)
@@ -764,25 +1109,7 @@ std::vector<road_node *> editor_road::chain(road_node &Road)
double editor_road::nearest_parameter(road_node::state const &State, glm::dvec3 const &Point)
{
auto const points{curve_points(State.axis)};
auto const distance = [&](double const T) {
auto const position{curve_point(points, T)};
return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z});
};
double best{0.0};
for (int i = 1; i <= 64; ++i)
if (distance(i / 64.0) < distance(best))
best = i / 64.0;
double step{1.0 / 64.0};
for (int i = 0; i < 24; ++i)
{
step *= 0.5;
if (best - step >= 0.0 && distance(best - step) < distance(best))
best -= step;
else if (best + step <= 1.0 && distance(best + step) < distance(best))
best += step;
}
return best;
return nearest_on(curve_points(State.axis), Point);
}
road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin)

View File

@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
#include <glm/glm.hpp>
#include "world/Road.h"
#include "world/RoadPoint.h"
// editor operations on the roads. a road in the scenery is a string of pieces joined end to end,
// each piece being a road node with lanes and geometry of its own; roads meet at junctions
@@ -54,10 +55,14 @@ class editor_road
std::vector<std::pair<junction_node *, junction_node::state>> junctions; // junctions with what they were like before
std::vector<junction_node *> junctions_created;
std::vector<junction_node *> junctions_removed;
std::vector<std::pair<roadpoint_node *, roadpoint_node::state>> points; // crossings and traffic points with what they were like before
std::vector<roadpoint_node *> points_created;
std::vector<roadpoint_node *> points_removed;
bool empty() const
{
return roads.empty() && roads_created.empty() && roads_removed.empty() && junctions.empty() && junctions_created.empty() && junctions_removed.empty();
return roads.empty() && roads_created.empty() && roads_removed.empty() && junctions.empty() && junctions_created.empty() && junctions_removed.empty() && points.empty() &&
points_created.empty() && points_removed.empty();
}
};
// a road led out of the side of another road, or out of a junction between its arms: the junction this takes,
@@ -91,6 +96,25 @@ class editor_road
static std::vector<junction_node *> create(std::vector<junction_node::state> const &States);
static void remove(std::vector<junction_node *> const &Junctions);
static void revive(std::vector<junction_node *> const &Junctions);
// level crossings, and the points the vehicles appear at or are taken away at. these go by the lanes and the rails they find
// around them, so unlike the roads they can be changed with vehicles around
static bool can_edit(roadpoint_node const &Point, std::string *Reason = nullptr);
static std::vector<roadpoint_node *> create(std::vector<roadpoint_node::state> const &States);
static void apply(std::vector<std::pair<roadpoint_node *, roadpoint_node::state>> const &Changes);
static void remove(std::vector<roadpoint_node *> const &Points);
static void revive(std::vector<roadpoint_node *> const &Points);
// the crossing or traffic point nearest to specified point, within specified distance from it
static roadpoint_node *nearest_point(glm::dvec3 const &Point, double const Radius);
// place on the middle of a lane nearest to specified point, within specified distance from it.
// Direction: receives the way the traffic goes there. returns: false if there's no lane that close
static bool nearest_lane(glm::dvec3 const &Point, double const Reach, glm::dvec3 &Position, glm::dvec3 &Direction);
// rails crossing specified piece within specified distance from a point. Middle: receives the point of the axis halfway between
// the outermost of them, Halfspan: how far along the road these are from it. returns: false if no rails cross the piece there
static bool rails_across(road_node const &Road, glm::dvec3 const &Near, double const Reach, glm::dvec3 &Middle, double &Halfspan);
// takes a point out of a road. the two pieces which meet at it are made one, led from the far end of one to the far end
// of the other the way these ends go; a point a road ends at takes the last piece of the road with it.
// what gets done is added to Record. returns: false if it can't be done, with the reason in Error
static bool dissolve(glm::dvec3 const &Joint, record &Record, std::string &Error);
// cuts the piece in two at specified point of its axis. returns: the new piece, holding the part past the cut
static road_node *split(road_node &Road, double const T);
// the pieces which make a single road together with specified one, in no particular order

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include "scene/scenelayers.h"
#include "world/Track.h"
#include "world/Road.h"
#include "world/RoadPoint.h"
#include "simulation/simulation.h"
#include "model/AnimModel.h"
@@ -923,6 +924,68 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(junction);
}
// level crossings and traffic points of the roads
std::vector<roadpoint_node *> savedroadpoints;
std::vector<roadpoint_node const *> droppedroadpoints;
auto const roadpoint_text = [](roadpoint_node &Point, glm::dvec3 const &Offset) {
auto const definition{Point.definition()};
auto local{definition};
local.position -= Offset;
Point.define(local);
std::string text;
Point.export_as_text(text);
Point.define(definition);
text.erase(text.find_last_not_of(" \t\r\n") + 1);
return text;
};
for (auto *point : simulation::Roadpoints.sequence())
{
if (point == nullptr || point->from_template())
{
continue;
}
auto const nodelayer{resolve(point->layer())};
if (false == is_output(nodelayer))
{
continue;
}
auto const lookup{m_sources.find(point)};
if (lookup == m_sources.end())
{
if (point->m_editorremoved || false == point->dirty())
{
continue;
}
created[nodelayer].emplace_back(point, roadpoint_text(*point, layer(nodelayer).context_insert().offset));
savedroadpoints.push_back(point);
continue;
}
auto const &source{lookup->second};
if ((false == point->m_editorremoved && false == point->dirty()) || false == writable(source.layer))
{
continue;
}
if (false == load(source.layer))
{
return fail(state.error);
}
file_patch::edit edit;
edit.begin = source.span.begin;
edit.end = source.span.end;
if (point->m_editorremoved)
{
droppedroadpoints.push_back(point);
}
else
{
edit.text = roadpoint_text(*point, source.context.offset);
edit.length = edit.text.size();
edit.node = point;
savedroadpoints.push_back(point);
}
state.patches[source.layer].edits.emplace_back(std::move(edit));
rewritten.emplace(point);
}
for (auto const &erased : m_erased)
{
file_patch::edit edit;
@@ -1382,6 +1445,14 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
{
m_sources.erase(junction);
}
for (auto *point : savedroadpoints)
{
point->m_dirty = false;
}
for (auto const *point : droppedroadpoints)
{
m_sources.erase(point);
}
if (removedterrain)
{
// binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load

View File

@@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/.
#include "world/MemCell.h"
#include "world/Track.h"
#include "world/Road.h"
#include "world/RoadPoint.h"
#include "world/Traction.h"
#include "world/TractionPower.h"
#include "audio/sound.h"
@@ -39,6 +40,7 @@ memory_table Memory;
path_table Paths;
road_table Roads;
junction_table Junctions;
roadpoint_table Roadpoints;
traction_table Traction;
powergridsource_table Powergrid;
instance_table Instances;
@@ -117,6 +119,8 @@ state_manager::update( double const Deltatime, int Iterationcount ) {
TAnimModel::AnimUpdate( totaltime ); // wykonanie zakolejkowanych animacji
simulation::Powergrid.update( totaltime );
// level crossings and the points road vehicles appear at; ahead of the vehicles, so the drivers act on what these decide
simulation::Roadpoints.update( totaltime );
simulation::Vehicles.update( Deltatime, Iterationcount );
}

View File

@@ -84,6 +84,7 @@ extern memory_table Memory;
extern path_table Paths;
extern road_table Roads;
extern junction_table Junctions;
extern roadpoint_table Roadpoints;
extern traction_table Traction;
extern powergridsource_table Powergrid;
extern instance_table Instances;

View File

@@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/lightarray.h"
#include "world/TractionPower.h"
#include "world/Road.h"
#include "world/RoadPoint.h"
#include "application/application.h"
#include "rendering/renderer.h"
#include "utilities/Logs.h"
@@ -182,6 +183,7 @@ state_serializer::deserialize_continue(std::shared_ptr<deserializer_state> state
// so it's inserted in the region only after that file had its chance to be written
simulation::Roads.create_geometry( Scratchpad );
simulation::Junctions.create_geometry( Scratchpad );
simulation::Roadpoints.create_geometry();
return false;
}
@@ -402,6 +404,8 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc
// the junctions tie the lanes of the roads together, what's left loose after that gets closed by the roads
simulation::Junctions.InitJunctions();
simulation::Roads.InitRoads();
// crossings and traffic points go by the lanes, which are complete at this point
simulation::Roadpoints.InitRoadpoints();
simulation::Traction.InitTraction();
simulation::Events.InitEvents();
simulation::Events.InitLaunchers();
@@ -538,6 +542,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
scene::Groups.insert( scene::Groups.handle(), junction );
scene::Layers.track( junction, { sourcebegin, Input.TokenEnd() } );
}
else if( roadpoint_node::is_keyword( nodedata.type ) ) {
// level crossing, or a point where road vehicles appear or are taken away
auto *point { new roadpoint_node( nodedata ) };
point->import(
Input,
( Scratchpad.location.offset.empty() ?
glm::dvec3 { 0.0 } :
glm::dvec3 { Scratchpad.location.offset.top() } ) );
if( false == simulation::Roadpoints.insert( point ) ) {
ErrorLog( "Bad scenario: duplicate road point name \"" + point->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" );
}
scene::Groups.insert( scene::Groups.handle(), point );
scene::Layers.track( point, { sourcebegin, Input.TokenEnd() } );
}
else if( nodedata.type == "traction" ) {
auto *traction { deserialize_traction( Input, Scratchpad, nodedata ) };
@@ -713,6 +731,9 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
{ "track", scene::layer_item::track },
{ "road", scene::layer_item::track },
{ "junction", scene::layer_item::track },
{ "crossing", scene::layer_item::track },
{ "spawn", scene::layer_item::track },
{ "despawn", scene::layer_item::track },
{ "traction", scene::layer_item::traction },
{ "tractionpowersource", scene::layer_item::powersource },
{ "model", scene::layer_item::model },
@@ -1427,6 +1448,11 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons
junction->export_as_text( scmfile );
}
}
for( auto const *point : Roadpoints.sequence() ) {
if( point != nullptr && false == point->m_editorremoved && point->dirty() == Dirty && point->group() == null_handle ) {
point->export_as_text( scmfile );
}
}
// traction
scmfile << "// traction\n";
for( auto const *traction : Traction.sequence() ) {
@@ -1482,7 +1508,8 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st
std::pair<int, int> state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) {
// statements which take more than a single token, with the tokens ending them
static std::unordered_map<std::string, std::string> const nodeends {
{ "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "junction", "endjunction" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" },
{ "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "junction", "endjunction" }, { "crossing", "endcrossing" }, { "spawn", "endspawn" }, { "despawn", "enddespawn" },
{ "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" },
{ "triangles", "endtri" }, { "triangle_strip", "endtri" }, { "triangle_fan", "endtri" }, { "lines", "endline" }, { "line_strip", "endline" }, { "line_loop", "endline" },
{ "memcell", "endmemcell" }, { "eventlauncher", "end" }, { "sound", "endsound" } };
static std::unordered_map<std::string, std::string> const statementends {

View File

@@ -47,6 +47,8 @@ class road_node; // kawałek drogi, z którego powstają pasy ruchu
class road_table;
class junction_node;
class junction_table;
class roadpoint_node; // przejazd kolejowy albo miejsce, w którym pojazdy drogowe się pojawiają lub znikają
class roadpoint_table;
struct dictionary_source;
class trainset_desc;
class scenery_desc;

1125
world/RoadPoint.cpp Normal file

File diff suppressed because it is too large Load Diff

207
world/RoadPoint.h Normal file
View File

@@ -0,0 +1,207 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <glm/glm.hpp>
#include "utilities/Classes.h"
#include "utilities/Names.h"
#include "scene/scenenode.h"
#include "world/Road.h"
class TDynamicObject;
class basic_event;
// a place on a road where something happens to the traffic. three kinds of it:
// - a level crossing: vehicles are stopped ahead of the rails for as long as a train is near,
// - a point where vehicles appear, drawn from a set made ready while the scenery is loaded,
// - a point where vehicles are taken away. the ones which came from a point of the previous kind go back to its set.
// scenery entries:
// node <max> <min> <name> crossing <position> [clearance <m>] [warning <m>] [stoplines <yes|no>] endcrossing
// node <max> <min> <name> spawn <position> [interval <s>] [variation <0..1>] [velocity <km/h>] [copies <n>]
// [node <max> <min> <name> dynamic <folder> <skin> <type> <path> <offset> <driver> <velocity> <load> [<load type>] enddynamic]... endspawn
// node <max> <min> <name> despawn <position> [radius <m>] enddespawn
// the vehicles of a spawn point are given the way vehicles are put in a scenery; where they're put and how they're driven
// is up to the point, so the path, the offset, the driver and the velocity of these entries don't matter
class roadpoint_node : public scene::basic_node
{
public:
// types
enum class kind_type
{
crossing,
spawn,
despawn
};
struct vehicle_data
{
std::string folder; // where the vehicle is defined, in the folder of the vehicles
std::string skin;
std::string type; // name of its definition files
int load{0};
std::string loadtype;
bool operator==(vehicle_data const &Other) const
{
return folder == Other.folder && skin == Other.skin && type == Other.type && load == Other.load && loadtype == Other.loadtype;
}
};
struct state
{
kind_type kind{kind_type::crossing};
glm::dvec3 position{0.0};
// level crossing
float clearance{6.f}; // distance along the road from the point to the places the vehicles stop at
float warning{800.f}; // how far along the rails a train coming closes the crossing
bool stoplines{true}; // lines are painted where the vehicles stop
// point where vehicles appear
float interval{20.f}; // seconds from a vehicle to the next
float variation{0.5f}; // part of the interval which is drawn at random
float velocity{50.f}; // speed the vehicles appear with, and are told to keep, km/h
int copies{2}; // number of each vehicle made ready
std::vector<vehicle_data> vehicles;
// point where vehicles are taken away
float radius{2.f}; // vehicles on lanes this close to the point are taken
// brings the content to a usable form
void normalize();
};
// place the vehicles of a lane stop at while a crossing is closed
struct stop_data
{
TTrack *track{nullptr}; // the lane it's on
glm::dvec3 position{0.0};
glm::dvec3 direction{0.0, 0.0, 1.0}; // the way the lane goes there
float width{3.5f}; // of the lane
};
// constructors
explicit roadpoint_node(scene::node_data const &Nodedata);
// methods
// restores content of the node from provided input stream; reads up to and including the closing statement
void import(cParser &Input, glm::dvec3 const &Offset);
state const &definition() const
{
return m_state;
}
// replaces what the node knows about itself. what it's tied to stays, it's up to the caller to bind it again.
// a spawn point given another set of vehicles makes them anew; the ones it had stay on the road until they're taken away
void define(state const &State);
// scenery keyword for specified kind of point, and the statement closing its entry
static std::string keyword(kind_type const Kind);
static bool is_keyword(std::string const &Type);
// reads vehicles out of a text. each is given the way vehicles are put in a scenery, or, if there's no such entry
// in the text, as "<folder> <skin> <type> [<load> <load type>]" on a line of its own
static std::vector<vehicle_data> parse_vehicles(std::string const &Text);
// ties the point to the lanes and the rails as they are at the moment, or lets go of them
void bind();
void unbind();
// makes the vehicles of a spawn point, once. it loads their models, which takes a while
void prepare();
// to be called with each step of the simulation
void update(double const Deltatime);
// state for display
bool closed() const
{
return m_closed;
}
std::vector<stop_data> const &stops() const
{
return m_stops;
}
std::vector<TTrack *> const &lanes() const
{
return m_lanes;
}
std::size_t rails() const
{
return m_rails.size();
}
// vehicles made for a spawn point, and how many of them wait for their turn
std::size_t vehicles() const
{
return m_pool.size();
}
std::size_t waiting() const;
// generates geometry of the stop lines
std::vector<scene::shape_node> create_shapes() const;
// puts geometry of the point in the scene, or takes it back
void show();
void hide();
bool shown() const
{
return m_shapes.shown();
}
// members
bool m_editorremoved{false}; // removed in the editor; kept around so the removal can be undone
private:
// types
struct pooled_vehicle
{
TDynamicObject *vehicle{nullptr};
double velocity{0.0}; // what its speed was when it was made, to be given back each time it's put on the road
};
// methods
float radius_() override;
void serialize_(std::ostream &Output) const override;
void deserialize_(std::istream &Input) override;
void export_as_text_(std::ostream &Output) const override;
void bind_crossing();
void update_crossing(double const Deltatime);
void update_spawn(double const Deltatime);
void update_despawn();
void close(bool const Closed);
// members
state m_state;
bool m_bound{false};
// level crossing
std::vector<stop_data> m_stops;
std::vector<basic_event *> m_events; // orders for the drivers, one for each stop. these are never freed, as a driver can hold on to one
std::vector<TTrack *> m_rails; // rails a train closes the crossing from
std::map<TDynamicObject const *, double> m_seen; // rail vehicles near, with how far each was the last time
bool m_closed{false};
double m_hold{0.0}; // time left for the crossing to stay closed after the last train was seen
double m_scan{0.0}; // time left to the next look at the rails
// spawn and despawn
std::vector<TTrack *> m_lanes; // spawn: the lane the vehicles are put on. despawn: lanes the vehicles are taken from
double m_offset{0.0}; // spawn: distance of the point from the start of that lane
std::vector<pooled_vehicle> m_pool;
bool m_prepared{false};
int m_made{0}; // number of vehicles made so far, for their names
double m_timer{0.0}; // time left to the next vehicle
owned_shapes m_shapes;
};
// collection of the road points present in the scene
class roadpoint_table : public basic_table<roadpoint_node>
{
public:
// legacy style initialization, to be performed when the lanes of the roads are tied together
void InitRoadpoints();
// lets go of the lanes and the rails, ahead of a change of the roads, and ties all points anew when it's done
void unbind();
void bind();
// puts geometry of the points in the scene
void create_geometry();
// to be called with each step of the simulation
void update(double const Deltatime);
private:
bool m_geometry{false}; // the scene is ready to take geometry of the points
};
//---------------------------------------------------------------------------