/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" #include "utilities/Globals.h" #include "rendering/renderer.h" #include "world/Track.h" #include "world/Road.h" #include "simulation/simulation.h" #include "imgui/imgui.h" #include #include #include #include namespace { // the same helper the track tools use to draw over the viewport class screen_projection { public: screen_projection() { ImGuiIO const &io = ImGui::GetIO(); m_size = io.DisplaySize; m_camera = GfxRenderer->Camera_Position(); float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); } glm::vec4 clip(glm::dvec3 const &Point) const { return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); } ImVec2 screen(glm::vec4 const &Clip) const { return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); } bool project(glm::dvec3 const &Point, ImVec2 &Screen) const { auto const c = clip(Point); if (c.w < kNear) return false; Screen = screen(c); return true; } void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const { glm::vec4 a = clip(A); glm::vec4 b = clip(B); if (a.w < kNear && b.w < kNear) return; if (a.w < kNear) a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); else if (b.w < kNear) b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); Drawlist->AddLine(screen(a), screen(b), Color, Thickness); } private: static constexpr float kNear{0.1f}; ImVec2 m_size; glm::dvec3 m_camera; glm::mat4 m_viewprojection; }; glm::dvec3 path_point(segment_data const &Path, double const T) { auto const &p0 = Path.points[segment_data::point::start]; auto const &p3 = Path.points[segment_data::point::end]; auto const &cp1 = Path.points[segment_data::point::control1]; auto const &cp2 = Path.points[segment_data::point::control2]; if (cp1 == glm::dvec3{0.0} && cp2 == glm::dvec3{0.0}) return glm::mix(p0, p3, T); auto const p1 = p0 + cp1; auto const p2 = p3 + cp2; double const u = 1.0 - T; return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; } bool is_curved(segment_data const &Path) { return Path.points[segment_data::point::control1] != glm::dvec3{0.0} || Path.points[segment_data::point::control2] != glm::dvec3{0.0}; } void copy_text(char *Buffer, std::size_t const Size, std::string const &Text) { std::strncpy(Buffer, Text.c_str(), Size - 1); Buffer[Size - 1] = 0; } std::string lower_case(std::string Text) { std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return 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 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 // lengths of the pieces a road following the ground is tried with, from the longest; the first one to fit the ground is taken double const kProfileSteps[]{50.0, 25.0, 12.5}; double const kSunkTolerance{0.05}; // how far under the height it's built at the road can get between the points it's led through double const kRaisedTolerance{0.5}; // and how far over it 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 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)}; } void editor_mode::render_road_menu() { if (ImGui::BeginMenu("Roads")) { ImGui::MenuItem("Road editor", nullptr, &m_roadtool.window); ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes); ImGui::EndMenu(); } } void editor_mode::update_road_tool() { auto &tool{m_roadtool}; if (tool.selected != nullptr && tool.selected->m_editorremoved) 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 (false == tool.window) { tool.chain = false; return; } glm::dvec3 const camera{Global.pCamera.Pos}; tool.mouse = camera + GfxRenderer->Mouse_Position(); if (glm::distance(tool.mouse, camera) > kReach) { tool.previewerror = "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); } void editor_mode::road_select(road_node *Road) { auto &tool{m_roadtool}; tool.selected = Road; if (Road != nullptr) { tool.junction = nullptr; tool.settings = Road->definition(); std::string reason; tool.status = "Selected: " + (Road->name().empty() ? std::string{"(unnamed road)"} : Road->name()); if (false == editor_road::can_edit(*Road, &reason)) tool.status += ". It can't be changed: " + reason; } copy_text(tool.surface, sizeof(tool.surface), tool.settings.surface); for (int side = 0; side < 2; ++side) copy_text(tool.sides[side], sizeof(tool.sides[side]), tool.settings.sides[side].material); } void editor_mode::junction_select(junction_node *Junction) { auto &tool{m_roadtool}; tool.junction = Junction; if (Junction == nullptr) return; tool.selected = nullptr; std::string reason; tool.status = "Selected: " + Junction->name(); if (false == editor_road::can_edit(*Junction, &reason)) tool.status += ". It can't be removed: " + reason; } void editor_mode::road_cancel() { m_roadtool.chain = false; } std::vector editor_mode::road_preview(editor_road::loose_end &Snap, std::string &Error, bool const Fresh) { auto const &tool{m_roadtool}; 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 = {}; glm::dvec2 enddirection{0.0, 1.0}; if (Snap.valid()) { glm::dvec2 const outwards{Snap.outwards.x, 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 {}; } } 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; } // 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) { 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 std::vector heights; 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); return; } auto const planned{Pieces}; bool fresh{Fresh}; for (auto const step : kProfileSteps) { Pieces = editor_road::refine(planned, step); auto const count{Pieces.size()}; // the ground is looked up where the pieces meet, and in the middle of each to see how the result fits it std::vector points; points.reserve(2 * count + 1); for (auto const &piece : Pieces) points.emplace_back(piece.points[segment_data::point::start]); points.emplace_back(Pieces.back().points[segment_data::point::end]); for (auto const &piece : Pieces) points.emplace_back(path_point(piece, 0.5)); // the planner leads the road between its ends, which are over the ground already for (auto &point : points) point.y -= tool.offset; auto ground{ground_heights(points, fresh)}; fresh = false; for (auto &height : ground) height += tool.offset; std::vector heights(ground.begin(), ground.begin() + count + 1); // 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); // 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)) continue; auto const over{path_point(Pieces[i], 0.5).y - ground[count + 1 + i]}; if (over < -kSunkTolerance || over > kRaisedTolerance) { fits = false; break; } } if (fits) break; } } // 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}; glm::dvec3 const camera{Global.pCamera.Pos}; glm::dvec3 const ground{camera + GfxRenderer->Mouse_Position()}; if (glm::distance(ground, camera) > kReach) { tool.status = "The click didn't land on anything near enough"; return; } tool.mouse = ground; if (tool.tool == 0) { auto *road{editor_road::nearest(ground, 1.5)}; auto *junction{road == nullptr ? editor_road::nearest_junction(ground) : nullptr}; if (road == nullptr && junction == nullptr) tool.status = "No road here"; road_select(road); junction_select(junction); return; } std::string reason; if (false == editor_road::available(&reason)) { 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 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) { if (false == editor_road::can_join(end, &reason)) { tool.status = "Can't build from here: " + reason; return; } tool.point = end.position; tool.direction = glm::normalize(outwards); 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(); tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; } else { tool.reversed = false; tool.settings.forward = end.forward; tool.settings.backward = end.backward; tool.settings.lanewidth = end.width; tool.settings.lanes.clear(); tool.settings.changes.clear(); tool.settings.normalize(); tool.status = "Leading a road out of \"" + end.junction->name() + "\", with the lanes the junction has there"; } road_select(nullptr); } 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.chain = true; return; } editor_road::loose_end snap; std::string error; auto pieces{road_preview(snap, error, true)}; if (pieces.empty()) { tool.status = "Can't build this: " + error; return; } if (snap.valid() && false == editor_road::can_join(snap, &reason)) { tool.status = "Can't join here: " + reason; return; } // where the next piece is going to carry on from auto const &last{pieces.back()}; auto const endpoint{last.points[segment_data::point::end]}; auto const tangent{last.points[segment_data::point::control2] != glm::dvec3{0.0} ? -last.points[segment_data::point::control2] : last.points[segment_data::point::end] - last.points[segment_data::point::start]}; auto const grade{editor_road::end_grade(last)}; if (tool.reversed) { for (auto &piece : pieces) { std::swap(piece.points[segment_data::point::start], piece.points[segment_data::point::end]); std::swap(piece.points[segment_data::point::control1], piece.points[segment_data::point::control2]); } std::reverse(pieces.begin(), pieces.end()); } std::vector states; for (auto const &piece : pieces) { auto state{tool.settings}; state.axis = piece; states.emplace_back(state); } auto const created{editor_road::create(states)}; if (created.empty()) { tool.status = "The road wasn't created"; return; } push_road_snapshot({}, created, {}); if (snap.valid()) { tool.chain = false; tool.status = "Joined with \"" + (snap.road != nullptr ? snap.road->name() : snap.junction->name()) + "\""; return; } glm::dvec2 const planar{tangent.x, tangent.z}; tool.point = endpoint; if (glm::length(planar) > 1e-6) { tool.direction = glm::normalize(planar); tool.hasdirection = true; } tool.grade = grade; tool.hasgrade = true; tool.status = std::to_string(created.size()) + (created.size() == 1 ? " piece added" : " pieces added"); } void editor_mode::road_apply() { auto &tool{m_roadtool}; if (tool.selected == nullptr) return; tool.settings.normalize(); auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; std::vector> before; 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; return; } auto state{tool.settings}; state.axis = road->definition().axis; 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()); changes.emplace_back(road, state); } editor_road::apply(changes); push_road_snapshot(before, {}, {}); tool.settings = tool.selected->definition(); tool.status = (pieces.size() == 1 ? std::string{"Changed"} : "Changed " + std::to_string(pieces.size()) + " pieces"); } bool editor_mode::road_delete() { auto &tool{m_roadtool}; if (false == tool.window || (tool.selected == nullptr && tool.junction == nullptr)) return false; std::string reason; if (tool.junction != nullptr) { auto *junction{tool.junction}; if (false == editor_road::can_edit(*junction, &reason)) { tool.status = "Can't delete: " + reason; return true; } editor_road::remove(std::vector{junction}); push_road_snapshot({}, {}, {}, {}, {junction}); tool.junction = nullptr; tool.status = "\"" + junction->name() + "\" deleted, Ctrl+Z brings it back"; return true; } auto *road{tool.selected}; if (false == editor_road::can_edit(*road, &reason)) { tool.status = "Can't delete: " + reason; return true; } editor_road::remove(std::vector{road}); push_road_snapshot({}, {}, {road}); tool.selected = nullptr; tool.status = "\"" + road->name() + "\" deleted, Ctrl+Z brings it back"; return true; } bool editor_mode::road_split() { auto &tool{m_roadtool}; if (false == tool.window || tool.selected == nullptr) return false; auto *road{tool.selected}; std::string reason; if (false == editor_road::can_edit(*road, &reason)) { 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))}; if (created == nullptr) { tool.status = "Can't split this close to the end"; return true; } push_road_snapshot({{road, before}}, {created}, {}); road_select(road); tool.status = "Split in two: \"" + road->name() + "\" and \"" + created->name() + "\""; return true; } void editor_mode::push_road_snapshot(std::vector> States, std::vector Created, std::vector Removed, std::vector Junctionscreated, std::vector Junctionsremoved) { if (States.empty() && Created.empty() && Removed.empty() && Junctionscreated.empty() && Junctionsremoved.empty()) return; if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); // the entry is listed under the name of the first thing it's about scene::basic_node const *node{nullptr}; if (false == States.empty()) node = States.front().first; else if (false == Created.empty()) node = Created.front(); else if (false == Removed.empty()) node = Removed.front(); else if (false == Junctionscreated.empty()) node = Junctionscreated.front(); else node = Junctionsremoved.front(); EditorSnapshot snap; snap.action = EditorSnapshot::Action::RoadEdit; snap.node_name = node->name(); 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); 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()) 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()); Opposite.push_back(std::move(current)); 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); } 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::apply(Snapshot.roads); auto &tool{m_roadtool}; tool.chain = false; if (tool.selected != nullptr) road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); if (tool.junction != nullptr && tool.junction->m_editorremoved) tool.junction = nullptr; } // draws controls for the layout of a road. returns: true if the layout was changed in a way which calls for the road to be made anew bool editor_mode::render_road_layout(road_node::state &State) { auto &tool{m_roadtool}; bool changed{false}; // typed values are taken when confirmed, so the road isn't 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); }; auto const material = [&tool](char const *Label, char *Buffer, std::size_t const Size, std::string &Value) { bool picked{false}; ImGui::PushID(Label); ImGui::SetNextItemWidth(200.0f); if (ImGui::InputText("##name", Buffer, Size, ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::IsItemDeactivatedAfterEdit()) { auto const name{lower_case(Buffer)}; if (name != Value && name.find(' ') == std::string::npos) { Value = name.empty() ? std::string{"none"} : name; picked = true; } copy_text(Buffer, Size, Value); } ImGui::SameLine(); if (ImGui::Button("...")) ImGui::OpenPopup("pick"); ImGui::SameLine(); ImGui::TextUnformatted(Label); if (ImGui::BeginPopup("pick")) { ImGui::TextDisabled("Materials and images found in the texture folder"); ImGui::SetNextItemWidth(200.0f); ImGui::InputText("Filter", tool.filter, sizeof(tool.filter)); auto const filter{lower_case(tool.filter)}; std::vector listed; for (auto const &name : editor_road::materials()) if (filter.empty() || name.find(filter) != std::string::npos) listed.emplace_back(&name); ImGui::BeginChild("list", ImVec2(340.0f, 280.0f), true); ImGuiListClipper clipper(static_cast(listed.size())); while (clipper.Step()) { for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) { if (ImGui::Selectable(listed[i]->c_str(), *listed[i] == Value)) { Value = *listed[i]; copy_text(Buffer, Size, Value); picked = true; ImGui::CloseCurrentPopup(); } } } ImGui::EndChild(); ImGui::EndPopup(); } ImGui::PopID(); return picked; }; // lanes ImGui::SetNextItemWidth(120.0f); if (ImGui::InputInt("Lanes along the axis", &State.forward)) changed = true; ImGui::SetNextItemWidth(120.0f); if (ImGui::InputInt("Lanes against the axis", &State.backward)) changed = true; if (changed) { // a different set of lanes starts with the common width and speed limit, and the default rules for changing lanes State.lanes.clear(); State.changes.clear(); State.normalize(); } if (number("Lane width [m]", State.lanewidth, 0.25f, "%.2f")) { State.lanewidth = std::clamp(State.lanewidth, 1.0f, 10.0f); for (auto &lane : State.lanes) lane.width = State.lanewidth; changed = true; } float limit{State.velocity > 0.0f ? State.velocity : 0.0f}; if (number("Speed limit [km/h], 0: none", limit, 10.0f, "%.0f")) { State.velocity = (limit > 0.0f ? limit : -1.0f); for (auto &lane : State.lanes) lane.velocity = State.velocity; changed = true; } // appearance ImGui::Separator(); char const *colours[]{"none", "white", "orange"}; int colour{State.markings == road_node::marking_colour::white ? 1 : State.markings == road_node::marking_colour::orange ? 2 : 0}; ImGui::SetNextItemWidth(120.0f); if (ImGui::Combo("Markings", &colour, colours, 3)) { State.markings = (colour == 1 ? road_node::marking_colour::white : colour == 2 ? road_node::marking_colour::orange : road_node::marking_colour::none); changed = true; } if (material("Surface", tool.surface, sizeof(tool.surface), State.surface)) changed = true; if (number("Texture length [m]", State.texturelength, 0.5f, "%.2f")) { State.texturelength = std::max(0.1f, State.texturelength); changed = true; } char const *sidenames[]{"Left side", "Right side"}; char const *sidetypes[]{"none", "shoulder", "sidewalk"}; for (int side = 0; side < 2; ++side) { ImGui::PushID(side); auto &data{State.sides[side]}; int type{data.type == road_node::side_type::shoulder ? 1 : data.type == road_node::side_type::sidewalk ? 2 : 0}; ImGui::SetNextItemWidth(120.0f); if (ImGui::Combo(sidenames[side], &type, sidetypes, 3)) { data.type = (type == 1 ? road_node::side_type::shoulder : type == 2 ? road_node::side_type::sidewalk : road_node::side_type::none); if (data.type != road_node::side_type::none && data.width <= 0.0f) data.width = 1.5f; changed = true; } if (data.type != road_node::side_type::none) { ImGui::Indent(); if (number("Width [m]", data.width, 0.25f, "%.2f")) { data.width = std::clamp(data.width, 0.1f, 20.0f); changed = true; } if (material("Material", tool.sides[side], sizeof(tool.sides[side]), data.material)) changed = true; if (data.material.empty() || data.material == "none") ImGui::TextDisabled("Pick a material to have the side drawn"); ImGui::Unindent(); } ImGui::PopID(); } if (State.sides[0].type == road_node::side_type::sidewalk || State.sides[1].type == road_node::side_type::sidewalk) { if (number("Kerb height [m]", State.kerbheight, 0.01f, "%.2f")) { State.kerbheight = std::clamp(State.kerbheight, 0.0f, 1.0f); changed = true; } } if (State.sides[0].type == road_node::side_type::shoulder || State.sides[1].type == road_node::side_type::shoulder) { if (number("Bank width [m]", State.slope.x, 0.25f, "%.2f")) { State.slope.x = std::clamp(State.slope.x, 0.0f, 20.0f); changed = true; } if (number("Bank drop [m]", State.slope.y, 0.1f, "%.2f")) { State.slope.y = std::clamp(State.slope.y, 0.0f, 20.0f); changed = true; } } // single lanes, and the rules for changing between them if (ImGui::CollapsingHeader("Lanes and lane changes", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::TextDisabled("Left to right when facing along the axis. f: along the axis, b: against it"); for (std::size_t lane = 0; lane < State.lanes.size(); ++lane) { ImGui::PushID(static_cast(lane)); auto &data{State.lanes[lane]}; auto const id{State.lane_id(lane)}; ImGui::TextColored(lane >= static_cast(State.backward) ? ImVec4(0.3f, 0.9f, 0.45f, 1.0f) : ImVec4(1.0f, 0.67f, 0.2f, 1.0f), "%-3s", id.c_str()); ImGui::SameLine(); if (number("m##width", data.width, 0.25f, "%.2f")) { data.width = std::clamp(data.width, 1.0f, 10.0f); changed = true; } ImGui::SameLine(); float lanelimit{data.velocity > 0.0f ? data.velocity : 0.0f}; if (number("km/h##velocity", lanelimit, 10.0f, "%.0f")) { data.velocity = (lanelimit > 0.0f ? lanelimit : -1.0f); changed = true; } if (lane + 1 < State.lanes.size() && lane < State.changes.size()) { // the entries are in the order of the stored values: none, to the right, to the left, both auto const next{State.lane_id(lane + 1)}; auto const toright{"only " + id + " to " + next}; auto const toleft{"only " + next + " to " + id}; char const *modes[]{"no lane change", toright.c_str(), toleft.c_str(), "lane change both ways"}; ImGui::Indent(); ImGui::SetNextItemWidth(200.0f); if (ImGui::Combo("##change", &State.changes[lane], modes, 4)) changed = true; ImGui::Unindent(); } ImGui::PopID(); } } return changed; } void editor_mode::render_road_window() { auto &tool{m_roadtool}; if (false == tool.window) return; ImGui::SetNextWindowSize(ImVec2(440.0f, 620.0f), ImGuiCond_FirstUseEver); if (false == ImGui::Begin("Roads###roadeditor", &tool.window)) { ImGui::End(); return; } std::string reason; if (false == editor_road::available(&reason)) ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", reason.c_str()); auto const previoustool{tool.tool}; 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; if (tool.tool != 0) { tool.selected = nullptr; tool.junction = nullptr; } } 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) { 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::Separator(); // what's set here is what the next pieces get render_road_layout(tool.settings); } else if (tool.tool == 2) { 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."); } 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); if (ImGui::Button("Delete (Del)")) road_delete(); } else if (tool.selected != nullptr) { auto const &road{*tool.selected}; ImGui::Text("%s, %.1f m, %d + %d lanes", road.name().c_str(), road.definition().length(), road.definition().forward, road.definition().backward); if (ImGui::Button("Delete (Del)")) road_delete(); ImGui::SameLine(); ImGui::TextDisabled("K: split at the cursor"); ImGui::Checkbox("Changes go to the whole road", &tool.whole); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nSplit a piece (K) to get a zone with rules of its own."); ImGui::Separator(); if (tool.selected != nullptr && render_road_layout(tool.settings)) road_apply(); } else { ImGui::TextDisabled("LMB: select a road piece or a junction"); } if (false == tool.status.empty()) { ImGui::Separator(); ImGui::TextWrapped("%s", tool.status.c_str()); } ImGui::End(); } void editor_mode::draw_road_overlay() const { auto const &tool{m_roadtool}; if (false == tool.lanes && false == tool.window) return; screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); glm::dvec3 const camera{Global.pCamera.Pos}; auto const drawpath = [&](segment_data const &Path, ImU32 const Color, float const Thickness, int const Samples) { auto previous{path_point(Path, 0.0)}; for (int i = 1; i <= Samples; ++i) { auto const next{path_point(Path, static_cast(i) / Samples)}; projection.line(drawlist, previous, next, Color, Thickness); previous = next; } }; // lanes, as the vehicles see them: a line for each, with an arrow pointing where the traffic goes if (tool.lanes) { for (auto const *road : simulation::Roads.sequence()) { if (road == nullptr || road->m_editorremoved) continue; auto const range{glm::distance(road->location(), camera)}; if (range > kLaneRange + 0.5 * road->definition().length()) continue; auto const samples{is_curved(road->definition().axis) ? (range < 150.0 ? 12 : 6) : 1}; auto const against{static_cast(road->definition().backward)}; auto const &tracks{road->tracks()}; for (std::size_t lane = 0; lane < tracks.size(); ++lane) { if (tracks[lane] == nullptr || tracks[lane]->m_paths.empty()) continue; auto const &path{tracks[lane]->m_paths.front()}; auto const color{lane >= against ? kForwardColor : kBackwardColor}; drawpath(path, color, 2.0f, samples); if (range > 300.0) continue; auto const middle{path_point(path, 0.5)}; auto direction{path_point(path, 0.52) - middle}; if (glm::length(direction) < 1e-6) continue; direction = glm::normalize(direction); glm::dvec3 const across{direction.z, 0.0, -direction.x}; auto const tip{middle + direction * 1.2}; projection.line(drawlist, tip, middle - direction * 0.6 + across * 0.6, color, 2.0f); projection.line(drawlist, tip, middle - direction * 0.6 - across * 0.6, color, 2.0f); } for (auto const *turn : road->turns()) { if (turn != nullptr && false == turn->m_paths.empty()) drawpath(turn->m_paths.front(), kTurnColor, 1.5f, 8); } } // ways through the junctions for (auto const *junction : simulation::Junctions.sequence()) { if (junction == nullptr || junction->m_editorremoved || glm::distance(junction->location(), camera) > kLaneRange) continue; for (auto const &way : junction->movements()) drawpath(way, kWayColor, 1.5f, 8); } } if (false == tool.window) return; // 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()}; drawpath(State.axis, Color, 3.0f, samples); glm::dvec3 previous[2]; for (int i = 0; i <= samples; ++i) { auto const t{static_cast(i) / samples}; auto const position{path_point(State.axis, t)}; auto const tangent{State.tangent(t)}; glm::dvec3 across{tangent.z, 0.0, -tangent.x}; if (glm::length(across) > 1e-6) across = glm::normalize(across); glm::dvec3 const edges[2]{position + across * halfwidth, position - across * halfwidth}; if (i > 0) { projection.line(drawlist, previous[0], edges[0], Color, 1.5f); projection.line(drawlist, previous[1], edges[1], Color, 1.5f); } previous[0] = edges[0]; previous[1] = edges[1]; } }; auto const drawpoint = [&](glm::dvec3 const &Point, ImU32 const Color, float const Radius, bool const Filled) { ImVec2 screen; if (false == projection.project(Point, screen)) return; if (Filled) drawlist->AddCircleFilled(screen, Radius, Color); else drawlist->AddCircle(screen, Radius, Color, 20, 3.0f); }; // outline of a junction: the edge of its surface, with the ends of the roads it's made for auto const drawjunction = [&](junction_node::state const &State, ImU32 const Color) { auto const outline{State.outline()}; for (std::size_t i = 0; i < outline.size(); ++i) projection.line(drawlist, outline[i], outline[(i + 1) % outline.size()], Color, 2.0f); for (auto const &arm : State.arms) drawpoint(arm.position, Color, 5.0f, true); }; if (tool.selected != nullptr && false == tool.selected->m_editorremoved) { auto const &state{tool.selected->definition()}; drawroad(state, kSelectedColor); drawpoint(state.axis.points[segment_data::point::start], kSelectedColor, 6.0f, true); drawpoint(state.axis.points[segment_data::point::end], kSelectedColor, 6.0f, true); } if (tool.junction != nullptr && false == tool.junction->m_editorremoved) drawjunction(tool.junction->definition(), kSelectedColor); std::string hint; if (tool.tool == 1) { if (tool.chain) { drawpoint(tool.point, kPreviewColor, 6.0f, true); if (tool.preview.empty()) { projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f); hint = tool.previewerror + " "; } for (auto const &piece : tool.preview) { 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"; } 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"; } } 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"; } // shown above the place the track tools put their hints at ImGuiIO const &io = ImGui::GetIO(); ImVec2 const position{12.0f, io.DisplaySize.y - 56.0f}; auto const size{ImGui::CalcTextSize(hint.c_str())}; drawlist->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 150), 4.0f); drawlist->AddText(position, IM_COL32(255, 255, 255, 230), hint.c_str()); }