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

fix(roads): lane transitions drawn as road with an island or a closed zone, end faces and rounded island ends, closed zone stripes on curves, banks applied to the whole road; junction banks to the ground and pedestrian crossings

This commit is contained in:
2026-10-04 03:25:32 +02:00
parent 27a94509c4
commit ceae93eec5
5 changed files with 995 additions and 132 deletions

View File

@@ -336,6 +336,8 @@ void editor_mode::junction_select(junction_node *Junction)
copy_text(tool.surface, sizeof(tool.surface), Junction->definition().surface);
copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material);
copy_text(tool.kerbtext, sizeof(tool.kerbtext), Junction->definition().kerbmaterial);
copy_text(tool.banktext, sizeof(tool.banktext), Junction->definition().bankmaterial);
copy_text(tool.mediantext, sizeof(tool.mediantext), Junction->definition().medianmaterial);
}
void editor_mode::roadpoint_select(roadpoint_node *Point)
@@ -771,6 +773,59 @@ void editor_mode::render_junction_layout(junction_node &Junction)
ImGui::Unindent();
}
// banks leading from the corners to the ground
if (ImGui::CollapsingHeader("Banks to the ground"))
{
if (render_road_material("Bank material", tool.banktext, sizeof(tool.banktext), state.bankmaterial))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "What the banks are covered with next to a sidewalk, a kerb or the bare edge.\nA bank of a shoulder is covered with the material of the shoulder.");
ImGui::SetNextItemWidth(120.0f);
if (ImGui::InputFloat("Run for a metre of drop [m]", &tool.bankgrade, 0.25f, 0.5f, "%.2f"))
tool.bankgrade = std::clamp(tool.bankgrade, 0.25f, 10.0f);
if (ImGui::Button("Lead the banks to the ground"))
{
if (changed)
junction_apply(Junction, state);
junction_bank_to_ground(Junction);
return;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Looks for the ground past each corner, at both roads the corner runs between,\n"
"and sets the bank of the corner to reach it. Done for the roads too, the banks meet at the ends of the roads.");
auto banked{false};
for (auto const &arm : state.arms)
banked = banked || arm.bank.set;
if (banked)
{
ImGui::SameLine();
if (ImGui::Button("Remove them"))
{
for (auto &arm : state.arms)
arm.bank = road_node::bank_data{};
changed = true;
}
}
}
if (state.arms.size() == 2)
{
// where a road changes its lanes: what's between its two directions, if the roads keep them apart
ImGui::Separator();
char const *mediantypes[]{"nothing, the ground shows", "surface closed to the traffic", "island"};
int median{state.median == road_node::median_type::gap ? 0 : state.median == road_node::median_type::island ? 2 : 1};
ImGui::SetNextItemWidth(220.0f);
if (ImGui::Combo("Between the directions", &median, mediantypes, 3))
{
state.median = (median == 0 ? road_node::median_type::gap : median == 2 ? road_node::median_type::island : road_node::median_type::painted);
changed = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Shows where a road which keeps its two directions apart meets this stretch:\n"
"what's between the directions is led on to the other road, where it closes or carries on.");
if (state.median == road_node::median_type::island && render_road_material("Island material", tool.mediantext, sizeof(tool.mediantext), state.medianmaterial))
changed = true;
}
// the roads
if (ImGui::CollapsingHeader("Roads of the junction", ImGuiTreeNodeFlags_DefaultOpen))
{
@@ -782,9 +837,30 @@ void editor_mode::render_junction_layout(junction_node &Junction)
ImGui::PushID(static_cast<int>(arm));
auto &data{state.arms[arm]};
ImGui::Text("%d: %d in, %d out, %.2f m a lane", static_cast<int>(arm) + 1, data.incoming, data.outgoing, data.width);
ImGui::SameLine();
if (ImGui::Checkbox("Stop line", &data.stopline))
changed = true;
if (state.arms.size() > 2)
{
ImGui::SameLine();
if (ImGui::Checkbox("Stop line", &data.stopline))
changed = true;
// stripes for the pedestrians, across the whole road where it meets the junction
ImGui::SameLine();
bool zebra{data.crosswalk > 0.f};
if (ImGui::Checkbox("Pedestrian crossing", &zebra))
{
data.crosswalk = (zebra ? 4.f : 0.f);
changed = true;
}
if (zebra)
{
ImGui::SameLine();
ImGui::SetNextItemWidth(90.0f);
if (ImGui::InputFloat("m##crosswalk", &data.crosswalk, 0.5f, 1.0f, "%.1f", ImGuiInputTextFlags_EnterReturnsTrue))
{
data.crosswalk = std::clamp(data.crosswalk, 1.0f, 20.0f);
changed = true;
}
}
}
if (data.incoming > 0 && state.arms.size() > 2)
{
ImGui::Indent();
@@ -1231,10 +1307,31 @@ void editor_mode::road_click()
editor_road::carry_out(plan.end, record);
}
std::vector<road_node::state> states;
// what changes from the start of a piece to its end is spread over the whole stretch being built,
// so the pieces don't each go through all of it
std::vector<double> stations{0.0};
for (auto const &piece : pieces)
stations.emplace_back(stations.back() + editor_road::planar_length(piece));
auto const along = [&stations](std::size_t const Index) {
auto const fraction{stations.back() > 0.0 ? stations[Index] / stations.back() : 0.0};
return static_cast<float>(fraction * fraction * (3.0 - 2.0 * fraction));
};
for (std::size_t index = 0; index < pieces.size(); ++index)
{
auto state{tool.settings};
state.axis = piece;
state.axis = pieces[index];
auto const from{along(index)};
auto const to{along(index + 1)};
auto const &median{tool.settings.median};
state.median.width = {glm::mix(median.width[0], median.width[1], from), glm::mix(median.width[0], median.width[1], to)};
// an island is rounded where the stretch starts and ends
state.median.round = {index == 0 ? median.round[0] : 0.f, index + 1 == pieces.size() ? median.round[1] : 0.f};
for (std::size_t side = 0; side < state.banks.size(); ++side)
{
auto const &bank{tool.settings.banks[side]};
state.banks[side].width = {glm::mix(bank.width[0], bank.width[1], from), glm::mix(bank.width[0], bank.width[1], to)};
state.banks[side].drop = {glm::mix(bank.drop[0], bank.drop[1], from), glm::mix(bank.drop[0], bank.drop[1], to)};
}
states.emplace_back(state);
}
auto const created{editor_road::create(states)};
@@ -1279,6 +1376,8 @@ void editor_mode::road_apply()
auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector<road_node *>{tool.selected}};
editor_road::record record;
std::vector<std::pair<road_node *, road_node::state>> changes;
// the banks are kept for each piece as they are, unless it's the banks which were just changed
auto const banked{tool.settings.banks != tool.selected->definition().banks};
for (auto *road : pieces)
{
std::string reason;
@@ -1295,9 +1394,12 @@ void editor_mode::road_apply()
state.taper = road->definition().taper;
if (road != tool.selected)
{
// and so are the banks, and how far apart the two directions are led
// and so is how far apart the two directions are led, and the rounded ends of what's between them
state.median.width = road->definition().median.width;
state.banks = road->definition().banks;
state.median.round = road->definition().median.round;
// banks led to the ground differ from piece to piece, so a change of something else leaves them alone
if (false == banked)
state.banks = road->definition().banks;
}
if (road != tool.selected && road->definition().changes.size() == state.changes.size())
{
@@ -1417,6 +1519,69 @@ void editor_mode::road_roundabout(glm::dvec3 const &Ground)
tool.status = "Roundabout made. Untick Roundabout and lead roads out of its side, or into it: the traffic joining it gives way to the traffic going around.";
}
// how far from specified point, and how far down, a slope of the grade set for the banks gets to the ground
std::pair<float, float> editor_mode::bank_reach(glm::dvec3 const &Edge, glm::dvec3 const &Outwards)
{
auto const grade{std::clamp(static_cast<double>(m_roadtool.bankgrade), 0.25, 10.0)};
auto const steps{static_cast<int>(kBankReach / kBankStep)};
// the ground further and further from the edge, without the roads themselves
std::vector<glm::dvec3> probes;
for (int step = 1; step <= steps; ++step)
probes.emplace_back(Edge + Outwards * (step * kBankStep));
auto const ground{ground_heights(probes, true)};
// the bank ends where a slope of the set grade, going down or up, gets to the ground
auto const rising{ground.front() > Edge.y};
auto width{kBankReach};
auto drop{Edge.y - ground.back()};
for (int step = 1; step <= steps; ++step)
{
auto const reach{step * kBankStep};
auto const height{ground[step - 1]};
if (rising ? height <= Edge.y + reach / grade : height >= Edge.y - reach / grade)
{
width = reach;
drop = Edge.y - height;
break;
}
}
return {static_cast<float>(width), static_cast<float>(drop)};
}
// leads the banks of the corners of a junction to the ground beside it
void editor_mode::junction_bank_to_ground(junction_node &Junction)
{
auto state{Junction.definition()};
auto const order{state.arm_order()};
auto const sided{state.side.type != road_node::side_type::none && state.side.width > 0.f};
// where the banks start: past the kerb and whatever lines the corners
auto const lining{(state.kerbs ? state.kerbwidth : 0.f) + (sided ? state.side.width : 0.f)};
auto const raised{sided && state.side.type == road_node::side_type::sidewalk ? state.kerbheight : 0.f};
for (std::size_t idx = 0; idx < order.size(); ++idx)
{
// a corner starts at the left edge of a road, seen from the junction, and ends at the right edge of the next one
auto &arm{state.arms[order[idx]]};
auto const &next{state.arms[order[(idx + 1) % order.size()]]};
glm::dvec3 const outwards[2]{glm::dvec3{arm.direction.y, 0.0, -arm.direction.x}, -glm::dvec3{next.direction.y, 0.0, -next.direction.x}};
glm::dvec3 const edges[2]{arm.position + outwards[0] * (0.5 * state.arm_width(order[idx]) + lining) + glm::dvec3{0.0, raised, 0.0},
next.position + outwards[1] * (0.5 * state.arm_width(order[(idx + 1) % order.size()]) + lining) + glm::dvec3{0.0, raised, 0.0}};
arm.bank.set = true;
for (int end = 0; end < 2; ++end)
{
auto const reach{bank_reach(edges[end], outwards[end])};
arm.bank.width[end] = reach.first;
arm.bank.drop[end] = reach.second;
}
}
junction_apply(Junction, state);
auto &tool{m_roadtool};
if (tool.status == "Changed")
{
tool.status = "Banks of the junction led to the ground.";
if ((false == sided || state.side.type != road_node::side_type::shoulder) && (state.bankmaterial.empty() || state.bankmaterial == "none"))
tool.status += " Pick a bank material to have them drawn.";
}
}
// leads the banks of the selected piece, or of the whole road, to the ground beside it
void editor_mode::road_bank_to_ground()
{
@@ -1433,8 +1598,6 @@ void editor_mode::road_bank_to_ground()
return;
}
}
auto const grade{std::clamp(static_cast<double>(tool.bankgrade), 0.25, 10.0)};
auto const steps{static_cast<int>(kBankReach / kBankStep)};
std::vector<std::pair<road_node *, road_node::state>> changes;
for (auto *road : pieces)
{
@@ -1455,29 +1618,10 @@ void editor_mode::road_bank_to_ground()
auto edge{state.point(t) + across * (0.5 * state.span(t) + (state.kerbs[side] ? state.kerbwidth : 0.f) + (sided ? data.width : 0.f))};
if (sided && data.type == road_node::side_type::sidewalk)
edge.y += state.kerbheight;
// the ground further and further from the road, without the roads themselves
std::vector<glm::dvec3> probes;
for (int step = 1; step <= steps; ++step)
probes.emplace_back(edge + across * (step * kBankStep));
auto const ground{ground_heights(probes, true)};
// the bank ends where a slope of the set grade, going down or up, gets to the ground
auto const rising{ground.front() > edge.y};
auto width{kBankReach};
auto drop{edge.y - ground.back()};
for (int step = 1; step <= steps; ++step)
{
auto const reach{step * kBankStep};
auto const height{ground[step - 1]};
if (rising ? height <= edge.y + reach / grade : height >= edge.y - reach / grade)
{
width = reach;
drop = edge.y - height;
break;
}
}
auto const reach{bank_reach(edge, across)};
state.banks[side].set = true;
state.banks[side].width[end] = static_cast<float>(width);
state.banks[side].drop[end] = static_cast<float>(drop);
state.banks[side].width[end] = reach.first;
state.banks[side].drop[end] = reach.second;
}
}
changes.emplace_back(road, state);
@@ -1914,7 +2058,8 @@ bool editor_mode::render_road_layout(road_node::state &State)
if (ImGui::CollapsingHeader("Banks to the ground"))
{
ImGui::TextDisabled("A slope from the edge of the road down, or up, to the ground.");
ImGui::TextDisabled("Set for each piece on its own, at its start and at its end.");
ImGui::TextDisabled("Set here it goes to every piece the changes go to; the button below");
ImGui::TextDisabled("works out each piece on its own.");
char const *banknames[]{"Bank on the left", "Bank on the right"};
for (int side = 0; side < 2; ++side)
{
@@ -1991,6 +2136,13 @@ bool editor_mode::render_road_layout(road_node::state &State)
if (material("Island material", tool.mediantext, sizeof(tool.mediantext), State.median.material))
changed = true;
ImGui::TextDisabled("The island is as high as the kerbs; with a kerb material set it gets kerbs of its own.");
if (number("Rounded at the start [m]", State.median.round[0], 0.5f, "%.2f"))
changed = true;
if (number("Rounded at the end [m]", State.median.round[1], 0.5f, "%.2f"))
changed = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", "Length of the rounded end of the island at that end of the piece; half of the width of the island makes it a half circle.\n"
"0: the island is cut straight, with a face. Use it where the next piece has no island.");
}
}
@@ -2240,9 +2392,17 @@ void editor_mode::render_road_window()
else if (tool.junction != nullptr)
{
auto &junction{*tool.junction};
ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast<int>(junction.definition().arms.size()), static_cast<int>(junction.movements().size()));
ImGui::TextDisabled("The ways through are made for the roads attached at the moment.");
ImGui::TextDisabled("Build tool, LMB on the junction: lead another road out of it");
if (junction.definition().arms.size() == 2)
{
ImGui::Text("%s, a stretch where the road changes its lanes", junction.name().c_str());
ImGui::TextDisabled("It's drawn as a part of the road, and leads the lanes of one side to the lanes of the other.");
}
else
{
ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast<int>(junction.definition().arms.size()), static_cast<int>(junction.movements().size()));
ImGui::TextDisabled("The ways through are made for the roads attached at the moment.");
ImGui::TextDisabled("Build tool, LMB on the junction: lead another road out of it");
}
render_junction_layout(junction);
if (ImGui::Button("Delete (Del)"))
road_delete();