17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 00:29:19 +02:00
Files
maszyna/application/editormodetrackux.cpp
2026-10-09 15:37:23 +02:00

1383 lines
48 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 "application/editorprojection.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "utilities/Globals.h"
#include "rendering/renderer.h"
#include "world/Track.h"
#include "scene/scene.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include "imgui/ImGuizmo.h"
#include <algorithm>
#include <unordered_set>
#include <unordered_map>
#include <queue>
#include <array>
#include <functional>
#include <cctype>
#include <chrono>
#include <cmath>
namespace
{
using geometry::bezier;
float const kHoverPixels{12.0f};
double const kHoverLabelDelay{0.35}; // s the cursor rests on a path before its data shows up
double const kFocusDistance{25.0};
auto const kContextClick{std::chrono::milliseconds(350)};
float const kContextTurn{0.02f}; // rad the camera may turn during a click of the right button
std::size_t const kSearchLimit{200};
int const kRailCategory{1};
bool viewport_click()
{
return false == ImGui::GetIO().MouseDownOwned[0];
}
bool rail(TTrack const &Track)
{
return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && Track.visible();
}
std::string name_of(TTrack const &Track)
{
return Track.name().empty() ? std::string{"(noname)"} : Track.name();
}
std::string lower(std::string Text)
{
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
return Text;
}
char const *type_of(TTrack const &Track)
{
switch (Track.eType)
{
case tt_Normal: return "normal";
case tt_Switch: return "switch";
case tt_Cross: return "cross";
case tt_Table: return "turntable";
case tt_Tributary: return "tributary";
default: return "unknown";
}
}
double segment_distance2(glm::vec2 const &Point, glm::vec2 const &A, glm::vec2 const &B, double &T)
{
auto const ab{B - A};
auto const length2{glm::dot(ab, ab)};
T = length2 > 1e-6f ? std::clamp(static_cast<double>(glm::dot(Point - A, ab) / length2), 0.0, 1.0) : 0.0;
auto const closest{A + ab * static_cast<float>(T)};
return glm::dot(Point - closest, Point - closest);
}
} // namespace
editor_mode::track_hover editor_mode::track_under_cursor() const
{
track_hover result;
auto const &io{ImGui::GetIO()};
screen_projection const projection;
glm::vec2 const mouse{io.MousePos.x, io.MousePos.y};
double best{kHoverPixels * kHoverPixels};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track) || track->m_editorremoved)
continue;
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
{
bezier const curve{track->m_paths[i]};
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 3.0), 6, 64)};
auto previous{curve.point(0.0)};
ImVec2 from;
bool visible{projection.project(previous, from)};
for (int k = 1; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
ImVec2 to;
bool const shown{projection.project(next, to)};
if (visible && shown)
{
double t;
auto const distance{segment_distance2(mouse, {from.x, from.y}, {to.x, to.y}, t)};
if (distance < best)
{
best = distance;
result.track = track;
result.path = i;
result.point = glm::mix(previous, next, t);
}
}
previous = next;
from = to;
visible = shown;
}
}
}
return result;
}
void editor_mode::update_track_hover()
{
if (typed_meaning() == nullptr)
m_typed.clear();
bool const blocked{ImGui::GetIO().WantCaptureMouse || mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing ||
m_straights.dragging || m_lay.active};
if (blocked)
{
m_hover = {};
return;
}
auto found{track_under_cursor()};
found.since = found.track == m_hover.track ? m_hover.since : ImGui::GetTime();
m_hover = found;
}
void editor_mode::draw_track_hover() const
{
using kind = track_intent::kind;
auto const *track{m_hover.track};
if (track == nullptr || (m_intent.what != kind::select && m_intent.what != kind::box && m_intent.what != kind::straight_set) || m_intent.track != track)
return;
screen_projection const projection;
if (track != selected_track())
{
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
{
bezier const curve{track->m_paths[i]};
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 3.0), 6, 64)};
auto const colour{i == m_hover.path ? IM_COL32(255, 255, 255, 200) : IM_COL32(255, 255, 255, 110)};
auto previous{curve.point(0.0)};
for (int k = 1; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
projection.line(drawlist, previous, next, colour, i == m_hover.path ? 3.5f : 2.0f);
previous = next;
}
}
}
}
bool editor_mode::track_gesture_on() const
{
return mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing || m_straights.dragging || m_straights.tool_mouse ||
m_point_drag.active || m_handle_drag.active || m_sweep.dragging || m_turntable.placing || m_signal.placing || m_signal.moving || ImGuizmo::IsUsing();
}
editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
{
using kind = track_intent::kind;
track_intent intent;
if (ImGui::GetIO().WantCaptureMouse)
return intent;
glm::dvec3 const ground{cursor_ground()};
intent.position = ground;
bool const shift{(Mods & GLFW_MOD_SHIFT) != 0};
bool const control{(Mods & GLFW_MOD_CONTROL) != 0};
bool const alt{(Mods & GLFW_MOD_ALT) != 0};
auto const make = [&](kind const What, char const *Action) {
intent.what = What;
intent.action = Action != nullptr ? STR(Action) : std::string{};
return intent;
};
auto *selected{selected_track()};
if (m_lay.active)
{
auto const &lay{m_lay};
if (lay.mouse_snap.track != nullptr && (lay.points.empty() || glm::distance(lay.mouse_snap.position, lay.points.front()) > 1.0))
{
intent.snap = lay.mouse_snap;
intent.track = lay.mouse_snap.track;
intent.position = lay.mouse_snap.position;
auto const diverging{lay.mouse_snap.track->eType == tt_Switch && lay.mouse_snap.point == editor_track::point_ref{1, editor_track::point_kind::end}};
if (lay.points.empty() && diverging)
return make(kind::lay_end, "Click: start from this free end, drag: the curve of the switch goes on, Shift: straight");
return make(kind::lay_end, lay.points.empty() ? "Click: start from this free end, drag: lead a track out of it" : "Click: join this end and lay");
}
intent.position = lay.mouse;
return make(kind::lay, lay.points.empty() ? "Click: start of the new track" : "Click: next point");
}
if (m_track_tab == track_tab::turntable)
return turntable_intent(ground, shift);
if (m_track_tab == track_tab::signals)
{
if (auto const hit{signal_hit()}; hit >= 0)
{
auto const &standing{m_signal.standing[hit]};
intent.index = hit;
intent.position = standing.include.location;
intent.action = format(STR_C("Click: select the signal %s, drag: move it along the track"), standing.name.c_str());
intent.what = kind::signal_move;
return intent;
}
auto const *armed{signal_armed()};
if (m_hover.track != nullptr && armed != nullptr)
{
signal_spot spot;
if (signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot))
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = spot.position;
intent.action = signal_intent_label(*armed, spot);
intent.what = kind::signal_place;
return intent;
}
}
if (m_hover.track != nullptr)
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
return make(kind::select, "Choose the type of the signal in the Tracks window first");
}
return intent;
}
if (m_track_tab == track_tab::path)
{
if (shift && control && m_hover.track != nullptr && rail(*m_hover.track))
{
intent.track = m_hover.track;
intent.position = m_hover.point;
auto const &preview{track_spread_preview(*m_hover.track)};
intent.action = format(in_track_set(m_hover.track) ? STR_C("Click: take out the %zu joined paths, %.0f m") : STR_C("Click: add the %zu joined paths, %.0f m"), preview.size(), m_spread.length);
intent.what = kind::spread;
return intent;
}
if (shift && false == gizmo_frame::over())
{
intent.track = m_hover.track;
return make(kind::box, m_hover.track == nullptr ? "Drag: the tracks in a box join the selection" : in_track_set(m_hover.track) ? "Click: take out of the selection, drag: a box" : "Click: add to the selection, drag: a box");
}
if (auto const hit{track_handle_hit()}; hit.valid() && selected != nullptr)
{
std::string reason;
if (editor_track::can_edit_geometry(*selected, reason))
{
intent.track = selected;
intent.point = hit;
intent.position = editor_track::point_position(*selected, hit);
if (false == editor_track::is_end(hit.kind))
return make(kind::point, "Drag: the control vector, the curve follows");
if (editor_track::is_connected(*selected, hit))
return make(kind::point, "Drag: the joint, the neighbours follow");
if (control)
return make(kind::point, "Drag: the end, dropped on a free end it joins it");
intent.snap.track = selected;
intent.snap.point = hit;
intent.snap.position = intent.position;
auto const diverging{selected->eType == tt_Switch && hit == editor_track::point_ref{1, editor_track::point_kind::end}};
return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on, Shift: straight; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end");
}
}
}
if (m_track_tab == track_tab::straights)
{
auto const &line{current_straight()};
if (alt)
{
intent.track = m_hover.track;
return m_hover.track != nullptr ? make(kind::straight_set, "Click: move this straight together with the others") : intent;
}
if (control && false == line.tracks.empty())
return make(kind::straight_break, "Press: break the straight here, drag to turn the rest");
if (shift && false == line.tracks.empty())
return make(kind::detour, "Click: next point of the shift around an obstacle");
}
if (m_track_tab == track_tab::straights && m_straights.tool == 0)
{
if (auto const hit{straight_handle_hit()}; hit >= 0)
{
auto const &line{current_straight()};
glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5};
intent.index = hit;
intent.position = handles[hit];
if (hit == 2)
{
auto const offer{straight_parallel_offer(line, {})};
if (offer.nearby)
{
intent.action = format(offer.snaps ? STR_C("Drag: shift the straight sideways — snaps parallel at %.2f m") : STR_C("Drag: shift the straight sideways — close: %.2f m %.1f°"), offer.spacing, offer.angle);
intent.what = kind::straight_handle;
return intent;
}
}
return make(kind::straight_handle, hit == 2 ? "Drag: shift the straight sideways" : "Drag: move this end, the curves at it follow");
}
}
if (int vertex, grip; route_hit(vertex, grip))
{
intent.index = vertex >= 0 ? vertex : grip;
intent.position = vertex >= 0 ? route_vertex_position(vertex) : alignment::evaluate(m_route.result, m_route.result.vertex_chainages[grip]).position;
return make(vertex >= 0 ? kind::route_vertex : kind::route_grip, vertex >= 0 ? "Drag: move this vertex, the curve follows" : "Drag: the radius of this curve");
}
if (gizmo_frame::over())
return make(kind::gizmo, nullptr);
if (m_track_tab == track_tab::straights)
{
auto const &line{current_straight()};
if (m_straights.tool != 0 && false == line.tracks.empty())
{
intent.position = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * std::clamp(line.along(ground), 0.0, line.length);
return make(kind::straight_tool, m_straights.tool == 1 ? "Click: break the straight here" : "Click: start of the shift");
}
}
if (selected != nullptr && false == selected->m_paths.empty())
{
glm::dvec3 point;
if (switch_reach(cursor_level(selected->m_paths.front().points[segment_data::point::start].y), point))
{
intent.track = selected;
intent.position = point;
return make(kind::switch_at, "Press and drag: a switch here, along sets its direction, sideways its side");
}
}
if (m_hover.track != nullptr)
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
switch (m_track_tab)
{
case track_tab::straights: return make(kind::select, "Click: the whole straight through this track");
case track_tab::route: return make(kind::select, "Click: the curve of this track with its straights");
case track_tab::turnout: return make(kind::select, m_hover.track->eType == tt_Switch ? "Click: edit this switch" : m_switch.armed < 0 ? "Click: choose this track, then the type of the switch" : "Click: put a switch into this track");
case track_tab::profile: return make(kind::select, "Click: the grade line through this track");
case track_tab::lineside: return make(kind::select, "Click: the line the objects go along");
default: return make(kind::select, "Click: select");
}
}
return intent;
}
void editor_mode::update_track_intent()
{
using kind = track_intent::kind;
if (track_gesture_on())
{
m_intent = {};
return;
}
auto const &io{ImGui::GetIO()};
m_intent = track_intent_at((io.KeyShift ? GLFW_MOD_SHIFT : 0) | (io.KeyCtrl ? GLFW_MOD_CONTROL : 0) | (io.KeyAlt ? GLFW_MOD_ALT : 0));
switch (m_intent.what)
{
case kind::none:
case kind::gizmo: break;
case kind::point:
case kind::straight_handle:
case kind::route_vertex:
case kind::route_grip: ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); break;
default: ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); break;
}
}
void editor_mode::track_press(track_intent const &Intent)
{
using kind = track_intent::kind;
switch (Intent.what)
{
case kind::lay_end:
if (m_lay.points.empty() && start_extend(Intent.snap))
return;
[[fallthrough]];
case kind::lay:
GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) {
if (viewport_click())
lay_click();
});
return;
case kind::sweep: sweep_press(); return;
case kind::box: start_track_box(); return;
case kind::spread:
if (Intent.track != nullptr)
track_spread_apply(*Intent.track);
return;
case kind::point: point_drag_start(Intent.point); return;
case kind::gizmo: return;
case kind::straight_set:
{
auto *hovered{Intent.track};
GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) {
auto *track{hovered != nullptr ? hovered : dynamic_cast<TTrack *>(node)};
if (viewport_click() && track != nullptr)
toggle_straight_set(*track);
});
return;
}
case kind::straight_break: start_straight_gesture(1); return;
case kind::detour: add_detour_point(); return;
case kind::straight_tool: place_straight_tool(); return;
case kind::straight_handle:
m_straights.handle = Intent.index;
handle_drag_start(Intent.position);
return;
case kind::route_vertex:
m_route.vertex = Intent.index;
m_route.grip = -1;
handle_drag_start(Intent.position);
return;
case kind::route_grip:
m_route.vertex = -1;
m_route.grip = Intent.index;
handle_drag_start(Intent.position);
return;
case kind::switch_at: start_switch_placement(); return;
case kind::extend_end:
if (false == start_extend(Intent.snap))
point_drag_start(Intent.snap.point);
return;
case kind::turntable_place:
case kind::turntable_exit:
case kind::turntable_fit: turntable_press(Intent); return;
case kind::signal_place:
case kind::signal_move: signal_press(Intent); return;
case kind::select:
case kind::none:
{
auto *hovered{Intent.track};
GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) {
if (false == viewport_click())
return;
auto *track{hovered != nullptr ? hovered : dynamic_cast<TTrack *>(node)};
if (m_track_tab == track_tab::path)
{
m_track_set.clear();
if (track == nullptr)
{
m_node = nullptr;
m_track_point = {};
ui()->set_node(nullptr);
return;
}
}
select_track(track != nullptr ? track : node);
});
return;
}
}
}
void editor_mode::draw_track_intent() const
{
using kind = track_intent::kind;
auto const &intent{m_intent};
auto const &io{ImGui::GetIO()};
if (intent.what == kind::none || intent.what == kind::gizmo || io.WantCaptureMouse)
return;
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
ImVec2 at;
if (projection.project(intent.position, at))
{
switch (intent.what)
{
case kind::point:
case kind::straight_handle:
case kind::route_vertex:
case kind::route_grip: drawlist->AddCircle(at, 13.0f, overlay_color::marked, 24, 2.5f); break;
case kind::switch_at:
case kind::straight_tool:
drawlist->AddCircleFilled(at, 4.0f, overlay_color::grip);
drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f);
break;
case kind::lay_end:
case kind::extend_end:
case kind::turntable_fit: drawlist->AddCircleFilled(at, 5.0f, IM_COL32(255, 60, 255, 255)); break;
case kind::turntable_exit: drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f); break;
case kind::lay: drawlist->AddCircle(at, 6.0f, IM_COL32(255, 255, 255, 200), 12, 1.5f); break;
default: break;
}
}
if (intent.action.empty() || false == track_readout().empty())
return;
std::string details;
auto const *track{m_hover.track};
if (intent.what == kind::select && track != nullptr && track == intent.track && ImGui::GetTime() - m_hover.since >= kHoverLabelDelay)
{
auto const &path{track->m_paths[std::min<std::size_t>(m_hover.path, track->m_paths.size() - 1)]};
details = name_of(*track);
details += "\n" + std::string{STR_C(type_of(*track))};
if (track->eType == tt_Switch)
details += m_hover.path == 0 ? STR(" (main)") : STR(" (branch)");
details += format(" L %.1f m", bezier{path}.plan_length());
details += path.radius != 0.0f ? format(" R %.0f m", std::abs(path.radius)) : std::string{" "} + STR_C("straight");
auto const velocity{editor_track::velocity(*track)};
details += velocity > 0.0 ? format(" V %.0f km/h", velocity) : std::string{};
}
auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())};
auto const actionsize{ImGui::CalcTextSize(intent.action.c_str())};
auto const detailsize{details.empty() ? ImVec2(0.0f, 0.0f) : ImGui::CalcTextSize(details.c_str())};
ImVec2 const size{std::max(actionsize.x, detailsize.x), actionsize.y + (details.empty() ? 0.0f : detailsize.y + 4.0f)};
ImVec2 position{io.MousePos.x + 18.0f, io.MousePos.y + 18.0f};
position.x = std::min(position.x, io.DisplaySize.x - size.x - 10.0f);
position.y = std::min(position.y, io.DisplaySize.y - size.y - 10.0f);
foreground->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, 170), 4.0f);
foreground->AddText(position, overlay_color::marked, intent.action.c_str());
if (false == details.empty())
foreground->AddText(ImVec2(position.x, position.y + actionsize.y + 4.0f), IM_COL32(255, 255, 255, 235), details.c_str());
}
void editor_mode::track_context_press()
{
auto &context{m_track_context};
auto const &io{ImGui::GetIO()};
context.armed = m_hover.track != nullptr;
context.target = m_hover;
context.pressed = std::chrono::steady_clock::now();
context.angle = Camera.Angle;
context.mouse = {io.MousePos.x, io.MousePos.y};
}
void editor_mode::track_context_release()
{
auto &context{m_track_context};
if (false == context.armed)
return;
context.armed = false;
if (std::chrono::steady_clock::now() - context.pressed < kContextClick && glm::length(Camera.Angle - context.angle) < kContextTurn)
context.pending = true;
}
void editor_mode::split_track_at(TTrack &Track, glm::dvec3 const &Point)
{
auto const parameter{editor_track::nearest_parameter(Track, Point)};
auto const before{editor_track::capture(Track)};
if (auto *created{editor_track::split_path(Track, parameter)})
{
push_track_snapshot({{&Track, before}}, {created});
straight_refresh();
}
}
void editor_mode::focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point)
{
auto const &path{Track.m_paths[std::min<std::size_t>(std::max(Path, 0), Track.m_paths.size() - 1)]};
bezier const curve{path};
glm::dvec3 look{curve.first(std::clamp(editor_track::nearest_parameter(Track, Point), 0.0, 1.0))};
look.y = 0.0;
look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0);
if (glm::dot(Camera.Pos - Point, look) > 0.0)
look = -look;
look = glm::normalize(look - glm::dvec3(0.0, 0.25, 0.0));
m_focus_start_pos = Camera.Pos;
m_focus_start_angle = Camera.Angle;
m_focus_target_pos = Point - look * kFocusDistance;
m_focus_target_angle = glm::vec3(static_cast<float>(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast<float>(std::atan2(-look.x, -look.z)), 0.0f);
m_focus_active = true;
m_focus_time = 0.0;
m_focus_duration = 0.6;
}
void editor_mode::render_track_context()
{
auto &context{m_track_context};
auto *track{context.target.track};
if (context.pending)
{
context.pending = false;
if (track != nullptr && false == track->m_editorremoved)
{
select_track(track);
ImGui::OpenPopup("##trackcontext");
ImGui::SetNextWindowPos(ImVec2(context.mouse.x, context.mouse.y));
}
}
if (false == ImGui::BeginPopup("##trackcontext"))
return;
if (track == nullptr || track->m_editorremoved)
{
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
return;
}
auto const point{context.target.point};
bool const normal{track->eType == tt_Normal && editor_track::is_supported(*track)};
ImGui::TextDisabled("%s (%s, %.1f m)", name_of(*track).c_str(), STR_C(type_of(*track)), track->Length());
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Split here"), "K", false, normal))
split_track_at(*track, point);
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Switch"), "T", false, normal || track->eType == tt_Switch))
show_track_tab(track_tab::turnout);
if (ImGui::MenuItem(STR_C("Whole straight"), "G", false, normal && editor_track::is_straight(*track, m_straights.tolerance)))
show_track_tab(track_tab::straights);
if (ImGui::MenuItem(STR_C("Curve"), "C", false, normal))
show_track_tab(track_tab::route);
if (ImGui::MenuItem(STR_C("Parallel track"), "L", false, normal))
show_track_tab(track_tab::lay);
if (ImGui::MenuItem(STR_C("Place a vehicle here")))
{
show_track_tab(track_tab::lineside);
vehicle_start(*track, point);
}
if (ImGui::MenuItem(STR_C("Hectometre posts from here")))
{
show_track_tab(track_tab::lineside);
hekto_start(*track, point);
}
if (ImGui::MenuItem(STR_C("Fouling point markers"), nullptr, false, track->eType == tt_Switch))
{
show_track_tab(track_tab::lineside);
m_fouling.scope = 0;
m_fouling.dirty = true;
m_fouling.expand = true;
}
if (ImGui::MenuItem(STR_C("Vertical profile of the line"), "P"))
{
show_track_tab(track_tab::profile);
profile_open_run(*track);
}
if (ImGui::MenuItem(STR_C("Speed check of the line"), "V"))
{
m_speed.scope = 0;
show_track_tab(track_tab::speed);
speed_start();
}
ImGui::Separator();
if (ImGui::MenuItem(in_track_set(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB"))
track_set_toggle(*track);
if (ImGui::MenuItem(STR_C("Look at it")))
focus_track(*track, context.target.path, point);
if (ImGui::MenuItem(STR_C("Copy the name"), nullptr, false, false == track->name().empty()))
ImGui::SetClipboardText(track->name().c_str());
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Delete"), "Del"))
delete_selected_track();
ImGui::EndPopup();
}
void editor_mode::render_track_search()
{
auto &search{m_track_search};
if (search.focus)
{
ImGui::SetKeyboardFocusHere();
search.focus = false;
}
ImGui::SetNextItemWidth(-1.0f);
ImGui::InputTextWithHint("##findtrack", STR_C("Find a track by its name (Ctrl+F)"), search.text, sizeof(search.text));
std::string const text{lower(search.text)};
if (text.empty())
{
search.searched.clear();
search.found.clear();
return;
}
if (text != search.searched)
{
search.searched = text;
search.found.clear();
search.total = 0;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track) || lower(track->name()).find(text) == std::string::npos)
continue;
++search.total;
if (search.found.size() < kSearchLimit)
search.found.push_back(track);
}
}
if (search.found.empty())
{
ImGui::TextDisabled("%s", STR_C("No track of that name"));
return;
}
if (search.total > search.found.size())
ImGui::TextDisabled(STR_C("%zu found, the first %zu listed"), search.total, search.found.size());
auto const rows{std::min<std::size_t>(search.found.size(), 8)};
ImGui::BeginChild("##foundtracks", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * static_cast<float>(rows) + 6.0f), true);
for (std::size_t i = 0; i < search.found.size(); ++i)
{
auto *track{search.found[i]};
if (track->m_editorremoved)
continue;
auto const label{format("%s##found%zu", name_of(*track).c_str(), i)};
if (ImGui::Selectable(label.c_str(), track == selected_track()))
{
m_node = track;
m_track_point = {};
ui()->set_node(m_node);
focus_track(*track, 0, bezier{track->m_paths.front()}.point(0.5));
}
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x * 0.6f);
ImGui::TextDisabled("%s, %.1f m", STR_C(type_of(*track)), track->Length());
}
ImGui::EndChild();
}
char const *editor_mode::typed_meaning() const
{
auto const &straights{m_straights};
if (m_extend.active)
return m_extend_freehand && false == ImGui::GetIO().KeyCtrl ? "type the radius, a space, the length" : "type the length";
if (straights.dragging)
return straights.handle == 2 ? "type the shift, + to the right" : "type the length";
if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging))
return "type the turn in degrees, + to the right";
if (m_lay.active && false == m_lay.points.empty())
return "type the length of the next leg, Enter adds the point";
return nullptr;
}
std::vector<double> editor_mode::typed_values() const
{
std::vector<double> values;
std::string text{m_typed};
std::replace(text.begin(), text.end(), ',', '.');
std::size_t at{0};
while (at < text.size())
{
auto const end{text.find(' ', at)};
auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)};
if (false == word.empty() && word != "-" && word != ".")
values.push_back(std::strtod(word.c_str(), nullptr));
if (end == std::string::npos)
break;
at = end + 1;
}
return values;
}
bool editor_mode::typed_key(int const Key)
{
if (typed_meaning() == nullptr)
{
m_typed.clear();
return false;
}
char character{'\0'};
if (Key >= GLFW_KEY_0 && Key <= GLFW_KEY_9)
character = static_cast<char>('0' + (Key - GLFW_KEY_0));
else if (Key >= GLFW_KEY_KP_0 && Key <= GLFW_KEY_KP_9)
character = static_cast<char>('0' + (Key - GLFW_KEY_KP_0));
else if (Key == GLFW_KEY_PERIOD || Key == GLFW_KEY_COMMA || Key == GLFW_KEY_KP_DECIMAL)
character = '.';
else if (Key == GLFW_KEY_MINUS || Key == GLFW_KEY_KP_SUBTRACT)
character = '-';
else if (Key == GLFW_KEY_SPACE)
character = ' ';
else if (Key == GLFW_KEY_BACKSPACE && false == m_typed.empty())
{
m_typed.pop_back();
return true;
}
else
return false;
auto const start{m_typed.empty() || m_typed.back() == ' '};
if ((character == '-' && false == start) || (character == ' ' && start) || (character == '.' && m_typed.substr(m_typed.rfind(' ') == std::string::npos ? 0 : m_typed.rfind(' ')).find('.') != std::string::npos))
return true;
if (m_typed.size() < 24)
m_typed += character;
return true;
}
void editor_mode::typed_extend()
{
auto const values{typed_values()};
if (values.empty())
return;
auto &tool{m_extend};
auto const &direction{tool.direction};
if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl)
{
auto const length{std::max(0.0, values[0])};
tool.mouse = {tool.point.x + direction.x * length, tool.mouse.y, tool.point.z + direction.y * length};
return;
}
auto const radius{std::abs(values[0])};
if (radius < 1.0)
return;
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
double const side{geometry::cross(direction, offset) >= 0.0 ? 1.0 : -1.0};
glm::dvec2 const point{tool.point.x, tool.point.z};
glm::dvec2 const centre{point + glm::dvec2{-direction.y, direction.x} * (side * radius)};
auto const radial{point - centre};
auto angle{values.size() > 1 ? std::abs(values[1]) / radius : side * geometry::signed_angle(radial, glm::dvec2{tool.mouse.x, tool.mouse.z} - centre)};
if (angle < 0.0)
angle += 2.0 * glm::pi<double>();
angle = std::clamp(angle, 1e-3, 2.0 * glm::pi<double>() - 0.1);
auto const end{centre + geometry::turned(radial, side * angle)};
tool.mouse = {end.x, tool.mouse.y, end.y};
}
glm::dvec3 editor_mode::typed_lay(glm::dvec3 const &Mouse) const
{
auto const values{typed_values()};
if (values.empty() || m_lay.points.empty() || values[0] <= 0.0)
return Mouse;
auto const &last{m_lay.points.back()};
glm::dvec2 heading{Mouse.x - last.x, Mouse.z - last.z};
if (glm::length(heading) < 1e-6)
return Mouse;
heading = glm::normalize(heading) * values[0];
return {last.x + heading.x, Mouse.y, last.z + heading.y};
}
void editor_mode::typed_turn()
{
auto &state{m_straights};
auto const values{typed_values()};
if (values.empty() || state.tool != 1)
return;
auto const reach{std::max(1.0, glm::length(glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}))};
auto const heading{geometry::turned(state.tool_line.direction, glm::radians(values[0])) * reach};
state.tool_handle = {state.tool_point.x + heading.x, state.tool_handle.y, state.tool_point.z + heading.y};
}
bool editor_mode::in_track_set(TTrack const *Track) const
{
return std::find(m_track_set.begin(), m_track_set.end(), Track) != m_track_set.end();
}
void editor_mode::track_set_prune()
{
m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }), m_track_set.end());
}
void editor_mode::track_set_toggle(TTrack &Track)
{
track_set_prune();
if (m_track_set.empty())
if (auto *selected{selected_track()}; selected != nullptr && selected != &Track)
m_track_set.push_back(selected);
if (auto const found{std::find(m_track_set.begin(), m_track_set.end(), &Track)}; found != m_track_set.end())
m_track_set.erase(found);
else
m_track_set.push_back(&Track);
m_node = m_track_set.empty() ? nullptr : m_track_set.back();
m_track_point = {};
ui()->set_node(m_node);
}
void editor_mode::start_track_box()
{
auto const &io{ImGui::GetIO()};
m_track_box.active = true;
m_track_box.from = m_track_box.to = {io.MousePos.x, io.MousePos.y};
m_track_box.hovered = m_hover.track;
}
void editor_mode::finish_track_box()
{
auto &box{m_track_box};
if (false == box.active)
return;
box.active = false;
auto const &io{ImGui::GetIO()};
box.to = {io.MousePos.x, io.MousePos.y};
if (glm::length(box.to - box.from) < 6.0f)
{
if (box.hovered != nullptr && false == box.hovered->m_editorremoved)
track_set_toggle(*box.hovered);
return;
}
glm::vec2 const low{std::min(box.from.x, box.to.x), std::min(box.from.y, box.to.y)};
glm::vec2 const high{std::max(box.from.x, box.to.x), std::max(box.from.y, box.to.y)};
screen_projection const projection;
track_set_prune();
if (m_track_set.empty())
if (auto *selected{selected_track()}; selected != nullptr)
m_track_set.push_back(selected);
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track) || in_track_set(track))
continue;
bool inside{false};
for (auto const &path : track->m_paths)
{
bezier const curve{path};
for (int k = 0; k <= 8 && false == inside; ++k)
{
ImVec2 screen;
if (projection.project(curve.point(k / 8.0), screen))
inside = screen.x >= low.x && screen.x <= high.x && screen.y >= low.y && screen.y <= high.y;
}
if (inside)
break;
}
if (inside)
m_track_set.push_back(track);
}
if (false == m_track_set.empty())
{
m_node = m_track_set.back();
m_track_point = {};
ui()->set_node(m_node);
}
}
std::vector<TTrack *> editor_mode::track_spread_from(TTrack &Start) const
{
auto const &rules{m_spread};
auto const bed = [](TTrack const &Track) { return Track.eType == tt_Switch ? (Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle) : Track.m_material2; };
auto const startspeed{editor_track::velocity(Start)};
auto const alike = [&](TTrack const &Track) {
if (rules.same_rails && Track.m_material1 != Start.m_material1)
return false;
if (rules.same_bed && bed(Track) != bed(Start))
return false;
if (rules.same_speed && std::abs(editor_track::velocity(Track) - startspeed) > 0.5)
return false;
return true;
};
auto const exits = [&](TTrack const &Track) {
std::vector<glm::dvec3> result;
if (Track.m_paths.empty())
return result;
auto const &main{Track.m_paths.front()};
result = {main.points[segment_data::point::start], main.points[segment_data::point::end]};
if (Track.eType == tt_Switch && rules.stop == 2)
for (std::size_t i = 1; i < Track.m_paths.size(); ++i)
result.push_back(Track.m_paths[i].points[segment_data::point::end]);
return result;
};
std::size_t constexpr limit{20000};
std::vector<TTrack *> result;
std::unordered_map<TTrack const *, double> reached{{&Start, 0.0}};
std::unordered_set<TTrack const *> done;
using entry = std::pair<double, TTrack *>;
std::priority_queue<entry, std::vector<entry>, std::greater<entry>> queue;
queue.push({0.0, &Start});
while (false == queue.empty() && result.size() < limit)
{
auto const [distance, track] = queue.top();
queue.pop();
if (false == done.insert(track).second)
continue;
result.push_back(track);
auto const further{distance + track->Length()};
if (rules.reach > 0.f && further > static_cast<double>(rules.reach))
continue;
for (auto const &end : exits(*track))
for (auto const &[other, point] : editor_track::connected_points(*track, end))
{
if (other == nullptr || done.count(other) > 0 || false == rail(*other) || other->eType == tt_Table || false == alike(*other))
continue;
if (other->eType == tt_Switch && (rules.stop == 0 || (rules.stop == 1 && point.path != 0)))
continue;
auto const known{reached.find(other)};
if (known != reached.end() && known->second <= further)
continue;
reached[other] = further;
queue.push({further, other});
}
}
return result;
}
std::vector<TTrack *> const &editor_mode::track_spread_preview(TTrack &Start) const
{
auto &spread{m_spread};
auto const signature{spread.stop | (spread.same_rails ? 4 : 0) | (spread.same_bed ? 8 : 0) | (spread.same_speed ? 16 : 0) | (static_cast<int>(spread.reach) << 5)};
if (spread.from != &Start || spread.signature != signature)
{
spread.from = &Start;
spread.signature = signature;
spread.preview = track_spread_from(Start);
spread.length = 0.0;
for (auto const *track : spread.preview)
spread.length += track->Length();
}
return spread.preview;
}
void editor_mode::track_spread_apply(TTrack &Start)
{
track_set_prune();
if (m_track_set.empty())
if (auto *selected{selected_track()}; selected != nullptr && selected != &Start)
m_track_set.push_back(selected);
auto const taken{track_spread_from(Start)};
if (in_track_set(&Start))
{
std::unordered_set<TTrack const *> const out(taken.begin(), taken.end());
m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [&](TTrack const *Track) { return out.count(Track) > 0; }), m_track_set.end());
}
else
for (auto *track : taken)
if (false == in_track_set(track))
m_track_set.push_back(track);
m_spread.from = nullptr;
m_node = m_track_set.empty() ? nullptr : m_track_set.back();
m_track_point = {};
ui()->set_node(m_node);
}
void editor_mode::track_set_grow()
{
track_set_prune();
auto const reach{m_spread.reach};
std::vector<TTrack *> added;
for (auto *track : m_track_set)
{
m_spread.reach = std::max(0.01f, static_cast<float>(track->Length()) - 0.01f);
for (auto *other : track_spread_from(*track))
if (false == in_track_set(other) && std::find(added.begin(), added.end(), other) == added.end())
added.push_back(other);
}
m_spread.reach = reach;
m_spread.from = nullptr;
m_track_set.insert(m_track_set.end(), added.begin(), added.end());
}
void editor_mode::render_track_spread_ui()
{
auto &spread{m_spread};
if (false == ImGui::CollapsingHeader(STR_C("Spread along the joins: Ctrl+Shift+LMB on a path")))
return;
char const *const stops[] = {STR_C("Stop before the switches"), STR_C("Through the switches, along their main track"), STR_C("Through everything joined")};
for (int i = 0; i < IM_ARRAYSIZE(stops); ++i)
ImGui::RadioButton(stops[i], &spread.stop, i);
ImGui::TextUnformatted(STR_C("Only to the paths like the clicked one:"));
ImGui::SameLine();
ImGui::Checkbox(STR_C("rails"), &spread.same_rails);
ImGui::SameLine();
ImGui::Checkbox(STR_C("trackbed"), &spread.same_bed);
ImGui::SameLine();
ImGui::Checkbox(STR_C("speed"), &spread.same_speed);
ImGui::SetNextItemWidth(110.f);
ImGui::InputFloat(STR_C("Reach along the track, m (0: no limit)"), &spread.reach, 50.f, 500.f, "%.0f");
spread.reach = std::clamp(spread.reach, 0.f, 100000.f);
ImGui::TextDisabled("%s", STR_C("Holding Ctrl+Shift shows what the click takes; on a path of the set it takes them out"));
}
void editor_mode::draw_track_spread() const
{
if (m_intent.what != track_intent::kind::spread || m_spread.from == nullptr)
return;
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
auto const removing{in_track_set(m_spread.from)};
auto const colour{removing ? IM_COL32(255, 90, 70, 220) : IM_COL32(90, 220, 255, 220)};
for (auto const *track : m_spread.preview)
{
if (track->m_editorremoved)
continue;
for (auto const &path : track->m_paths)
{
bezier const curve{path};
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 4.0), 4, 48)};
auto previous{curve.point(0.0)};
for (int k = 1; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
projection.line(drawlist, previous, next, colour, 2.5f);
previous = next;
}
}
}
}
void editor_mode::draw_track_set() const
{
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
if (m_track_box.active)
{
auto const &io{ImGui::GetIO()};
ImVec2 const from{m_track_box.from.x, m_track_box.from.y};
drawlist->AddRectFilled(from, io.MousePos, IM_COL32(255, 160, 40, 40));
drawlist->AddRect(from, io.MousePos, IM_COL32(255, 160, 40, 220), 0.0f, 0, 1.5f);
}
if (m_track_set.size() < 2)
return;
for (auto const *track : m_track_set)
{
if (track->m_editorremoved)
continue;
for (auto const &path : track->m_paths)
{
bezier const curve{path};
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 3.0), 6, 64)};
auto previous{curve.point(0.0)};
for (int k = 1; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
projection.line(drawlist, previous, next, IM_COL32(255, 160, 40, 230), 3.0f);
previous = next;
}
}
}
}
void editor_mode::track_set_apply(std::function<bool(TTrack &)> const &Change, bool const Rebuild, char const *What)
{
track_set_prune();
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> changed;
std::size_t locked{0};
for (auto *track : m_track_set)
{
if (false == scene::Layers.editable(track))
{
++locked;
continue;
}
auto const before{editor_track::capture(*track)};
if (false == Change(*track))
continue;
states.emplace_back(track, before);
changed.push_back(track);
}
if (changed.empty())
return;
push_track_snapshot(std::move(states));
for (auto *track : changed)
{
if (Rebuild)
editor_track::commit_parameters(*track);
else
track->mark_dirty();
}
auto status{format(STR_C("%s set on %zu paths, Ctrl+Z takes it back"), STR_C(What), changed.size())};
if (locked > 0)
status += format(STR_C(", %zu in the files which can't be changed are left as they are"), locked);
ui()->set_status(status, false);
}
void editor_mode::track_set_delete()
{
track_set_prune();
std::vector<TTrack *> removed;
std::size_t kept{0};
for (auto *track : m_track_set)
{
if (false == track->Dynamics.empty() || false == scene::Layers.editable(track))
{
++kept;
continue;
}
editor_track::retire(*track);
removed.push_back(track);
if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end())
m_route = {};
}
push_track_snapshot({}, {}, removed);
m_track_set.clear();
m_node = nullptr;
ui()->set_node(nullptr);
m_track_point = {};
straight_refresh();
auto status{format(STR_C("%zu paths deleted, Ctrl+Z brings them back"), removed.size())};
if (kept > 0)
status += format(STR_C(", %zu with vehicles on them or in the files which can't be changed are left"), kept);
ui()->set_status(status, false);
}
void editor_mode::render_track_set_ui()
{
track_set_prune();
if (m_track_set.size() < 2)
return;
auto &fields{m_track_set_fields};
double length{0.0};
for (auto const *track : m_track_set)
length += track->Length();
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_C("%zu paths selected, %.1f m"), m_track_set.size(), length);
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Clear")))
{
m_track_set.clear();
return;
}
ImGui::TextDisabled("%s", STR_C("The gizmo moves them together; Shift+LMB adds or takes out a path"));
render_parallel_set_ui();
if (ImGui::SmallButton(STR_C("Grow by the neighbours")))
track_set_grow();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Adds the paths joined to the set, by the rules of the spread"));
auto const button_width{90.0f};
ImGui::PushItemWidth(-button_width - ImGui::GetStyle().ItemSpacing.x);
ImGui::InputDouble("##setvelocity", &fields.velocity, 0.0, 0.0, "%.0f");
ImGui::SameLine();
if (ImGui::Button(format("%s##setv", STR_C("Velocity")).c_str(), ImVec2(button_width, 0.0f)))
{
auto const velocity{fields.velocity};
track_set_apply([velocity](TTrack &Track) { editor_track::velocity(Track, velocity); return true; }, false, "Velocity");
}
ImGui::InputTextWithHint("##setrail", STR_C("texture of the rails"), fields.rail, sizeof(fields.rail));
ImGui::SameLine();
if (ImGui::Button(format("%s##setr", STR_C("Rails")).c_str(), ImVec2(button_width, 0.0f)) && fields.rail[0] != '\0')
{
auto const material{editor_track::fetch_material(fields.rail)};
track_set_apply([material](TTrack &Track) {
Track.m_material1 = material;
if (Track.eType == tt_Switch)
Track.m_material2 = material;
return true;
}, true, "Rails");
}
ImGui::InputTextWithHint("##settrackbed", STR_C("texture of the trackbed"), fields.trackbed, sizeof(fields.trackbed));
ImGui::SameLine();
if (ImGui::Button(format("%s##setb", STR_C("Trackbed")).c_str(), ImVec2(button_width, 0.0f)) && fields.trackbed[0] != '\0')
{
auto const material{editor_track::fetch_material(fields.trackbed)};
track_set_apply([material](TTrack &Track) {
if (Track.eType == tt_Switch)
{
if (Track.SwitchExtension == nullptr)
return false;
Track.SwitchExtension->m_material3 = material;
}
else
Track.m_material2 = material;
return true;
}, true, "Trackbed");
}
ImGui::InputTextWithHint("##setcircuits", STR_C("track circuits, separated by spaces"), fields.circuits, sizeof(fields.circuits));
ImGui::SameLine();
if (ImGui::Button(format("%s##setc", STR_C("Circuits")).c_str(), ImVec2(button_width, 0.0f)))
{
std::vector<std::string> names;
std::string const text{fields.circuits};
std::size_t at{0};
while (at < text.size())
{
auto const end{text.find(' ', at)};
auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)};
if (false == word.empty())
names.push_back(word);
if (end == std::string::npos)
break;
at = end + 1;
}
track_set_apply([names](TTrack &Track) { editor_track::isolated(Track, names); return true; }, false, "Circuits");
}
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
ImGui::Combo("##setenvironment", &fields.environment, environments, IM_ARRAYSIZE(environments));
ImGui::SameLine();
if (ImGui::Button(format("%s##sete", STR_C("Environment")).c_str(), ImVec2(button_width, 0.0f)))
{
auto const environment{static_cast<TEnvironmentType>(fields.environment)};
track_set_apply([environment](TTrack &Track) { Track.eEnvironment = environment; return true; }, false, "Environment");
}
ImGui::PopItemWidth();
if (ImGui::Button(format(STR_C("Delete %zu paths"), m_track_set.size()).c_str()))
track_set_delete();
ImGui::Separator();
}
void editor_mode::render_track_set_gizmo()
{
track_set_prune();
if (m_track_set.size() < 2)
{
m_track_set_using = false;
return;
}
gizmo_frame const frame;
if (false == m_track_set_using)
{
glm::dvec3 centre{0.0};
for (auto const *track : m_track_set)
centre += editor_track::pivot(*track);
m_track_set_pivot = centre / static_cast<double>(m_track_set.size());
m_track_set_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(m_track_set_pivot - frame.camera));
}
glm::mat4 delta(1.0f);
frame.manipulate(ImGuizmo::TRANSLATE, m_track_set_gizmo, m_gizmo_snap, &delta);
if (ImGuizmo::IsUsing())
{
if (false == m_track_set_using)
{
std::string reason;
if (false == tracks_editable(m_track_set, reason))
{
ui()->set_status(reason, true);
return;
}
m_track_set_using = true;
m_track_drag = m_track_set;
m_track_set_joints.clear();
for (auto *track : m_track_set)
{
auto joints{editor_track::joints(*track)};
joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return in_track_set(Joint.other); }), joints.end());
for (auto const &joint : joints)
if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end())
m_track_drag.push_back(joint.other);
m_track_set_joints.emplace_back(track, std::move(joints));
}
std::vector<std::pair<TTrack *, editor_track::state>> states;
for (auto *track : m_track_drag)
states.emplace_back(track, editor_track::capture(*track));
push_track_snapshot(std::move(states));
}
glm::dvec3 const offset{delta[3]};
if (glm::length(offset) > 1e-9)
{
for (auto &entry : m_track_set_joints)
{
editor_track::translate(*entry.first, offset);
editor_track::follow(*entry.first, entry.second);
}
m_track_dirty = true;
}
commit_track_drag(false);
}
else if (m_track_set_using)
{
commit_track_drag(true);
m_track_set_using = false;
m_track_drag.clear();
m_track_set_joints.clear();
straight_refresh();
}
}
void editor_mode::render_lineside_ui()
{
struct tab
{
char const *label;
bool *open;
bool *expand;
std::function<void()> render;
};
std::array<tab, 4> const tabs{{
{"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }},
{"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }},
{"Parallel track", &m_parallel.open, &m_parallel.expand, [this]() { render_parallel_ui(); }},
{"Vehicle", &m_vehicle.open, &m_vehicle.expand, [this]() { render_vehicle_ui(); }},
}};
if (selected_track() == nullptr)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: the line the objects go along"));
if (false == ImGui::BeginTabBar("##lineside"))
return;
for (int i = 0; i < static_cast<int>(tabs.size()); ++i)
{
auto const &entry{tabs[i]};
auto const wanted{*entry.expand};
*entry.expand = false;
*entry.open = ImGui::BeginTabItem(STR_C(entry.label), nullptr, wanted ? ImGuiTabItemFlags_SetSelected : 0);
if (false == *entry.open)
continue;
m_lineside_tab = i;
entry.render();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}