From 1d4c448e30cef3abd36ceadb62af5b7808f3bb61 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 3 Oct 2026 21:20:00 +0200 Subject: [PATCH] feat(roads): junctions made by leading a road out of another, width and height set at road points --- application/editormode.h | 53 ++- application/editormoderoad.cpp | 800 +++++++++++++++++++++------------ editor/editorRoad.cpp | 415 +++++++++++++++-- editor/editorRoad.hpp | 59 ++- world/Road.cpp | 106 +++-- world/Road.h | 9 +- 6 files changed, 1059 insertions(+), 383 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 12562559..b2352179 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -107,11 +107,7 @@ class editor_mode : public application_mode std::vector> tracks; std::vector created; std::vector removed; - std::vector> roads; // road pieces with what they were like before the change - std::vector roads_created; - std::vector roads_removed; - std::vector junctions_created; - std::vector junctions_removed; + editor_road::record roads; // what a change of the roads comes to }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -489,13 +485,25 @@ class editor_mode : public application_mode bool m_track_drag_connected{true}; // roads: a menu and a window of their own, independent from the edit modes of the toolset + // what a click of the build tool would make + struct road_plan + { + std::vector pieces; // axes of the pieces, in the order they're laid + editor_road::loose_end snap; // loose end of a road or arm of a junction the road is led to + editor_road::branch start; // junction made, or given another arm, for a road led out of the side of a road or out of a junction + bool hasstart{false}; + editor_road::branch end; // the same for a road led into the side of a road or into a junction + bool hasend{false}; + std::string error; // why there's nothing to make + }; struct road_tool { 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, 2: junction + int tool{0}; // 0: select, 1: build road_node *selected{nullptr}; junction_node *junction{nullptr}; // selected junction + std::vector points; // selected points where the pieces meet or end road_node::state settings; // layout shown in the window: of the selected piece, or of the pieces about to be built bool whole{true}; // layout changes go to every piece of the road the selected one belongs to bool chain{false}; // the point to build from is set @@ -505,16 +513,17 @@ class editor_mode : public application_mode bool reversed{false}; // the pieces are laid from their ends, to carry on a road from its start double grade{0.0}; // slope the next piece has to start with, to carry on what it's attached to bool hasgrade{false}; - float offset{0.1f}; // height above the ground the roads and junctions are built at + // the road being built leaves the side of a road, or a junction between its roads. the point to build from is the centre + // of the junction, which gets made, or gets another arm, along with the first piece + road_node *branchroad{nullptr}; + double branchat{0.0}; // value of the curve parameter of the axis of that road + junction_node *branchjunction{nullptr}; + float offset{0.1f}; // height above the ground the roads are built at bool follow{true}; // built roads are led over the ground between the clicked points - int kind{0}; // kind of the junctions being placed, an editor_road::junction_kind - float crossingspeed{30.f}; // speed limit on the junctions being placed - // what a click would make at the moment, worked out once a frame for display - std::vector preview; - junction_node::state previewjunction; - bool hasjunction{false}; - editor_road::loose_end previewsnap; - std::string previewerror; + float crossingspeed{30.f}; // speed limit on the junctions being made + 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}; glm::dvec3 mouse{0.0}; std::string status; char surface[128]{}; @@ -529,19 +538,21 @@ class editor_mode : public application_mode void road_click(); void road_select(road_node *Road); void junction_select(junction_node *Junction); - void junction_click(); - bool junction_preview(junction_node::state &State, editor_road::loose_end &Snap, std::string &Error) const; void road_apply(); bool road_delete(); bool road_split(); void road_cancel(); + // sets the width of the road, or its height, at the selected points + void road_point_width(float const Width); + void road_point_height(double const Height); + bool road_points_apply(std::vector> const &Changes, std::size_t const Skipped); // Fresh: the ground is looked up anew instead of going by what was gathered for the previous frames - std::vector road_preview(editor_road::loose_end &Snap, std::string &Error, bool const Fresh); - void road_profile(std::vector &Pieces, editor_road::loose_end const &Snap, bool const Fresh); + road_plan road_preview(bool const Fresh); + // Firstgrade, Lastgrade: slopes the road has to start and end with, nullptr for none + void road_profile(std::vector &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh); // height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height std::vector ground_heights(std::vector const &Points, bool const Fresh); - void push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed, std::vector Junctionscreated = {}, - std::vector Junctionsremoved = {}); + void push_road_snapshot(editor_road::record Record); void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); road_tool m_roadtool; }; diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 2dcba223..09427d7f 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -107,6 +107,10 @@ std::string lower_case(std::string Text) } double const kSnapRadius{4.0}; // an end of a road this close to the cursor is what the click is taken to mean +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 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 double const kLongestStretch{2000.0}; // a single click doesn't lead a road further than this @@ -119,11 +123,41 @@ ImU32 const kForwardColor{IM_COL32(70, 230, 110, 230)}; ImU32 const kBackwardColor{IM_COL32(255, 170, 50, 230)}; ImU32 const kTurnColor{IM_COL32(210, 210, 210, 150)}; ImU32 const kWayColor{IM_COL32(120, 200, 255, 170)}; +ImU32 const kPointColor{IM_COL32(255, 255, 255, 200)}; 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)}; +// gathers new definitions for the pieces which meet or end at provided points. Change: receives a definition, and which end of it is at the point. +// Skipped: receives the number of points left out for being at a junction, which is made for the road the way it is +template +std::vector> point_changes(std::vector const &Points, std::size_t &Skipped, Change_ Change) +{ + std::vector> changes; + Skipped = 0; + for (auto const &point : Points) + { + if (editor_road::at_junction(point)) + { + ++Skipped; + continue; + } + for (auto const &end : editor_road::ends_at(point)) + { + // a piece with both its ends among the points gets both changed + auto entry{std::find_if(changes.begin(), changes.end(), [&end](std::pair const &Listed) { return Listed.first == end.road; })}; + if (entry == changes.end()) + { + changes.emplace_back(end.road, end.road->definition()); + entry = std::prev(changes.end()); + } + Change(entry->second, end.atend); + } + } + return changes; +} + } void editor_mode::render_road_menu() @@ -143,10 +177,17 @@ void editor_mode::update_road_tool() tool.selected = nullptr; if (tool.junction != nullptr && tool.junction->m_editorremoved) tool.junction = nullptr; - tool.preview.clear(); - tool.hasjunction = false; - tool.previewsnap = {}; - tool.previewerror.clear(); + 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 + tool.branchroad = nullptr; + tool.branchjunction = nullptr; + tool.chain = false; + } + // so are the points whose pieces were removed or changed behind the back of the selection + 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.chain = false; @@ -156,14 +197,30 @@ void editor_mode::update_road_tool() tool.mouse = camera + GfxRenderer->Mouse_Position(); if (glm::distance(tool.mouse, camera) > kReach) { - tool.previewerror = "The cursor isn't over anything near enough"; + tool.plan.error = "The cursor isn't over anything near enough"; return; } - // what a click would make at the moment, to be drawn over the view - if (tool.tool == 1 && tool.chain) - tool.preview = road_preview(tool.previewsnap, tool.previewerror, false); - else if (tool.tool == 2) - tool.hasjunction = junction_preview(tool.previewjunction, tool.previewsnap, tool.previewerror); + if (tool.tool != 1) + return; + if (tool.chain) + { + // what a click would make at the moment, to be drawn over the view + tool.plan = road_preview(false); + return; + } + // where a click would lead a road out of the side of a road, or out of a junction + if (editor_road::find_end(tool.mouse, kSnapRadius).valid()) + return; + if (auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; road != nullptr) + { + tool.hover = road->definition().point(editor_road::nearest_parameter(road->definition(), tool.mouse)); + tool.hashover = true; + } + else if (auto const *junction{editor_road::nearest_junction(tool.mouse)}; junction != nullptr) + { + tool.hover = junction->definition().centre; + tool.hashover = true; + } } void editor_mode::road_select(road_node *Road) @@ -194,71 +251,162 @@ void editor_mode::junction_select(junction_node *Junction) std::string reason; tool.status = "Selected: " + Junction->name(); if (false == editor_road::can_edit(*Junction, &reason)) - tool.status += ". It can't be removed: " + reason; + tool.status += ". It can't be changed: " + reason; } void editor_mode::road_cancel() { - m_roadtool.chain = false; + auto &tool{m_roadtool}; + tool.chain = false; + tool.branchroad = nullptr; + tool.branchjunction = nullptr; } -std::vector editor_mode::road_preview(editor_road::loose_end &Snap, std::string &Error, bool const Fresh) +// works out what a click of the build tool would make with the cursor where it is +editor_mode::road_plan editor_mode::road_preview(bool const Fresh) { auto const &tool{m_roadtool}; - Snap = editor_road::find_end(tool.mouse, kSnapRadius); + auto const &layout{tool.settings}; + road_plan plan; + // lanes of the road being built which lead to its far end, and the ones leading back from there + auto const arriving{tool.reversed ? layout.backward : layout.forward}; + auto const leaving{tool.reversed ? layout.forward : layout.backward}; + float lanewidth{0.f}; + auto const evenlanes{editor_road::lane_width(layout, 0.0, lanewidth)}; + char const *unevenlanes{"Lanes of the road being built differ in width, and a junction takes roads with lanes of the same width"}; + + // where the road is led to. a loose end of a road or an arm of a junction near the cursor comes first + plan.snap = editor_road::find_end(tool.mouse, kSnapRadius); // the end the road is being drawn from isn't somewhere to lead it to - if (Snap.valid() && glm::distance(Snap.position, tool.point) < 0.5) - Snap = {}; + if (plan.snap.valid() && glm::distance(plan.snap.position, tool.point) < 0.5) + plan.snap = {}; + auto target{tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}}; glm::dvec2 enddirection{0.0, 1.0}; - if (Snap.valid()) + double endgrade{0.0}; + bool tied{false}; // the road has to arrive at the target in set direction, with set slope + if (plan.snap.valid()) { - glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z}; + glm::dvec2 const outwards{plan.snap.outwards.x, plan.snap.outwards.z}; if (glm::length(outwards) > 1e-6) - enddirection = -glm::normalize(outwards); - else - Snap = {}; - } - // in the open the road is put over what's under the cursor - auto const target{Snap.valid() ? Snap.position : tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}}; - if (glm::distance(tool.point, target) > kLongestStretch) - { - Error = "Too far from the last point"; - return {}; - } - if (Snap.junction != nullptr) - { - // the junction has its ways through made for a set of lanes at each arm - auto const arriving{tool.reversed ? tool.settings.backward : tool.settings.forward}; - auto const leaving{tool.reversed ? tool.settings.forward : tool.settings.backward}; - if (arriving != Snap.backward || leaving != Snap.forward || std::abs(tool.settings.lanewidth - Snap.width) > 0.01f) { - char width[32]; - std::snprintf(width, sizeof(width), "%.2f", Snap.width); - Error = "This arm of the junction takes a road with " + std::to_string(Snap.backward) + " lane(s) leading into it and " + std::to_string(Snap.forward) + " out of it, " + width + " m wide each"; - return {}; + target = plan.snap.position; + enddirection = -glm::normalize(outwards); + endgrade = -plan.snap.outwards.y / glm::length(outwards); + tied = true; + } + else + { + plan.snap = {}; } } - auto pieces{editor_road::plan(tool.point, tool.hasdirection ? &tool.direction : nullptr, target, Snap.valid() ? &enddirection : nullptr, Global.ctrlState, Error)}; - road_profile(pieces, Snap, Fresh); - return pieces; + if (plan.snap.junction != nullptr) + { + // the junction has its ways through made for a set of lanes at each arm + if (false == evenlanes || arriving != plan.snap.backward || leaving != plan.snap.forward || std::abs(lanewidth - plan.snap.width) > 0.01f) + { + char width[32]; + std::snprintf(width, sizeof(width), "%.2f", plan.snap.width); + plan.error = "This arm of the junction takes a road with " + std::to_string(plan.snap.backward) + " lane(s) leading into it and " + std::to_string(plan.snap.forward) + " out of it, " + width + " m wide each"; + return plan; + } + } + else if (false == plan.snap.valid() && glm::distance(tool.mouse, tool.point) > kJoinClearance) + { + // then the side of a road, or a junction between its roads: the road is taken in with a junction made there, or with another arm of the one which is there + auto *road{editor_road::nearest(tool.mouse, kSideMargin)}; + auto *junction{road == nullptr ? editor_road::nearest_junction(tool.mouse) : nullptr}; + if (road != nullptr && road == tool.branchroad) + road = nullptr; + if (junction != nullptr && junction == tool.branchjunction) + junction = nullptr; + if (road != nullptr || junction != nullptr) + { + if (false == evenlanes) + { + plan.error = unevenlanes; + return plan; + } + auto const t{road != nullptr ? editor_road::nearest_parameter(road->definition(), tool.mouse) : 0.0}; + auto const centre{road != nullptr ? road->definition().point(t) : junction->definition().centre}; + // seen from the junction the road comes from where it's being led from + glm::dvec2 const from{tool.point.x - centre.x, tool.point.z - centre.z}; + if (false == (road != nullptr ? editor_road::branch_from(*road, t, from, arriving, leaving, lanewidth, plan.end, plan.error) : editor_road::branch_from(*junction, from, arriving, leaving, lanewidth, plan.end, plan.error))) + return plan; + plan.hasend = true; + auto const &arm{plan.end.layout.arms[plan.end.arm]}; + target = arm.position; + enddirection = -arm.direction; + tied = true; + } + } + + // where the road starts + auto start{tool.point}; + auto direction{tool.direction}; + auto hasdirection{tool.hasdirection}; + auto startgrade{tool.grade}; + auto hasgrade{tool.hasgrade}; + if (tool.branchroad != nullptr || tool.branchjunction != nullptr) + { + // out of the side of a road, or out of a junction, in the direction of where it's led to + if (false == evenlanes) + { + plan.error = unevenlanes; + return plan; + } + glm::dvec2 const away{target.x - tool.point.x, target.z - tool.point.z}; + if (glm::length(away) < 1.0) + { + plan.error = "Move the cursor to where the road is to go"; + return plan; + } + // the axis of the road goes away from the junction, so the lanes going against it are the ones leading into the junction + if (false == (tool.branchroad != nullptr ? editor_road::branch_from(*tool.branchroad, tool.branchat, away, layout.backward, layout.forward, lanewidth, plan.start, plan.error) : + editor_road::branch_from(*tool.branchjunction, away, layout.backward, layout.forward, lanewidth, plan.start, plan.error))) + return plan; + plan.hasstart = true; + auto const &arm{plan.start.layout.arms[plan.start.arm]}; + start = arm.position; + direction = arm.direction; + hasdirection = true; + startgrade = 0.0; + hasgrade = true; + } + if (plan.hasstart && plan.hasend) + { + // the two junctions are laid out for the roads as they are now, which stops being true for the second once the first is made + bool shared{plan.start.junction != nullptr && plan.start.junction == plan.end.junction}; + auto const touches = [](editor_road::branch const &Branch, road_node const *Road) { + return std::any_of(Branch.changes.begin(), Branch.changes.end(), [Road](std::pair const &Change) { return Change.first == Road; }) || + std::find(Branch.removed.begin(), Branch.removed.end(), Road) != Branch.removed.end(); + }; + for (auto const &change : plan.start.changes) + shared = shared || touches(plan.end, change.first); + for (auto const *road : plan.start.removed) + shared = shared || touches(plan.end, road); + if (shared) + { + plan.error = "The junctions at both ends would take the same piece of road; lead the road somewhere else first"; + return plan; + } + } + if (glm::distance(start, target) > kLongestStretch) + { + plan.error = "Too far from the last point"; + return plan; + } + plan.pieces = editor_road::plan(start, hasdirection ? &direction : nullptr, target, tied ? &enddirection : nullptr, Global.ctrlState, plan.error); + road_profile(plan.pieces, hasgrade ? &startgrade : nullptr, tied ? &endgrade : nullptr, Fresh); + return plan; } // gives the pieces laid out by the planner their heights: over the ground if the road is to follow it, with the slopes // of the pieces matched where they meet, and with what the road is attached to -void editor_mode::road_profile(std::vector &Pieces, editor_road::loose_end const &Snap, bool const Fresh) +void editor_mode::road_profile(std::vector &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh) { auto const &tool{m_roadtool}; if (Pieces.empty()) return; - // an end of a road or an arm of a junction is reached with the slope it has - double endgrade{0.0}; - if (Snap.valid()) - { - auto const run{glm::length(glm::dvec2{Snap.outwards.x, Snap.outwards.z})}; - endgrade = (run > 1e-6 ? -Snap.outwards.y / run : 0.0); - } - auto const *firstgrade{tool.hasgrade ? &tool.grade : nullptr}; - auto const *lastgrade{Snap.valid() ? &endgrade : nullptr}; if (false == tool.follow) { // straight from a point to the next one, the way a ramp or a viaduct goes @@ -266,7 +414,7 @@ void editor_mode::road_profile(std::vector &Pieces, editor_road::l for (auto const &piece : Pieces) heights.emplace_back(piece.points[segment_data::point::start].y); heights.emplace_back(Pieces.back().points[segment_data::point::end].y); - editor_road::profile(Pieces, heights, firstgrade, lastgrade); + editor_road::profile(Pieces, heights, Firstgrade, Lastgrade); return; } auto const planned{Pieces}; @@ -294,13 +442,13 @@ void editor_mode::road_profile(std::vector &Pieces, editor_road::l // the ends are where they were put heights.front() = Pieces.front().points[segment_data::point::start].y; heights.back() = Pieces.back().points[segment_data::point::end].y; - editor_road::profile(Pieces, heights, firstgrade, lastgrade); + editor_road::profile(Pieces, heights, Firstgrade, Lastgrade); // shorter pieces are called for if the ground shows through the road, or the road hangs over it. // the pieces attached to something are let off, as they have to get to its height one way or another bool fits{true}; for (std::size_t i = 0; i < count; ++i) { - if ((i == 0 && firstgrade != nullptr) || (i + 1 == count && lastgrade != nullptr)) + if ((i == 0 && Firstgrade != nullptr) || (i + 1 == count && Lastgrade != nullptr)) continue; auto const over{path_point(Pieces[i], 0.5).y - ground[count + 1 + i]}; if (over < -kSunkTolerance || over > kRaisedTolerance) @@ -314,110 +462,6 @@ void editor_mode::road_profile(std::vector &Pieces, editor_road::l } } -// lays out the junction a click would make. returns: false if there's none to make, with the reason in Error unless -// it's a matter of the first click being still ahead -bool editor_mode::junction_preview(junction_node::state &State, editor_road::loose_end &Snap, std::string &Error) const -{ - auto const &tool{m_roadtool}; - auto const kind{static_cast(std::clamp(tool.kind, 0, 3))}; - Snap = {}; - junction_node::state state; - // the look goes after the road the junction is made for - auto const dress = [&state](road_node::state const &Layout) { - state.surface = Layout.surface; - state.texturelength = Layout.texturelength; - state.markings = Layout.markings; - state.friction = Layout.friction; - state.sounddistance = Layout.sounddistance; - state.quality = Layout.quality; - state.environment = Layout.environment; - }; - if (false == tool.chain) - { - // at a loose end of a road the junction is made for that road, and put right past its end - Snap = editor_road::find_end(tool.mouse, kSnapRadius, false); - if (Snap.road == nullptr) - return false; - auto const &layout{Snap.road->definition()}; - glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z}; - if (glm::length(outwards) < 1e-6 || layout.lanes.empty()) - { - Error = "There's no telling which way this road goes at its end"; - return false; - } - // the ways through the junction start and end where the lanes of the road are expected, which takes lanes of the same width - auto const width{layout.lanes.front().width}; - for (auto const &lane : layout.lanes) - { - if (std::abs(lane.width - width) > 0.01f) - { - Error = "Lanes of this road differ in width, and a junction takes roads with lanes of the same width"; - return false; - } - } - state = editor_road::junction(kind, Snap.position, glm::normalize(outwards), Snap.atend ? layout.forward : layout.backward, Snap.atend ? layout.backward : layout.forward, width); - dress(layout); - } - else - { - // in the open the centre is set already, and the cursor shows where the first road comes from - glm::dvec2 const away{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; - if (glm::length(away) < 1.0) - { - Error = "Move the cursor away from the centre to turn the junction"; - return false; - } - auto const heading{-glm::normalize(away)}; - auto const &layout{tool.settings}; - auto const reach{editor_road::junction_reach(layout.forward + layout.backward, layout.lanewidth)}; - glm::dvec3 const entry{tool.point.x - heading.x * reach, tool.point.y, tool.point.z - heading.y * reach}; - state = editor_road::junction(kind, entry, heading, layout.forward, layout.backward, layout.lanewidth); - dress(layout); - } - state.velocity = tool.crossingspeed; - state.normalize(); - State = state; - return true; -} - -void editor_mode::junction_click() -{ - auto &tool{m_roadtool}; - junction_node::state state; - editor_road::loose_end snap; - std::string error; - if (false == junction_preview(state, snap, error)) - { - if (false == error.empty()) - { - tool.status = "Can't make the junction: " + error; - } - else if (false == tool.chain) - { - // away from the roads the first click sets the centre, the second one turns the junction - tool.point = tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}; - tool.chain = true; - tool.status = "Centre set"; - } - return; - } - std::string reason; - if (snap.valid() && false == editor_road::can_join(snap, &reason)) - { - tool.status = "Can't make the junction here: " + reason; - return; - } - auto const created{editor_road::create(std::vector{state})}; - if (created.empty()) - { - tool.status = "The junction wasn't created"; - return; - } - push_road_snapshot({}, {}, {}, created, {}); - tool.chain = false; - tool.status = "\"" + created.front()->name() + "\" made" + (snap.road != nullptr ? " at the end of \"" + snap.road->name() + "\"" : std::string{}) + ". Lead roads out of its arms with the Build tool"; -} - void editor_mode::road_click() { auto &tool{m_roadtool}; @@ -431,6 +475,23 @@ void editor_mode::road_click() tool.mouse = ground; if (tool.tool == 0) { + glm::dvec3 joint; + if (editor_road::nearest_joint(ground, kPointRadius, joint)) + { + // a point where pieces meet or end. with Shift it's added to the selected ones, or taken out of them + auto const listed{std::find_if(tool.points.begin(), tool.points.end(), [&joint](glm::dvec3 const &Point) { return glm::distance(Point, joint) < 0.25; })}; + if (false == Global.shiftState) + tool.points.assign(1, joint); + else if (listed != tool.points.end()) + tool.points.erase(listed); + else + tool.points.emplace_back(joint); + tool.selected = nullptr; + tool.junction = nullptr; + tool.status = std::to_string(tool.points.size()) + (tool.points.size() == 1 ? " point selected" : " points selected"); + return; + } + tool.points.clear(); auto *road{editor_road::nearest(ground, 1.5)}; auto *junction{road == nullptr ? editor_road::nearest_junction(ground) : nullptr}; if (road == nullptr && junction == nullptr) @@ -446,19 +507,19 @@ void editor_mode::road_click() tool.status = reason; return; } - if (tool.tool == 2) - { - junction_click(); - return; - } if (false == tool.chain) { - // first click sets where the road starts. started from a loose end of a road it carries that road on, - // started from an arm of a junction it's the road the junction is made for there + // first click sets where the road starts + tool.branchroad = nullptr; + tool.branchjunction = nullptr; + tool.selected = nullptr; + tool.junction = nullptr; + tool.points.clear(); auto const end{editor_road::find_end(ground, kSnapRadius)}; glm::dvec2 const outwards{end.outwards.x, end.outwards.z}; if (end.valid() && glm::length(outwards) > 1e-6) { + // started from a loose end of a road it carries that road on, started from an arm of a junction it's the road the junction is made for there if (false == editor_road::can_join(end, &reason)) { tool.status = "Can't build from here: " + reason; @@ -469,12 +530,14 @@ void editor_mode::road_click() tool.hasdirection = true; tool.grade = end.outwards.y / glm::length(outwards); tool.hasgrade = true; - tool.junction = nullptr; if (end.road != nullptr) { // carried on from its start the road keeps the direction of its axis, so the lanes stay what they were tool.reversed = (false == end.atend); tool.settings = end.road->definition(); + // and it stays as wide as it is at that end + auto const scale{tool.settings.taper[end.atend ? 1 : 0]}; + tool.settings.taper = {scale, scale}; tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; } else @@ -483,6 +546,7 @@ void editor_mode::road_click() tool.settings.forward = end.forward; tool.settings.backward = end.backward; tool.settings.lanewidth = end.width; + tool.settings.taper = {1.f, 1.f}; tool.settings.lanes.clear(); tool.settings.changes.clear(); tool.settings.normalize(); @@ -492,31 +556,52 @@ void editor_mode::road_click() } else { - tool.point = ground + glm::dvec3{0.0, tool.offset, 0.0}; tool.hasdirection = false; tool.hasgrade = false; tool.reversed = false; - tool.selected = nullptr; - tool.junction = nullptr; - tool.status = "Start set"; + tool.settings.taper = {1.f, 1.f}; + if (auto *road{editor_road::nearest(ground, kSideMargin)}; road != nullptr) + { + // started from the side of a road it leaves that road at a junction, made along with its first piece + tool.branchroad = road; + tool.branchat = editor_road::nearest_parameter(road->definition(), ground); + tool.point = road->definition().point(tool.branchat); + tool.status = "Leading a road out of the side of \"" + road->name() + "\": click where it goes"; + } + else if (auto *junction{editor_road::nearest_junction(ground)}; junction != nullptr) + { + // started from a junction it's another road of that junction + if (junction->definition().arms.size() >= 4) + { + tool.status = "\"" + junction->name() + "\" has four roads already"; + return; + } + tool.branchjunction = junction; + tool.point = junction->definition().centre; + tool.status = "Leading another road out of \"" + junction->name() + "\": click where it goes"; + } + else + { + tool.point = ground + glm::dvec3{0.0, tool.offset, 0.0}; + tool.status = "Start set"; + } } tool.chain = true; return; } - editor_road::loose_end snap; - std::string error; - auto pieces{road_preview(snap, error, true)}; - if (pieces.empty()) + auto plan{road_preview(true)}; + if (plan.pieces.empty()) { - tool.status = "Can't build this: " + error; + tool.status = "Can't build this: " + plan.error; return; } - if (snap.valid() && false == editor_road::can_join(snap, &reason)) + if (plan.snap.valid() && false == editor_road::can_join(plan.snap, &reason)) { tool.status = "Can't join here: " + reason; return; } + auto pieces{plan.pieces}; // where the next piece is going to carry on from auto const &last{pieces.back()}; auto const endpoint{last.points[segment_data::point::end]}; @@ -531,6 +616,20 @@ void editor_mode::road_click() } std::reverse(pieces.begin(), pieces.end()); } + // the junctions the road starts or ends with are made first, so the road finds them when it's made + editor_road::record record; + if (plan.hasstart) + { + if (plan.start.junction == nullptr) + plan.start.layout.velocity = tool.crossingspeed; + editor_road::carry_out(plan.start, record); + } + if (plan.hasend) + { + if (plan.end.junction == nullptr) + plan.end.layout.velocity = tool.crossingspeed; + editor_road::carry_out(plan.end, record); + } std::vector states; for (auto const &piece : pieces) { @@ -539,16 +638,22 @@ void editor_mode::road_click() states.emplace_back(state); } auto const created{editor_road::create(states)}; + record.roads_created.insert(record.roads_created.end(), created.begin(), created.end()); + push_road_snapshot(std::move(record)); + tool.branchroad = nullptr; + tool.branchjunction = nullptr; if (created.empty()) { + tool.chain = false; tool.status = "The road wasn't created"; return; } - push_road_snapshot({}, created, {}); - if (snap.valid()) + if (plan.snap.valid() || plan.hasend) { tool.chain = false; - tool.status = "Joined with \"" + (snap.road != nullptr ? snap.road->name() : snap.junction->name()) + "\""; + tool.status = (plan.snap.valid() ? "Joined with \"" + (plan.snap.road != nullptr ? plan.snap.road->name() : plan.snap.junction->name()) + "\"" : + plan.end.junction != nullptr ? "Led into \"" + plan.end.junction->name() + "\" as another of its roads" : + std::string{"Led into the side of the road, with a junction made there"}); return; } glm::dvec2 const planar{tangent.x, tangent.z}; @@ -561,6 +666,8 @@ void editor_mode::road_click() tool.grade = grade; tool.hasgrade = true; tool.status = std::to_string(created.size()) + (created.size() == 1 ? " piece added" : " pieces added"); + if (plan.hasstart) + tool.status += (plan.start.junction != nullptr ? ", as another road of the junction" : ", with a junction where the road leaves"); } void editor_mode::road_apply() @@ -570,14 +677,13 @@ void editor_mode::road_apply() return; tool.settings.normalize(); auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; - std::vector> before; + editor_road::record record; std::vector> changes; for (auto *road : pieces) { std::string reason; if (false == editor_road::can_edit(*road, &reason)) { - tool.status = "\"" + road->name() + "\" can't be changed: " + reason; // what the window shows goes back to what the road is like road_select(tool.selected); tool.status = "\"" + road->name() + "\" can't be changed: " + reason; @@ -585,16 +691,18 @@ void editor_mode::road_apply() } auto state{tool.settings}; state.axis = road->definition().axis; + // how wide the road is at its points is set at these points, for each piece on its own + state.taper = road->definition().taper; if (road != tool.selected && road->definition().changes.size() == state.changes.size()) { // where the lanes can be changed is set for each piece on its own state.changes = road->definition().changes; } - before.emplace_back(road, road->definition()); + record.roads.emplace_back(road, road->definition()); changes.emplace_back(road, state); } editor_road::apply(changes); - push_road_snapshot(before, {}, {}); + push_road_snapshot(std::move(record)); tool.settings = tool.selected->definition(); tool.status = (pieces.size() == 1 ? std::string{"Changed"} : "Changed " + std::to_string(pieces.size()) + " pieces"); } @@ -603,8 +711,12 @@ bool editor_mode::road_delete() { auto &tool{m_roadtool}; if (false == tool.window || (tool.selected == nullptr && tool.junction == nullptr)) - return false; + { + // with points selected the key isn't meant for whatever else is selected in the editor + return tool.window && false == tool.points.empty(); + } std::string reason; + editor_road::record record; if (tool.junction != nullptr) { auto *junction{tool.junction}; @@ -614,7 +726,8 @@ bool editor_mode::road_delete() return true; } editor_road::remove(std::vector{junction}); - push_road_snapshot({}, {}, {}, {}, {junction}); + record.junctions_removed.emplace_back(junction); + push_road_snapshot(std::move(record)); tool.junction = nullptr; tool.status = "\"" + junction->name() + "\" deleted, Ctrl+Z brings it back"; return true; @@ -626,7 +739,8 @@ bool editor_mode::road_delete() return true; } editor_road::remove(std::vector{road}); - push_road_snapshot({}, {}, {road}); + record.roads_removed.emplace_back(road); + push_road_snapshot(std::move(record)); tool.selected = nullptr; tool.status = "\"" + road->name() + "\" deleted, Ctrl+Z brings it back"; return true; @@ -644,23 +758,80 @@ bool editor_mode::road_split() tool.status = "Can't split: " + reason; return true; } - auto const before{road->definition()}; - auto *created{editor_road::split(*road, editor_road::nearest_parameter(before, tool.mouse))}; + editor_road::record record; + record.roads.emplace_back(road, road->definition()); + auto *created{editor_road::split(*road, editor_road::nearest_parameter(road->definition(), tool.mouse))}; if (created == nullptr) { tool.status = "Can't split this close to the end"; return true; } - push_road_snapshot({{road, before}}, {created}, {}); + record.roads_created.emplace_back(created); + push_road_snapshot(std::move(record)); road_select(road); - tool.status = "Split in two: \"" + road->name() + "\" and \"" + created->name() + "\""; + tool.status = "Split in two: \"" + road->name() + "\" and \"" + created->name() + "\". Click the point between them to set the width or the height of the road there"; return true; } -void editor_mode::push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed, std::vector Junctionscreated, - std::vector Junctionsremoved) +// makes the changes gathered for the selected points. returns: true if they were made +bool editor_mode::road_points_apply(std::vector> const &Changes, std::size_t const Skipped) { - if (States.empty() && Created.empty() && Removed.empty() && Junctionscreated.empty() && Junctionsremoved.empty()) + auto &tool{m_roadtool}; + std::string const leftalone{Skipped > 0 ? " The points at junctions were left alone, as a junction is made for the road the way it is." : ""}; + if (Changes.empty()) + { + tool.status = "Nothing was changed." + leftalone; + return false; + } + editor_road::record record; + for (auto const &change : Changes) + { + std::string reason; + if (false == editor_road::can_edit(*change.first, &reason)) + { + tool.status = "\"" + change.first->name() + "\" can't be changed: " + reason; + return false; + } + record.roads.emplace_back(change.first, change.first->definition()); + } + editor_road::apply(Changes); + push_road_snapshot(std::move(record)); + tool.status = (Changes.size() == 1 ? std::string{"Changed 1 piece."} : "Changed " + std::to_string(Changes.size()) + " pieces.") + leftalone; + return true; +} + +void editor_mode::road_point_width(float const Width) +{ + std::size_t skipped{0}; + // the pieces on both sides of a point get to that width there, and from there go back to their own along their length + auto const changes{point_changes(m_roadtool.points, skipped, [Width](road_node::state &State, bool const Atend) { + auto const width{State.width()}; + if (width > 0.0) + State.taper[Atend ? 1 : 0] = std::clamp(static_cast(Width / width), 0.25f, 4.f); + })}; + road_points_apply(changes, skipped); +} + +void editor_mode::road_point_height(double const Height) +{ + auto &tool{m_roadtool}; + std::size_t skipped{0}; + // the control points are kept relative to the ends, so the slopes the pieces have at the point stay + auto const changes{point_changes(tool.points, skipped, [Height](road_node::state &State, bool const Atend) { State.axis.points[Atend ? segment_data::point::end : segment_data::point::start].y = Height; })}; + // the selection follows what it's made of + std::vector moved; + for (auto const &point : tool.points) + moved.emplace_back(false == editor_road::at_junction(point)); + if (false == road_points_apply(changes, skipped)) + return; + for (std::size_t i = 0; i < tool.points.size(); ++i) + if (moved[i]) + tool.points[i].y = Height; +} + +void editor_mode::push_road_snapshot(editor_road::record Record) +{ + if (Record.empty()) return; if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) @@ -668,59 +839,67 @@ void editor_mode::push_road_snapshot(std::vectorname(); snap.position = node->location(); - snap.roads = std::move(States); - snap.roads_created = std::move(Created); - snap.roads_removed = std::move(Removed); - snap.junctions_created = std::move(Junctionscreated); - snap.junctions_removed = std::move(Junctionsremoved); + snap.roads = std::move(Record); m_history.push_back(std::move(snap)); g_redo.clear(); } void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo) { - if (Snapshot.roads.empty() && Snapshot.roads_created.empty() && Snapshot.roads_removed.empty() && Snapshot.junctions_created.empty() && Snapshot.junctions_removed.empty()) + auto const &record{Snapshot.roads}; + if (record.empty()) return; // what the roads are like now is what the opposite operation brings back EditorSnapshot current{Snapshot}; - current.roads.clear(); - for (auto const &entry : Snapshot.roads) - current.roads.emplace_back(entry.first, entry.first->definition()); + for (auto &entry : current.roads.roads) + entry.second = entry.first->definition(); + for (auto &entry : current.roads.junctions) + 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 if (Undo) { - editor_road::remove(Snapshot.roads_created); - editor_road::remove(Snapshot.junctions_created); - editor_road::revive(Snapshot.junctions_removed); - editor_road::revive(Snapshot.roads_removed); + editor_road::remove(record.roads_created); + editor_road::remove(record.junctions_created); } else { - editor_road::revive(Snapshot.roads_created); - editor_road::revive(Snapshot.junctions_created); - editor_road::remove(Snapshot.junctions_removed); - editor_road::remove(Snapshot.roads_removed); + editor_road::remove(record.roads_removed); + editor_road::remove(record.junctions_removed); + } + editor_road::apply(record.roads); + editor_road::apply(record.junctions); + if (Undo) + { + editor_road::revive(record.roads_removed); + editor_road::revive(record.junctions_removed); + } + else + { + editor_road::revive(record.roads_created); + editor_road::revive(record.junctions_created); } - editor_road::apply(Snapshot.roads); auto &tool{m_roadtool}; - tool.chain = false; + road_cancel(); if (tool.selected != nullptr) road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); if (tool.junction != nullptr && tool.junction->m_editorremoved) @@ -949,68 +1128,98 @@ void editor_mode::render_road_window() ImGui::RadioButton("Select", &tool.tool, 0); ImGui::SameLine(); ImGui::RadioButton("Build", &tool.tool, 1); - ImGui::SameLine(); - ImGui::RadioButton("Junction", &tool.tool, 2); if (tool.tool != previoustool) { - tool.chain = false; + road_cancel(); if (tool.tool != 0) { tool.selected = nullptr; tool.junction = nullptr; + tool.points.clear(); } } ImGui::SameLine(); ImGui::Checkbox("Show lanes", &tool.lanes); - // how the things being built are put on the ground - auto const placement = [&tool](bool const Road) { + // typed values are taken when confirmed, so nothing is made anew with each digit + auto const number = [](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, ImGuiInputTextFlags_EnterReturnsTrue); + }; + if (tool.tool == 1) + { + ImGui::TextDisabled("LMB: start, then each next point. Ctrl: straight ahead. Esc: finish"); + ImGui::TextDisabled("Start or end on a loose end of a road to join it."); + ImGui::TextDisabled("Start or end on the side of a road to make a junction there,"); + ImGui::TextDisabled("or on a junction to give it another road."); + if (tool.chain) + { + if (ImGui::Button("Finish (Esc)")) + road_cancel(); + } + // how the road is put on the ground ImGui::SetNextItemWidth(120.0f); if (ImGui::InputFloat("Height above the ground [m]", &tool.offset, 0.05f, 0.25f, "%.2f")) tool.offset = std::clamp(tool.offset, -10.0f, 100.0f); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", "How far over the ground under the cursor the points are put.\nA few centimetres keep the surface from flickering where it lies on the ground."); - if (false == Road) - return; ImGui::Checkbox("Follow the ground", &tool.follow); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", "On: the road is led over the ground between the clicked points, in pieces as short as the ground calls for,\nwith the profile rounded so the slope doesn't change abruptly.\nOff: the road goes straight from a point to the next one, like a ramp or a viaduct."); - }; - if (tool.tool == 1) - { - ImGui::TextDisabled("LMB: start, then each next point. Ctrl: straight ahead. Esc: finish"); - ImGui::TextDisabled("Start or end on a loose end of a road or an arm of a junction to join it"); - if (tool.chain) - { - if (ImGui::Button("Finish (Esc)")) - tool.chain = false; - } - placement(true); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Speed limit on new junctions [km/h]", &tool.crossingspeed, 5.0f, 10.0f, "%.0f")) + tool.crossingspeed = std::clamp(tool.crossingspeed, 5.0f, 200.0f); ImGui::Separator(); // what's set here is what the next pieces get render_road_layout(tool.settings); } - else if (tool.tool == 2) + else if (false == tool.points.empty()) { - ImGui::TextDisabled("LMB on a loose end of a road: junction for that road, right past its end"); - ImGui::TextDisabled("LMB elsewhere: the centre, then the side the first road comes from. Esc: start over"); - char const *kinds[]{"Crossroads", "T: the road ends, roads to both sides", "T: the road goes on, a road to the left", "T: the road goes on, a road to the right"}; - ImGui::SetNextItemWidth(280.0f); - ImGui::Combo("Kind", &tool.kind, kinds, 4); - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputFloat("Speed limit on the junction [km/h]", &tool.crossingspeed, 5.0f, 10.0f, "%.0f")) - tool.crossingspeed = std::clamp(tool.crossingspeed, 5.0f, 200.0f); - placement(false); - ImGui::Separator(); - ImGui::TextWrapped("Away from the roads the junction is made for roads with the lanes set in the Build tool: %d + %d, %.2f m wide. Its surface and markings go after the road it's made for.", tool.settings.forward, tool.settings.backward, - tool.settings.lanewidth); - ImGui::TextWrapped("Roads are led out of the arms with the Build tool, and get the lanes the junction has there."); + // the road at the selected points + ImGui::Text("%d %s selected", static_cast(tool.points.size()), tool.points.size() == 1 ? "point" : "points"); + ImGui::TextDisabled("Shift+LMB: add a point, or take it out. K on a selected piece: make a point at the cursor"); + auto const ends{editor_road::ends_at(tool.points.front())}; + if (false == ends.empty()) + { + auto const &layout{ends.front().road->definition()}; + auto width{static_cast(layout.width() * layout.taper[ends.front().atend ? 1 : 0])}; + if (number("Road width here [m]", width, 0.25f, "%.2f")) + road_point_width(std::clamp(width, 1.0f, 100.0f)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Combined width of the lanes at this point; each lane takes its share of it.\nThe pieces on both sides get wider or narrower along their length to meet it.\nSelect several points to keep the width over the pieces between them."); + auto height{static_cast(tool.points.front().y)}; + if (number("Height [m]", height, 0.1f, "%.2f")) + road_point_height(height); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Height of the axis of the road at this point. The slopes of the pieces at the point are kept."); + if (editor_road::at_junction(tool.points.front())) + ImGui::TextDisabled("This point is at a junction, which is made for the road the way it is"); + } } else if (tool.junction != nullptr) { - auto const &junction{*tool.junction}; - ImGui::Text("%s, %d arms, %d ways through", junction.name().c_str(), static_cast(junction.definition().arms.size()), static_cast(junction.movements().size())); - ImGui::TextDisabled("Speed limit %.0f km/h. The ways through are made for the roads attached at the moment", junction.definition().velocity); + auto &junction{*tool.junction}; + ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast(junction.definition().arms.size()), static_cast(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"); + auto limit{junction.definition().velocity > 0.f ? junction.definition().velocity : 0.f}; + if (number("Speed limit [km/h], 0: none", limit, 5.0f, "%.0f")) + { + if (editor_road::can_edit(junction, &reason)) + { + editor_road::record record; + record.junctions.emplace_back(&junction, junction.definition()); + auto state{junction.definition()}; + state.velocity = (limit > 0.f ? limit : -1.f); + editor_road::apply(std::vector>{{&junction, state}}); + push_road_snapshot(std::move(record)); + tool.status = "Changed"; + } + else + { + tool.status = "\"" + junction.name() + "\" can't be changed: " + reason; + } + } if (ImGui::Button("Delete (Del)")) road_delete(); } @@ -1031,7 +1240,7 @@ void editor_mode::render_road_window() } else { - ImGui::TextDisabled("LMB: select a road piece or a junction"); + ImGui::TextDisabled("LMB: select a road piece, a junction, or a point where pieces meet"); } if (false == tool.status.empty()) { @@ -1112,8 +1321,7 @@ void editor_mode::draw_road_overlay() const // outline of a road piece: its axis and the edges of its surface auto const drawroad = [&](road_node::state const &State, ImU32 const Color) { - auto const samples{is_curved(State.axis) ? 12 : 1}; - auto const halfwidth{0.5 * State.width()}; + auto const samples{is_curved(State.axis) || State.taper[0] != State.taper[1] ? 12 : 1}; drawpath(State.axis, Color, 3.0f, samples); glm::dvec3 previous[2]; for (int i = 0; i <= samples; ++i) @@ -1124,6 +1332,7 @@ void editor_mode::draw_road_overlay() const glm::dvec3 across{tangent.z, 0.0, -tangent.x}; if (glm::length(across) > 1e-6) across = glm::normalize(across); + auto const halfwidth{0.5 * State.width() * State.scale(t)}; glm::dvec3 const edges[2]{position + across * halfwidth, position - across * halfwidth}; if (i > 0) { @@ -1168,49 +1377,72 @@ void editor_mode::draw_road_overlay() const { if (tool.chain) { + auto const &plan{tool.plan}; drawpoint(tool.point, kPreviewColor, 6.0f, true); - if (tool.preview.empty()) + if (plan.pieces.empty()) { projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f); - hint = tool.previewerror + " "; + hint = plan.error + " "; } - for (auto const &piece : tool.preview) + else + { + // junctions the road would start or end with + if (plan.hasstart) + drawjunction(plan.start.layout, kPreviewColor); + if (plan.hasend) + drawjunction(plan.end.layout, kPreviewColor); + } + for (auto const &piece : plan.pieces) { auto state{tool.settings}; state.axis = piece; drawroad(state, kPreviewColor); } - if (tool.previewsnap.valid()) - drawpoint(tool.previewsnap.position, kSnapColor, 14.0f, false); - hint += "LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece"; + if (plan.snap.valid()) + drawpoint(plan.snap.position, kSnapColor, 14.0f, false); + hint += (tool.branchroad != nullptr ? "LMB: lead the road out to the cursor, with a junction where it leaves Esc: give up" : + tool.branchjunction != nullptr ? "LMB: lead the road out to the cursor, as another road of the junction Esc: give up" : + "LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece"); } else { auto const end{editor_road::find_end(tool.mouse, kSnapRadius)}; if (end.valid()) + { drawpoint(end.position, kSnapColor, 14.0f, false); - hint = "LMB: set where the road starts; on a loose end of a road or an arm of a junction (ring) to carry on from there"; + hint = "LMB: carry on from this end"; + } + else if (tool.hashover) + { + drawpoint(tool.hover, kSnapColor, 5.0f, true); + drawpoint(tool.hover, kSnapColor, 14.0f, false); + hint = "LMB: lead a road out of here, with a junction"; + } + else + { + hint = "LMB: set where the road starts; on a loose end of a road to carry it on, on the side of a road or on a junction to lead a road out of it"; + } } } - else if (tool.tool == 2) - { - if (tool.chain) - drawpoint(tool.point, kPreviewColor, 6.0f, true); - if (tool.hasjunction) - drawjunction(tool.previewjunction, kPreviewColor); - else if (false == tool.previewerror.empty()) - hint = tool.previewerror + " "; - if (tool.previewsnap.valid()) - drawpoint(tool.previewsnap.position, kSnapColor, 14.0f, false); - hint += (tool.chain ? "LMB: make the junction, with the first road coming from the side of the cursor Esc: start over" : - tool.previewsnap.valid() ? "LMB: make the junction at the end of this road" : - "LMB: on a loose end of a road (ring) to make its junction there; elsewhere to set the centre of one"); - } else { - hint = tool.selected != nullptr ? "LMB: select a road piece or a junction K: split at the cursor Del: delete Ctrl+Z: undo" : - tool.junction != nullptr ? "LMB: select a road piece or a junction Del: delete Ctrl+Z: undo" : - "LMB: select a road piece or a junction"; + // points where the pieces meet or end, to pick from + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved || glm::distance(road->location(), camera) > kPointRange) + continue; + drawpoint(road->definition().axis.points[segment_data::point::start], kPointColor, 3.5f, true); + drawpoint(road->definition().axis.points[segment_data::point::end], kPointColor, 3.5f, true); + } + for (auto const &point : tool.points) + { + 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)"; } // shown above the place the track tools put their hints at ImGuiIO const &io = ImGui::GetIO(); diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index 4cbd4051..aa37f692 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -31,6 +31,9 @@ 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 void add_unique(std::vector &Roads, road_node *Road) { @@ -129,6 +132,135 @@ bool turning(road_node const &Road) 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(State.scale(From)), static_cast(State.scale(To))}; + return result; +} + +int const kStations{64}; + +// distances along a path for evenly spaced values of its curve parameter +std::vector stations(road_node::state const &State) +{ + std::vector result(kStations + 1, 0.0); + auto previous{State.point(0.0)}; + for (int i = 1; i <= kStations; ++i) + { + auto const current{State.point(static_cast(i) / kStations)}; + result[i] = result[i - 1] + glm::distance(previous, current); + previous = current; + } + return result; +} + +// distance along a path for specified value of its curve parameter, and the other way around +double station_at(std::vector 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(position))}; + return glm::mix(Stations[index], Stations[index + 1], position - index); +} + +double parameter_at(std::vector const &Stations, double const Station) +{ + if (Station <= 0.0) + return 0.0; + if (Station >= Stations.back()) + return 1.0; + auto const index{static_cast(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 &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); + 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; +} + +// 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; + Junction.friction = Road.friction; + Junction.sounddistance = Road.sounddistance; + Junction.quality = Road.quality; + Junction.environment = Road.environment; +} + void retire(std::vector const &Tracks) { for (auto *track : Tracks) @@ -441,6 +573,24 @@ void editor_road::revive(std::vector const &Roads) }); } +void editor_road::apply(std::vector> const &Changes) +{ + std::vector 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 editor_road::create(std::vector const &States) { static int counter{0}; @@ -538,8 +688,12 @@ road_node *editor_road::split(road_node &Road, double const T) auto const roll{static_cast(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * T)}; first.axis.rolls[1] = roll; second.axis.rolls[0] = roll; - apply({{&Road, first}}); - auto const created{create({second})}; + // the width at the cut is what it was there + auto const scale{static_cast(Road.definition().scale(T))}; + first.taper[1] = scale; + second.taper[0] = scale; + apply(std::vector>{{&Road, first}}); + auto const created{create(std::vector{second})}; return created.empty() ? nullptr : created.front(); } @@ -907,39 +1061,238 @@ void editor_road::profile(std::vector &Pieces, std::vector } } -double editor_road::junction_reach(int const Lanes, float const Width) +bool editor_road::nearest_joint(glm::dvec3 const &Point, double const Radius, glm::dvec3 &Joint) { - // half of the road, and room for the corners - return 0.5 * std::max(1, Lanes) * Width + 6.0; + 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; } -junction_node::state editor_road::junction(junction_kind const Kind, glm::dvec3 const &Entry, glm::dvec2 const &Heading, int const Incoming, int const Outgoing, float const Width) +std::vector editor_road::ends_at(glm::dvec3 const &Point) { - junction_node::state state; - auto const heading{glm::length(Heading) > 1e-6 ? glm::normalize(Heading) : glm::dvec2{0.0, 1.0}}; - glm::dvec2 const left{heading.y, -heading.x}; - auto const reach{junction_reach(Incoming + Outgoing, Width)}; - auto const spot = [&](glm::dvec2 const &Direction, double const Distance) { return glm::dvec3{Entry.x + Direction.x * Distance, Entry.y, Entry.z + Direction.y * Distance}; }; - state.centre = spot(heading, reach); - auto const arm = [&](glm::dvec2 const &Direction, int const In, int const Out) { - junction_node::arm_data data; - data.position = glm::dvec3{state.centre.x + Direction.x * reach, Entry.y, state.centre.z + Direction.y * reach}; - data.direction = Direction; - data.incoming = In; - data.outgoing = Out; - data.width = Width; - state.arms.emplace_back(data); - }; - // the road the junction is entered by, then the others: what comes in by one leaves by them - arm(-heading, Incoming, Outgoing); - if (Kind == junction_kind::cross || Kind == junction_kind::tee_left || Kind == junction_kind::tee_right) - arm(heading, Outgoing, Incoming); - if (Kind == junction_kind::cross || Kind == junction_kind::tee_sides || Kind == junction_kind::tee_left) - arm(left, Outgoing, Incoming); - if (Kind == junction_kind::cross || Kind == junction_kind::tee_sides || Kind == junction_kind::tee_right) - arm(-left, Outgoing, Incoming); - state.normalize(); - return state; + std::vector 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; +} + +bool editor_road::at_junction(glm::dvec3 const &Point) +{ + return false == junctions_at(Point).empty(); +} + +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(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; + } + float lanewidth{0.f}; + if (false == lane_width(layout, T, lanewidth)) + { + Error = "Lanes of \"" + Road.name() + "\" differ in width, and a junction takes roads with lanes of the same width"; + return false; + } + auto const halfwidth{0.5 * (layout.forward + layout.backward) * lanewidth}; + 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 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 = "\"" + change.first->name() + "\" can't be changed: " + reason; + return false; + } + } + for (auto const *piece : Result.removed) + { + if (false == can_edit(*piece, &reason)) + { + Error = "\"" + piece->name() + "\" can't be changed: " + reason; + 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) + { + auto const &state{cut.road->definition()}; + junction_node::arm_data arm; + arm.position = state.point(cut.t); + // the way out of the junction is the way the kept part of the piece goes from the cut + arm.direction = planar(state.tangent(cut.t)) * (cut.keepstart ? -1.0 : 1.0); + // lanes heading for the cut lead into the junction + arm.incoming = (cut.keepstart ? state.forward : state.backward); + arm.outgoing = (cut.keepstart ? state.backward : state.forward); + if (false == lane_width(state, cut.t, arm.width)) + { + Error = "Lanes of \"" + cut.road->name() + "\" differ in width, and a junction takes roads with lanes of the same width"; + 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); + 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 = "\"" + Junction.name() + "\" can't be changed: " + reason; + 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) + { + Record.junctions.emplace_back(Branch.junction, Branch.junction->definition()); + apply(std::vector>{{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) + Record.roads.emplace_back(change.first, change.first->definition()); + 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{Branch.layout})}; + if (junctions.empty()) + return nullptr; + Record.junctions_created.emplace_back(junctions.front()); + return junctions.front(); } std::vector const &editor_road::materials() diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp index e7f7fff3..751d04c6 100644 --- a/editor/editorRoad.hpp +++ b/editor/editorRoad.hpp @@ -39,12 +39,37 @@ class editor_road return road != nullptr || junction != nullptr; } }; - enum class junction_kind + // an end of a road piece + struct piece_end { - cross, // four roads - tee_sides, // the road ends, with roads to both sides - tee_left, // the road goes on, with a road to the left - tee_right // the road goes on, with a road to the right + road_node *road{nullptr}; + bool atend{false}; // the end of its axis rather than the start + }; + // what an operation changed, in the form needed to take it back + struct record + { + std::vector> roads; // pieces with what they were like before + std::vector roads_created; + std::vector roads_removed; + std::vector> junctions; // junctions with what they were like before + std::vector junctions_created; + std::vector junctions_removed; + + bool empty() const + { + return roads.empty() && roads_created.empty() && roads_removed.empty() && junctions.empty() && junctions_created.empty() && junctions_removed.empty(); + } + }; + // a road led out of the side of another road, or out of a junction between its arms: the junction this takes, + // and what has to be done with the roads to make room for it + struct branch + { + junction_node *junction{nullptr}; // the junction which gets another arm, nullptr if a new one is made + junction_node::state layout; // the junction as it's going to be + std::size_t arm{0}; // the arm of it made for the road + std::vector> changes; // pieces cut short, with their new definitions + std::vector added; // part of a piece left past the junction, to be made a piece of its own + std::vector removed; // pieces which lie within the junction }; // true if the roads can be built and changed at all. optionally explains why they can't static bool available(std::string *Reason = nullptr); @@ -57,6 +82,8 @@ class editor_road static std::vector create(std::vector const &States); // replaces definitions of the pieces and makes their lanes and geometry anew static void apply(std::vector> const &Changes); + // replaces definitions of the junctions, and makes their geometry and the ways through them anew + static void apply(std::vector> const &Changes); // takes the pieces out of the scenery. they're kept around, so they can be brought back static void remove(std::vector const &Roads); static void revive(std::vector const &Roads); @@ -72,6 +99,23 @@ class editor_road static road_node *nearest(glm::dvec3 const &Point, double const Margin); // the junction specified point lies on static junction_node *nearest_junction(glm::dvec3 const &Point); + // the point where pieces meet or end which is nearest to specified one, within specified distance from it. returns: false if there's none + static bool nearest_joint(glm::dvec3 const &Point, double const Radius, glm::dvec3 &Joint); + // ends of the pieces which meet at specified point + static std::vector ends_at(glm::dvec3 const &Point); + // true if there's an arm of a junction at specified point + static bool at_junction(glm::dvec3 const &Point); + // width shared by all lanes of the road at specified value of the curve parameter of its axis. returns: false if the lanes differ in width + static bool lane_width(road_node::state const &State, double const T, float &Width); + // lays out a junction for a road leaving the side of specified piece at specified value of the curve parameter of its axis. + // Direction: the way the road leaves; Incoming, Outgoing: lanes of that road leading into the junction and out of it. + // nothing is changed until the result is carried out. returns: false if it can't be done, with the reason in Error + static bool 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); + // the same for a road leaving a junction between its arms, which gives the junction another arm + static bool branch_from(junction_node &Junction, glm::dvec2 const &Direction, int const Incoming, int const Outgoing, float const Width, branch &Result, std::string &Error); + // makes the junction laid out for a branch, or gives the existing one its new arm, and cuts the roads to fit. what it does is added to Record. + // returns: the junction + static junction_node *carry_out(branch const &Branch, record &Record); // value of the curve parameter for the point of the axis nearest to specified point static double nearest_parameter(road_node::state const &State, glm::dvec3 const &Point); // end of a road with nothing attached, within specified distance from specified point; optionally arms of junctions too @@ -91,11 +135,6 @@ class editor_road static double planar_length(segment_data const &Path); // slope of the path at its end, in the direction it's laid out static double end_grade(segment_data const &Path); - // distance from the centre of a junction to the ends of the roads it's made for - static double junction_reach(int const Lanes, float const Width); - // lays out a junction entered by a road. Entry: middle of the end of that road, Heading: the way the road goes there, - // Incoming, Outgoing: lanes of that road leading into the junction and out of it - static junction_node::state junction(junction_kind const Kind, glm::dvec3 const &Entry, glm::dvec2 const &Heading, int const Incoming, int const Outgoing, float const Width); // names of materials and images present in the texture folder, to pick from static std::vector const &materials(); diff --git a/world/Road.cpp b/world/Road.cpp index a0da0b62..b9e4a537 100644 --- a/world/Road.cpp +++ b/world/Road.cpp @@ -192,13 +192,24 @@ class road_axis // corner of the cross-section of something laid along the road struct profile_point { - double offset; // distance from the axis, positive to the left + double offset; // distance from the axis, positive to the left. this part of it follows the taper of the road double height; // above the surface float texture; // texture coordinate across the road + double shift{0.0}; // further distance from the axis, which the taper leaves alone }; +// how far the taper of a road has got at specified part of its length. it starts and ends level, so the pieces of a road +// meet without a bend in their edges, and it's what a lane running from one width to another comes to on a straight +double taper_progress(double const Fraction) +{ + auto const t{std::clamp(Fraction, 0.0, 1.0)}; + return t * t * (3.0 - 2.0 * t); +} + // stretches provided profile along a part of the axis. the profile points go left to right, each two neighbours make a band of triangles. -void loft(std::vector &Output, road_axis const &Axis, std::vector const &Profile, double const From, double const To, double const Step, double const Texturelength) +// Taper: what the offsets of the profile are multiplied by at the start and at the end of the axis +void loft(std::vector &Output, road_axis const &Axis, std::vector const &Profile, double const From, double const To, double const Step, double const Texturelength, + std::array const &Taper) { if (Profile.size() < 2 || To <= From || Step <= 0.0) { @@ -219,9 +230,10 @@ void loft(std::vector &Output, road_axis const &Axis, std::vector< { auto const station{From + (To - From) * section / count}; auto const frame{Axis.at(station)}; + auto const scale{Taper[0] == Taper[1] ? static_cast(Taper[0]) : glm::mix(static_cast(Taper[0]), static_cast(Taper[1]), taper_progress(station / Axis.length()))}; for (std::size_t idx = 0; idx < pointcount; ++idx) { - current[idx] = frame.position + frame.left * Profile[idx].offset + up * Profile[idx].height; + current[idx] = frame.position + frame.left * (Profile[idx].offset * scale + Profile[idx].shift) + up * Profile[idx].height; } if (section > 0) { @@ -279,6 +291,10 @@ void road_node::state::normalize() forward = 1; } lanewidth = std::max(1.f, lanewidth); + for (auto &end : taper) + { + end = std::clamp(end, 0.25f, 4.f); + } if (texturelength < 0.01f) { texturelength = 4.f; @@ -372,7 +388,7 @@ double road_node::state::lane_offset(std::size_t const Lane) const return Lane < lanes.size() ? offset - 0.5 * lanes[Lane].width : offset; } -// combined width of the lanes +// combined width of the lanes, before the taper double road_node::state::width() const { double total{0.0}; @@ -383,24 +399,33 @@ double road_node::state::width() const return total; } +// what the widths of the lanes are multiplied by at specified value of the curve parameter +double road_node::state::scale(double const T) const +{ + return glm::mix(static_cast(taper[0]), static_cast(taper[1]), taper_progress(T)); +} + // shape of the middle of a lane, laid out in the direction of travel segment_data road_node::state::lane_path(std::size_t const Lane) const { road_axis const centre{axis}; - auto const offset{lane_offset(Lane)}; + // on a road which gets wider or narrower the lane moves away from the axis or towards it along the way + auto const beginoffset{lane_offset(Lane) * taper[0]}; + auto const endoffset{lane_offset(Lane) * taper[1]}; auto const begin{centre.frame(0.0)}; auto const end{centre.frame(1.0)}; segment_data path; - path.points[segment_data::point::start] = begin.position + begin.left * offset; - path.points[segment_data::point::end] = end.position + end.left * offset; - if (centre.curved()) + path.points[segment_data::point::start] = begin.position + begin.left * beginoffset; + path.points[segment_data::point::end] = end.position + end.left * endoffset; + if (centre.curved() || beginoffset != endoffset) { - // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors - path.points[segment_data::point::control1] = centre.control(0) * std::max(0.05, 1.0 - offset * std::cos(begin.roll) * centre.curvature(0.0)); - path.points[segment_data::point::control2] = centre.control(1) * std::max(0.05, 1.0 - offset * std::cos(end.roll) * centre.curvature(1.0)); + // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors. + // the control vectors go along the axis at both ends, which is what makes the lane meet the next piece without a bend + path.points[segment_data::point::control1] = centre.control(0) * std::max(0.05, 1.0 - beginoffset * std::cos(begin.roll) * centre.curvature(0.0)); + path.points[segment_data::point::control2] = centre.control(1) * std::max(0.05, 1.0 - endoffset * std::cos(end.roll) * centre.curvature(1.0)); // if the roll changes along the road its sides rise or fall relative to the axis - auto const climb{offset * (std::sin(end.roll) - std::sin(begin.roll)) / 3.0}; + auto const climb{(endoffset * std::sin(end.roll) - beginoffset * std::sin(begin.roll)) / 3.0}; path.points[segment_data::point::control1].y += climb; path.points[segment_data::point::control2].y -= climb; } @@ -409,7 +434,7 @@ segment_data road_node::state::lane_path(std::size_t const Lane) const { // the lanes on the inner side of the bend have it tighter auto const turn{centre.curvature(0.5) >= 0.0 ? 1.0 : -1.0}; - path.radius = static_cast(std::max(1.0, std::abs(axis.radius) - turn * offset)); + path.radius = static_cast(std::max(1.0, std::abs(axis.radius) - turn * 0.5 * (beginoffset + endoffset))); } if (Lane < static_cast(backward)) { @@ -490,6 +515,12 @@ void road_node::import(cParser &Input, glm::dvec3 const &Offset) Input.getTokens(); Input >> m_state.velocity; } + else if (token == "taper") + { + // taper + Input.getTokens(2); + Input >> m_state.taper[0] >> m_state.taper[1]; + } else if (token == "lane") { // lane @@ -779,7 +810,7 @@ std::vector road_node::create_shapes() const if (road.surface != "none") { auto const tile{static_cast(texture_ratio(road.surface)) * texturelength}; - loft(vertices(road.surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast(0.5 - halfwidth / tile)}}, 0.0, length, step, texturelength); + loft(vertices(road.surface, lighting_data{}), axis, {{halfwidth, 0.0, static_cast(0.5 + halfwidth / tile)}, {-halfwidth, 0.0, static_cast(0.5 - halfwidth / tile)}}, 0.0, length, step, texturelength, road.taper); } // sides @@ -797,10 +828,10 @@ std::vector road_node::create_shapes() const { // the image is laid out like the one of the legacy roads: the edge of the surface on its right, the bank on its left half profile.push_back({direction * halfwidth, 0.0, 1.f}); - profile.push_back({direction * (halfwidth + data.width), 0.0, 0.5f}); + profile.push_back({direction * halfwidth, 0.0, 0.5f, direction * data.width}); if (road.slope.x > 0.f) { - profile.push_back({direction * (halfwidth + data.width + road.slope.x), -road.slope.y, 0.f}); + profile.push_back({direction * halfwidth, -road.slope.y, 0.f, direction * (data.width + road.slope.x)}); } } else @@ -809,14 +840,14 @@ std::vector road_node::create_shapes() const auto const tile{static_cast(texture_ratio(data.material)) * texturelength}; profile.push_back({direction * halfwidth, 0.0, 0.f}); profile.push_back({direction * halfwidth, road.kerbheight, static_cast(road.kerbheight / tile)}); - profile.push_back({direction * (halfwidth + data.width), road.kerbheight, static_cast((road.kerbheight + data.width) / tile)}); + profile.push_back({direction * halfwidth, road.kerbheight, static_cast((road.kerbheight + data.width) / tile), direction * data.width}); } if (side == 0) { // the loft takes the corners left to right std::reverse(profile.begin(), profile.end()); } - loft(vertices(data.material, lighting_data{}), axis, profile, 0.0, length, step, texturelength); + loft(vertices(data.material, lighting_data{}), axis, profile, 0.0, length, step, texturelength, road.taper); } // markings. these are painted with plain colour instead of an image @@ -827,26 +858,27 @@ std::vector road_node::create_shapes() const paint.ambient = paint.diffuse; auto &lines{vertices("colored", paint)}; - auto const solid = [&](double const Offset) { - loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, 0.0, length, std::min(step, dash_step), texturelength); + // Offset: where the line runs on a road as wide as its lanes say, Shift: how far from there it's kept whatever the width + auto const solid = [&](double const Offset, double const Shift) { + loft(lines, axis, {{Offset, line_lift, 0.f, Shift + 0.5 * line_width}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width}}, 0.0, length, std::min(step, dash_step), texturelength, road.taper); }; // the dashes are fitted so the gaps at both ends of the road are a half of the regular one, which makes them match the next piece - auto const dashed = [&](double const Offset) { + auto const dashed = [&](double const Offset, double const Shift) { auto const count{std::max(1, static_cast(std::lround(length / dash_period)))}; auto const period{length / count}; for (int dash = 0; dash < count; ++dash) { auto const from{(dash + 1.0 / 3.0) * period}; - loft(lines, axis, {{Offset + 0.5 * line_width, line_lift, 0.f}, {Offset - 0.5 * line_width, line_lift, 1.f}}, from, from + period / 3.0, dash_step, texturelength); + loft(lines, axis, {{Offset, line_lift, 0.f, Shift + 0.5 * line_width}, {Offset, line_lift, 1.f, Shift - 0.5 * line_width}}, from, from + period / 3.0, dash_step, texturelength, road.taper); } }; // edges of the surface - auto const edge{halfwidth - edge_inset - 0.5 * line_width}; - if (edge > 0.5) + auto const inset{edge_inset + 0.5 * line_width}; + if (halfwidth * std::min(road.taper[0], road.taper[1]) - inset > 0.5) { - solid(edge); - solid(-edge); + solid(halfwidth, -inset); + solid(-halfwidth, inset); } // lines between the lanes. a lane can be left across a dashed line, or across a pair of lines if the dashed one is on its side auto offset{halfwidth}; @@ -859,19 +891,19 @@ std::vector road_node::create_shapes() const { case change_both: { - dashed(offset); + dashed(offset, 0.0); break; } case change_toright: { - dashed(offset + pair); - solid(offset - pair); + dashed(offset, pair); + solid(offset, -pair); break; } case change_toleft: { - solid(offset + pair); - dashed(offset - pair); + solid(offset, pair); + dashed(offset, -pair); break; } default: @@ -879,12 +911,12 @@ std::vector road_node::create_shapes() const if (opposite) { // traffic going opposite ways is kept apart with a double line - solid(offset + pair); - solid(offset - pair); + solid(offset, pair); + solid(offset, -pair); } else { - solid(offset); + solid(offset, 0.0); } break; } @@ -1671,7 +1703,7 @@ TTrack *road_node::create_track(std::string const &Name, segment_data const &Pat // radius() subclass details, calculates node's bounding radius float road_node::radius_() { - return static_cast(0.5 * m_state.length() + 0.5 * m_state.width() + std::max(m_state.sides[0].width, m_state.sides[1].width) + m_state.slope.x); + return static_cast(0.5 * m_state.length() + 0.5 * m_state.width() * std::max(m_state.taper[0], m_state.taper[1]) + std::max(m_state.sides[0].width, m_state.sides[1].width) + m_state.slope.x); } // serialize() subclass details, sends content of the subclass to provided stream @@ -1701,6 +1733,10 @@ void road_node::export_as_text_(std::ostream &Output) const Output.precision(precision); // lanes Output << "lanes " << road.forward << ' ' << road.backward << ' ' << "width " << road.lanewidth << ' '; + if (road.taper[0] != 1.f || road.taper[1] != 1.f) + { + Output << "taper " << road.taper[0] << ' ' << road.taper[1] << ' '; + } if (road.velocity > 0.f) { Output << "velocity " << road.velocity << ' '; diff --git a/world/Road.h b/world/Road.h index de77977f..db4e8d75 100644 --- a/world/Road.h +++ b/world/Road.h @@ -47,7 +47,8 @@ class owned_shapes scene::basic_section *m_section{nullptr}; }; -// a piece of a road for wheeled traffic: an axis, and a cross-section which stays the same along it. +// a piece of a road for wheeled traffic: an axis, and a cross-section which stays the same along it, save for getting +// evenly wider or narrower if it's told to. // the node itself is neither driven on nor drawn. it produces the lanes, which are ordinary invisible one-way tracks, // and the surface, the sides and the markings, which are ordinary shapes. this way the vehicles, the ai and the renderers // deal with a road without knowing about it. @@ -100,6 +101,7 @@ class road_node : public scene::basic_node float velocity{-1.f}; std::vector lanes; std::vector changes; // permissions to change the lane, one for each pair of neighbouring lanes + std::array taper{1.f, 1.f}; // what the widths of the lanes are multiplied by at the start and at the end of the axis std::string surface{"asphaltdark1"}; float texturelength{4.f}; std::array sides; // left, right @@ -121,8 +123,10 @@ class road_node : public scene::basic_node int default_change(std::size_t const Boundary) const; // distance of the middle of a lane from the axis, positive to the left when facing along the axis double lane_offset(std::size_t const Lane) const; - // combined width of the lanes + // combined width of the lanes, before the taper double width() const; + // what the widths of the lanes are multiplied by at specified value of the curve parameter + double scale(double const T) const; // shape of the middle of a lane, laid out in the direction of travel segment_data lane_path(std::size_t const Lane) const; // length of the axis @@ -206,6 +210,7 @@ class road_node : public scene::basic_node // gets a legacy crossroads path, which is what the vehicles and their drivers already know how to pick a way through. // scenery entry: // node junction arm ... [ ]... endjunction +// the arms don't have to be level with each other: the surface is spanned between their ends and the centre class junction_node : public scene::basic_node {