17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02:00
Files
maszyna/application/editormoderoad.cpp

840 lines
28 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "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 <algorithm>
#include <cmath>
#include <cstring>
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<char>(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
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 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 (false == tool.window)
{
tool.chain = false;
return;
}
tool.mouse = Global.pCamera.Pos + GfxRenderer->Mouse_Position();
}
void editor_mode::road_select(road_node *Road)
{
auto &tool{m_roadtool};
tool.selected = Road;
if (Road != 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::road_cancel()
{
m_roadtool.chain = false;
}
std::vector<segment_data> editor_mode::road_preview(editor_road::loose_end &Snap, std::string &Error) const
{
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.road != nullptr && glm::distance(Snap.position, tool.point) < 0.5)
Snap = {};
glm::dvec2 enddirection{0.0, 1.0};
if (Snap.road != nullptr)
{
glm::dvec2 const outwards{Snap.outwards.x, Snap.outwards.z};
if (glm::length(outwards) > 1e-6)
enddirection = -glm::normalize(outwards);
else
Snap = {};
}
return editor_road::plan(tool.point, tool.hasdirection ? &tool.direction : nullptr, Snap.road != nullptr ? Snap.position : tool.mouse, Snap.road != nullptr ? &enddirection : nullptr, Global.ctrlState, Error);
}
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)};
if (road == nullptr)
tool.status = "No road here";
road_select(road);
return;
}
std::string reason;
if (false == editor_road::available(&reason))
{
tool.status = reason;
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
auto const end{editor_road::find_end(ground, kSnapRadius)};
glm::dvec2 const outwards{end.outwards.x, end.outwards.z};
if (end.road != nullptr && glm::length(outwards) > 1e-6)
{
tool.point = end.position;
tool.direction = glm::normalize(outwards);
tool.hasdirection = true;
// 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);
auto const layout{end.road->definition()};
tool.selected = nullptr;
tool.settings = layout;
road_select(nullptr);
tool.status = "Carrying on \"" + end.road->name() + "\" with its layout";
}
else
{
tool.point = ground;
tool.hasdirection = false;
tool.reversed = false;
tool.selected = nullptr;
tool.status = "Start set";
}
tool.chain = true;
return;
}
editor_road::loose_end snap;
std::string error;
auto pieces{road_preview(snap, error)};
if (pieces.empty())
{
tool.status = "Can't build this: " + error;
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]};
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<road_node::state> 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.road != nullptr)
{
tool.chain = false;
tool.status = "Joined with \"" + snap.road->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.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<road_node *>{tool.selected}};
std::vector<std::pair<road_node *, road_node::state>> before;
std::vector<std::pair<road_node *, road_node::state>> 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)
return false;
auto *road{tool.selected};
std::string reason;
if (false == editor_road::can_edit(*road, &reason))
{
tool.status = "Can't delete: " + reason;
return true;
}
editor_road::remove({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<std::pair<road_node *, road_node::state>> States, std::vector<road_node *> Created, std::vector<road_node *> Removed)
{
if (States.empty() && Created.empty() && Removed.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));
auto const *road = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front();
EditorSnapshot snap;
snap.action = EditorSnapshot::Action::RoadEdit;
snap.node_name = road->name();
snap.position = road->location();
snap.roads = std::move(States);
snap.roads_created = std::move(Created);
snap.roads_removed = std::move(Removed);
m_history.push_back(std::move(snap));
g_redo.clear();
}
void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo)
{
if (Snapshot.roads.empty() && Snapshot.roads_created.empty() && Snapshot.roads_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::revive(Snapshot.roads_removed);
}
else
{
editor_road::revive(Snapshot.roads_created);
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);
}
// 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<std::string const *> 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<int>(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<int>(lane));
auto &data{State.lanes[lane]};
auto const id{State.lane_id(lane)};
ImGui::TextColored(lane >= static_cast<std::size_t>(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);
if (tool.tool != previoustool)
{
tool.chain = false;
if (tool.tool == 1)
tool.selected = nullptr;
}
ImGui::SameLine();
ImGui::Checkbox("Show lanes", &tool.lanes);
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");
if (tool.chain)
{
if (ImGui::Button("Finish (Esc)"))
tool.chain = false;
}
ImGui::Separator();
// what's set here is what the next pieces get
render_road_layout(tool.settings);
}
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");
}
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<double>(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<std::size_t>(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);
}
}
}
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<double>(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);
};
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);
}
std::string hint;
if (tool.tool == 1)
{
if (tool.chain)
{
editor_road::loose_end snap;
std::string error;
auto const pieces{road_preview(snap, error)};
drawpoint(tool.point, kPreviewColor, 6.0f, true);
if (pieces.empty())
{
projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f);
hint = error + " ";
}
for (auto const &piece : pieces)
{
auto state{tool.settings};
state.axis = piece;
drawroad(state, kPreviewColor);
}
if (snap.road != nullptr)
drawpoint(snap.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.road != nullptr)
drawpoint(end.position, kSnapColor, 14.0f, false);
hint = "LMB: set where the road starts; on a loose end of a road (ring) to carry it on";
}
}
else
{
hint = tool.selected != nullptr ? "LMB: select a road piece K: split at the cursor Del: delete Ctrl+Z: undo" : "LMB: select a road piece";
}
// 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());
}