mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 15:04:54 +02:00
The include bank read every .inc of the scenery directory on the first use, on the main thread: opening the switch panel or selecting a switch stopped the editor until all of them were read, which on a large scenery directory under Windows takes long. The bank now starts looking as the editor starts, in the background, and the main thread only reads the descriptions once the files are read. - the switch drives, turntables and signals are read again from the bank when it changes; the panels show "Looking for the templates..." meanwhile, only the edits which need the templates wait for them - the sweep tool takes the templates from the bank instead of looking through the scenery directory again - a folder linked into one of its own subfolders no longer leads the walk in circles - the log tells how many templates were found and how long it took
2204 lines
96 KiB
C++
2204 lines
96 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:
|
|
*/
|
|
|
|
|
|
#include "stdafx.h"
|
|
#include "application/editormode.h"
|
|
#include "application/editoruilayer.h"
|
|
#include "application/editorprojection.h"
|
|
#include "application/editormodeshared.h"
|
|
#include "application/editorgestures.h"
|
|
#include "editor/editorFormat.hpp"
|
|
#include "editor/editorAlignmentStore.hpp"
|
|
#include "editor/editorSwitchStore.hpp"
|
|
#include "editor/editorGeometry.hpp"
|
|
#include "editor/editorIncludeInfo.hpp"
|
|
|
|
#include "utilities/Globals.h"
|
|
#include "rendering/renderer.h"
|
|
#include "world/Track.h"
|
|
#include "world/TrackFeatures.h"
|
|
#include "scene/scene.h"
|
|
#include "scene/scenelayers.h"
|
|
#include "simulation/simulation.h"
|
|
#include "utilities/Logs.h"
|
|
#include "utilities/translation.h"
|
|
|
|
#include "imgui/imgui.h"
|
|
#include "imgui/ImGuizmo.h"
|
|
#include <glm/gtc/type_ptr.hpp>
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <iterator>
|
|
#include <set>
|
|
|
|
#include "application/editormodetrackshared.h"
|
|
|
|
using namespace track_mode;
|
|
|
|
|
|
TTrack *editor_mode::selected_track() const
|
|
{
|
|
return dynamic_cast<TTrack *>(m_node);
|
|
}
|
|
|
|
void editor_mode::select_track(scene::basic_node *Node)
|
|
{
|
|
auto *track = dynamic_cast<TTrack *>(Node);
|
|
if (track == nullptr)
|
|
{
|
|
m_track_point = {};
|
|
return;
|
|
}
|
|
if (track != m_node)
|
|
{
|
|
m_track_point = {};
|
|
m_track_mode_notice.clear();
|
|
}
|
|
// a track out of the set ends it, the set is the selection only together with its tracks
|
|
if (false == in_track_set(track))
|
|
m_track_set.clear();
|
|
m_node = track;
|
|
ui()->set_node(m_node);
|
|
if (ui()->mode() != nodebank_panel::TRACK)
|
|
return;
|
|
m_track_window_open = true;
|
|
m_track_mode_notice.clear();
|
|
if (track_plan_tab() && plan_tab_for(track) != m_track_tab)
|
|
{
|
|
show_track_tab(plan_tab_for(track));
|
|
return;
|
|
}
|
|
// the mode stays as it is, the path is taken up by it if it can be
|
|
switch (m_track_tab)
|
|
{
|
|
case track_tab::straights:
|
|
if (track->eType == tt_Switch)
|
|
m_track_mode_notice = STR_C("Switches aren't a part of a line: T edits the switch, Esc goes back to Select");
|
|
break;
|
|
case track_tab::route:
|
|
if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end())
|
|
break;
|
|
if (track->eType != tt_Normal || false == editor_track::is_supported(*track))
|
|
m_track_mode_notice = STR_C("Switches aren't a part of a line: T edits the switch, Esc goes back to Select");
|
|
else if (false == route_load(*track))
|
|
m_track_mode_notice = m_route.error.empty() ? std::string{STR_C("The curve can't be read from this path, edit it as a single path")} : m_route.error;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
editor_track::point_ref editor_mode::track_handle_hit() const
|
|
{
|
|
auto *track = selected_track();
|
|
// the tracks of a set are moved by its gizmo, not by the points of one of them
|
|
if (track == nullptr || false == editor_track::is_supported(*track) || m_track_tab != track_tab::path || (m_track_set.size() >= 2 && in_track_set(track)))
|
|
return {};
|
|
|
|
screen_projection const projection;
|
|
float best = kHandleRadius * kHandleRadius;
|
|
editor_track::point_ref hit;
|
|
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
|
{
|
|
for (auto const kind : kPointKinds)
|
|
{
|
|
editor_track::point_ref const point{i, kind};
|
|
float const distance = projection.mouse_distance2(editor_track::point_position(*track, point)) * (editor_track::is_end(kind) ? 0.5f : 1.0f);
|
|
if (distance < best)
|
|
{
|
|
best = distance;
|
|
hit = point;
|
|
}
|
|
}
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
void editor_mode::draw_track_overlay() const
|
|
{
|
|
auto const *track = selected_track();
|
|
if (track == nullptr)
|
|
return;
|
|
|
|
screen_projection const projection;
|
|
ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport());
|
|
bool const trackmode = ui()->mode() == nodebank_panel::TRACK;
|
|
// a track of the set is drawn with the set, see draw_track_set()
|
|
if (trackmode && m_track_set.size() >= 2 && in_track_set(track))
|
|
return;
|
|
|
|
ImU32 const coursecolor = IM_COL32(40, 220, 255, 220);
|
|
int constexpr samples{32};
|
|
for (auto const &path : track->m_paths)
|
|
{
|
|
auto previous = bezier{path}.point(0.0);
|
|
for (int i = 1; i <= samples; ++i)
|
|
{
|
|
auto const next = bezier{path}.point(static_cast<double>(i) / samples);
|
|
projection.line(drawlist, previous, next, coursecolor, 2.5f);
|
|
previous = next;
|
|
}
|
|
}
|
|
if (false == trackmode || m_route_tab || false == editor_track::is_supported(*track))
|
|
return;
|
|
|
|
ImU32 const vectorcolor = IM_COL32(200, 200, 200, 160);
|
|
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
|
{
|
|
projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::start}), editor_track::point_position(*track, {i, editor_track::point_kind::control1}), vectorcolor, 1.0f);
|
|
projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::end}), editor_track::point_position(*track, {i, editor_track::point_kind::control2}), vectorcolor, 1.0f);
|
|
}
|
|
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
|
{
|
|
for (auto const kind : kPointKinds)
|
|
{
|
|
editor_track::point_ref const point{i, kind};
|
|
ImVec2 screen;
|
|
if (false == projection.project(editor_track::point_position(*track, point), screen))
|
|
continue;
|
|
bool const selected = (point == m_track_point);
|
|
if (editor_track::is_end(kind))
|
|
{
|
|
ImU32 const color = editor_track::is_connected(*track, point) ? IM_COL32(60, 220, 60, 255) : IM_COL32(240, 60, 60, 255);
|
|
drawlist->AddCircleFilled(screen, 6.0f, color);
|
|
}
|
|
else
|
|
{
|
|
drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 4.0f), ImVec2(screen.x + 4.0f, screen.y + 4.0f), IM_COL32(230, 230, 230, 255));
|
|
}
|
|
if (selected)
|
|
drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 220, 40, 255), 16, 2.5f);
|
|
}
|
|
}
|
|
if (m_track_snap.track != nullptr)
|
|
{
|
|
ImVec2 screen;
|
|
if (projection.project(m_track_snap.position, screen))
|
|
drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f);
|
|
projection.line(drawlist, m_track_snap.position, m_track_snap.position + m_track_snap.direction * 5.0, IM_COL32(255, 60, 255, 200), 2.0f);
|
|
}
|
|
}
|
|
|
|
void editor_mode::commit_track_drag(bool const Force)
|
|
{
|
|
if (false == m_track_dirty || m_track_drag.empty())
|
|
return;
|
|
auto const now = std::chrono::steady_clock::now();
|
|
if (false == Force && now - m_track_last_commit < std::chrono::milliseconds(100))
|
|
return;
|
|
editor_track::commit(m_track_drag);
|
|
m_track_dirty = false;
|
|
m_track_last_commit = now;
|
|
}
|
|
|
|
void editor_mode::trim_history()
|
|
{
|
|
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));
|
|
}
|
|
|
|
void editor_mode::push_track_snapshot(std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created, std::vector<TTrack *> Removed)
|
|
{
|
|
if (States.empty() && Created.empty() && Removed.empty())
|
|
return;
|
|
|
|
trim_history();
|
|
auto *track = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front();
|
|
EditorSnapshot snap;
|
|
snap.action = EditorSnapshot::Action::TrackEdit;
|
|
snap.node_name = track->name();
|
|
snap.node_ptr = track;
|
|
snap.position = track->location();
|
|
snap.uuid = track->uuid;
|
|
snap.tracks = std::move(States);
|
|
snap.created = std::move(Created);
|
|
snap.removed = std::move(Removed);
|
|
infra_attach(snap);
|
|
snap.alignments = alignment_store::take_changes();
|
|
snap.switch_records = switch_store::take_changes();
|
|
snap.directives.insert(snap.directives.end(), m_drive_buffer.begin(), m_drive_buffer.end());
|
|
m_drive_buffer.clear();
|
|
m_history.push_back(std::move(snap));
|
|
g_redo.clear();
|
|
++m_track_edits;
|
|
}
|
|
|
|
bool editor_mode::tracks_editable(std::vector<TTrack *> const &Tracks, std::string &Reason)
|
|
{
|
|
for (auto const *track : Tracks)
|
|
{
|
|
std::string reason;
|
|
if (track != nullptr && false == editor_track::can_edit_geometry(*track, reason))
|
|
{
|
|
Reason = (track->name().empty() ? std::string{STR_C("(no name)")} : track->name()) + ": " + reason;
|
|
return false;
|
|
}
|
|
}
|
|
Reason.clear();
|
|
return true;
|
|
}
|
|
|
|
void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo)
|
|
{
|
|
if (Snapshot.tracks.empty() && Snapshot.created.empty() && Snapshot.removed.empty())
|
|
return;
|
|
|
|
++m_track_edits;
|
|
std::vector<TTrack *> tracks;
|
|
for (auto const &entry : Snapshot.tracks)
|
|
tracks.emplace_back(entry.first);
|
|
m_infra_suspended = true;
|
|
EditorSnapshot current{Snapshot};
|
|
current.tracks.clear();
|
|
auto involved{tracks};
|
|
if (Undo)
|
|
involved.insert(involved.end(), Snapshot.created.begin(), Snapshot.created.end());
|
|
for (auto *track : involved)
|
|
current.tracks.emplace_back(track, editor_track::capture(*track));
|
|
current.infra.clear();
|
|
for (auto const &entry : Snapshot.infra)
|
|
if (auto const *binding{infra_find(entry.saved)})
|
|
current.infra.push_back(infra_state_of(*binding));
|
|
if (Snapshot.profiles)
|
|
{
|
|
std::vector<scene::layer_handle> layers;
|
|
for (auto const *library : {&std::as_const(m_profiles), &Snapshot.profile_library})
|
|
for (auto const &entry : *library)
|
|
layers.push_back(entry.layer);
|
|
current.profile_library = std::move(m_profiles);
|
|
m_profiles = Snapshot.profile_library;
|
|
profile_library_mark(std::move(layers));
|
|
}
|
|
alignment_store::settle();
|
|
switch_store::settle();
|
|
if (Snapshot.alignments.has_value())
|
|
{
|
|
current.alignments = alignment_store::records();
|
|
alignment_store::restore(*Snapshot.alignments);
|
|
}
|
|
if (Snapshot.switch_records.has_value())
|
|
{
|
|
current.switch_records = switch_store::records();
|
|
switch_store::restore(*Snapshot.switch_records);
|
|
}
|
|
Opposite.push_back(std::move(current));
|
|
|
|
if (Undo)
|
|
{
|
|
for (auto *track : Snapshot.created)
|
|
editor_track::retire(*track);
|
|
for (auto *track : Snapshot.removed)
|
|
{
|
|
editor_track::revive(*track);
|
|
tracks.push_back(track);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (auto *track : Snapshot.removed)
|
|
editor_track::retire(*track);
|
|
for (auto *track : Snapshot.created)
|
|
editor_track::revive(*track);
|
|
}
|
|
for (auto const &entry : Snapshot.tracks)
|
|
editor_track::apply(*entry.first, entry.second);
|
|
editor_track::commit(tracks);
|
|
for (auto const &entry : Snapshot.infra)
|
|
infra_restore_state(entry);
|
|
m_infra_buffer.clear();
|
|
m_drive_buffer.clear();
|
|
m_infra_suspended = false;
|
|
if (false == m_route.chain.tracks.empty())
|
|
{
|
|
std::string error;
|
|
if (m_route.from->m_editorremoved || m_route.to->m_editorremoved || false == editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true))
|
|
{
|
|
m_route.chain = {};
|
|
m_route.result = {};
|
|
m_route.vertex = -1;
|
|
m_route.grip = -1;
|
|
}
|
|
else
|
|
{
|
|
route_bind_ends();
|
|
route_update();
|
|
}
|
|
}
|
|
|
|
lay_after_history(Undo);
|
|
m_track_snap = {};
|
|
m_track_dirty = false;
|
|
m_track_point = {};
|
|
m_node = tracks.empty() || tracks.front()->m_editorremoved ? nullptr : tracks.front();
|
|
ui()->set_node(m_node);
|
|
straight_refresh();
|
|
profile_after_undo();
|
|
if (Snapshot.profiles && false == m_profile.route.spans.empty())
|
|
profile_restore();
|
|
}
|
|
|
|
std::array<editor_mode::track_mode, 11> const &editor_mode::track_modes()
|
|
{
|
|
static std::array<track_mode, 11> const modes{{
|
|
{"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters", editor_icons::icon::select, 0},
|
|
{"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track", editor_icons::icon::lay_track, 0},
|
|
{"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too", editor_icons::icon::curve, 0},
|
|
{"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch", editor_icons::icon::turnout, 0},
|
|
{"Parallel", "N", track_tab::parallel, "A second track beside a track: LMB on it, then the side and the spacing", editor_icons::icon::parallel, 0},
|
|
{"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it", editor_icons::icon::turntable, 1},
|
|
{"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by", editor_icons::icon::signal, 1},
|
|
{"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, trainsets", editor_icons::icon::objects, 1},
|
|
{"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line", editor_icons::icon::profile, 2},
|
|
{"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows", editor_icons::icon::speed, 2},
|
|
{"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints", editor_icons::icon::joints, 2},
|
|
// the infrastructure tool (track_tab::infra) is left out: no button nor key
|
|
}};
|
|
return modes;
|
|
}
|
|
|
|
void editor_mode::show_track_tab(track_tab const Wanted)
|
|
{
|
|
auto const Tab{Wanted == track_tab::straights || Wanted == track_tab::route ? plan_tab_for(selected_track()) : Wanted};
|
|
m_track_window_open = true;
|
|
m_roadtool.window = false;
|
|
if (Tab == m_track_tab)
|
|
return;
|
|
m_track_mode_notice.clear();
|
|
// what was going on in the mode left stays behind
|
|
cancel_track_tools();
|
|
m_straights.tool = 0;
|
|
m_lay.points.clear();
|
|
m_lay.start = {};
|
|
m_lay.chain.clear();
|
|
m_lay.preview.clear();
|
|
m_lay.status.clear();
|
|
m_lay.active = Tab == track_tab::lay;
|
|
m_switch.armed = -1;
|
|
m_track_tab = Tab;
|
|
auto *track{selected_track()};
|
|
switch (Tab)
|
|
{
|
|
case track_tab::turnout:
|
|
arm_switch();
|
|
break;
|
|
case track_tab::straights:
|
|
if (track != nullptr && track->eType == tt_Switch)
|
|
m_track_mode_notice = STR_C("Switches aren't a part of a line: LMB on a plain path");
|
|
break;
|
|
case track_tab::route:
|
|
if (track != nullptr && std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) == m_route.chain.tracks.end())
|
|
{
|
|
if (track->eType != tt_Normal || false == editor_track::is_supported(*track))
|
|
m_track_mode_notice = STR_C("Switches aren't a part of a line: LMB on a plain path");
|
|
else if (false == route_load(*track))
|
|
m_track_mode_notice = m_route.error.empty() ? std::string{STR_C("The curve can't be read from this path, edit it as a single path")} : m_route.error;
|
|
}
|
|
break;
|
|
case track_tab::profile:
|
|
if (track != nullptr && m_profile.route.spans.empty())
|
|
profile_open_run(*track);
|
|
break;
|
|
case track_tab::turntable:
|
|
scan_turntables(false);
|
|
turntable_read(track);
|
|
break;
|
|
case track_tab::signals:
|
|
signal_refresh(true);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void editor_mode::arm_switch()
|
|
{
|
|
auto &tool{m_switch};
|
|
if (false == tool.collected)
|
|
{
|
|
tool.templates = editor_track::standard_switch_templates();
|
|
tool.collected = true;
|
|
}
|
|
tool.status.clear();
|
|
tool.armed = tool.templates.empty() ? -1 : std::clamp(tool.last, 0, static_cast<int>(tool.templates.size()) - 1);
|
|
if (tool.armed >= 0 && tool.templates[tool.armed].double_slip)
|
|
{
|
|
auto const ordinary{std::find_if(tool.templates.begin(), tool.templates.end(), [](editor_track::switch_template const &Template) { return false == Template.double_slip; })};
|
|
tool.armed = ordinary != tool.templates.end() ? static_cast<int>(ordinary - tool.templates.begin()) : -1;
|
|
}
|
|
}
|
|
|
|
bool editor_mode::track_shortcut(int const Key)
|
|
{
|
|
auto const key{Key == GLFW_KEY_C ? GLFW_KEY_G : Key};
|
|
for (auto const &mode : track_modes())
|
|
{
|
|
if (mode.key[1] != '\0' || key != GLFW_KEY_A + (mode.key[0] - 'A'))
|
|
continue;
|
|
// the key of the mode on leaves it
|
|
auto const on{m_track_tab == mode.tab || (mode.tab == track_tab::straights && track_plan_tab())};
|
|
show_track_tab(m_track_window_open && on ? track_tab::path : mode.tab);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
editor_mode::track_tab editor_mode::plan_tab_for(TTrack const *Track) const
|
|
{
|
|
if (Track == nullptr)
|
|
return track_plan_tab() ? m_track_tab : track_tab::straights;
|
|
if (Track->eType == tt_Normal && editor_track::is_supported(*Track) && false == editor_track::is_straight(*Track, m_straights.tolerance))
|
|
return track_tab::route;
|
|
return track_tab::straights;
|
|
}
|
|
|
|
bool editor_mode::track_busy() const
|
|
{
|
|
auto const &straights{m_straights};
|
|
return (m_lay.active && false == m_lay.points.empty()) || m_turntable.placing || m_signal.placing || m_signal.moving || straights.dragging || m_extend.active || m_point_drag.active || m_handle_drag.active || m_switch.placing || m_track_point.valid() || m_route.vertex >= 0 || m_route.grip >= 0 || false == straights.detour.empty() || straights.tool != 0 || straights.handle >= 0;
|
|
}
|
|
|
|
std::string editor_mode::track_mode_name() const
|
|
{
|
|
if (false == m_track_window_open)
|
|
return STR_C("Select");
|
|
switch (m_track_tab)
|
|
{
|
|
case track_tab::lay: return STR_C("Lay track");
|
|
case track_tab::turnout: return STR_C("Switch");
|
|
case track_tab::straights:
|
|
if (false == m_straights.detour.empty() || m_straights.tool == 2)
|
|
return STR_C("Plan, straight: S-curve");
|
|
if (m_straights.tool == 1)
|
|
return STR_C("Plan, straight: break");
|
|
return STR_C("Plan, straight");
|
|
case track_tab::route: return STR_C("Plan, curve");
|
|
case track_tab::profile: return STR_C("Vertical profile");
|
|
case track_tab::speed: return STR_C("Speed check");
|
|
case track_tab::joints: return STR_C("Joints");
|
|
case track_tab::lineside: return STR_C("Objects along the track");
|
|
case track_tab::infra: return STR_C("Infrastructure");
|
|
case track_tab::parallel: return STR_C("Parallel track");
|
|
case track_tab::signals: return STR_C("Signal");
|
|
case track_tab::turntable: return STR_C("Turntable");
|
|
default: return STR_C("Select");
|
|
}
|
|
}
|
|
|
|
void editor_mode::render_track_inspector()
|
|
{
|
|
auto const &io{ImGui::GetIO()};
|
|
m_profile.open = m_track_window_open && m_track_tab == track_tab::profile;
|
|
m_speed.open = m_track_window_open && m_track_tab == track_tab::speed;
|
|
m_infra.open = m_track_window_open && m_track_tab == track_tab::infra;
|
|
m_joints.open = m_track_window_open && m_track_tab == track_tab::joints;
|
|
if (false == m_track_window_open || m_track_tab != track_tab::lineside)
|
|
{
|
|
m_sweep.open = false;
|
|
m_hekto.open = false;
|
|
m_fouling.open = false;
|
|
m_parallel.open = false;
|
|
m_vehicle.open = false;
|
|
}
|
|
ui()->set_track(m_track_window_open);
|
|
if (false == m_track_window_open)
|
|
{
|
|
// a closed window takes its mode with it
|
|
if (m_track_tab != track_tab::path)
|
|
show_track_tab(track_tab::path);
|
|
m_track_window_open = false;
|
|
return;
|
|
}
|
|
|
|
m_route_tab = m_track_tab != track_tab::path;
|
|
|
|
// the tools of the mode are drawn in the tool options window and the selected path in the inspector, see
|
|
// render_track_tool_options() and render_track_selection(); the checks list what they find in a window of their own
|
|
if (m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_track_tab == track_tab::infra)
|
|
{
|
|
auto const *title{m_track_tab == track_tab::speed ? STR_C("Speed check") : m_track_tab == track_tab::joints ? STR_C("Joints") : STR_C("Infrastructure")};
|
|
ImGui::SetNextWindowSize(ImVec2(600.0f, std::min(320.0f, io.DisplaySize.y - 80.0f)), ImGuiCond_FirstUseEver);
|
|
if (ImGui::Begin((std::string{title} + "###trackanalysis").c_str(), nullptr, ImGuiWindowFlags_NoCollapse))
|
|
{
|
|
switch (m_track_tab)
|
|
{
|
|
case track_tab::speed: render_speed_body(); break;
|
|
case track_tab::joints: render_joints_body(); break;
|
|
default: render_infra_body(); break;
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
m_profile.strip_top = -1.0f;
|
|
if (m_track_tab == track_tab::profile && m_track_window_open)
|
|
render_profile_strip();
|
|
}
|
|
|
|
void editor_mode::render_track_tool_options()
|
|
{
|
|
auto *track{selected_track()};
|
|
// the tools themselves are in the toolbar
|
|
render_track_search();
|
|
ImGui::Spacing();
|
|
render_track_guide();
|
|
// the texts of the tools go on to the next line at the edge of the panel, none of them makes it scroll sideways
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
switch (m_track_tab)
|
|
{
|
|
case track_tab::lay:
|
|
render_lay_ui();
|
|
break;
|
|
case track_tab::parallel:
|
|
render_parallel_ui();
|
|
break;
|
|
case track_tab::turnout:
|
|
if (track != nullptr && track->eType == tt_Switch)
|
|
render_turnout_ui();
|
|
else
|
|
render_switch_ui();
|
|
break;
|
|
case track_tab::straights:
|
|
render_straight_ui();
|
|
if (ImGui::CollapsingHeader(STR_C("Straights in the scenery")))
|
|
render_straights_ui();
|
|
break;
|
|
case track_tab::route:
|
|
render_route_ui();
|
|
break;
|
|
case track_tab::profile: render_profile_body(); break;
|
|
case track_tab::speed:
|
|
case track_tab::joints:
|
|
case track_tab::infra:
|
|
ImGui::TextDisabled("%s", STR_C("What the check finds is listed in its window"));
|
|
break;
|
|
case track_tab::lineside: render_lineside_ui(); break;
|
|
case track_tab::signals: render_signal_ui(); break;
|
|
case track_tab::turntable: render_turntable_ui(); break;
|
|
default:
|
|
render_track_set_ui();
|
|
render_track_spread_ui();
|
|
break;
|
|
}
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
|
|
void editor_mode::render_track_selection()
|
|
{
|
|
track_field_settle();
|
|
auto const tracks{selected_tracks()};
|
|
if (tracks.empty())
|
|
{
|
|
ImGui::TextDisabled("%s", STR_C("Nothing selected"));
|
|
if (m_track_tab != track_tab::path)
|
|
return;
|
|
ImGui::Spacing();
|
|
gesture_rows({{"LMB on a track", "its properties show here"}, {"Ctrl+F", "finds a track by its name"}});
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("%s", STR_C("Or start with:"));
|
|
auto const width{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)};
|
|
if (ImGui::Button(STR_C("Lay a track (L)"), ImVec2(width, ImGui::GetFrameHeight() * 1.4f)))
|
|
show_track_tab(track_tab::lay);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Tutorial"), ImVec2(width, ImGui::GetFrameHeight() * 1.4f)))
|
|
m_first_steps_open = true;
|
|
return;
|
|
}
|
|
// the properties belong to the select tool and to the switch tool, the other tools show the track they work with
|
|
auto const sheet{m_track_tab == track_tab::path || m_track_tab == track_tab::turnout};
|
|
if (tracks.size() >= 2)
|
|
{
|
|
render_track_set_header(tracks);
|
|
if (false == sheet)
|
|
return;
|
|
render_track_select_tools(tracks);
|
|
render_track_style(tracks);
|
|
render_track_properties(tracks);
|
|
ImGui::Spacing();
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.18f, 0.15f, 1.0f));
|
|
if (ImGui::Button(format(STR_P("Delete %d track", "Delete %d tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size())).c_str()))
|
|
track_set_delete();
|
|
ImGui::PopStyleColor();
|
|
item_tooltip("Deletes the selected tracks (Del); Ctrl+Z brings them back");
|
|
return;
|
|
}
|
|
auto *track{tracks.front()};
|
|
if (false == sheet)
|
|
{
|
|
ImGui::TextUnformatted(track->name().empty() ? STR_C("(no name)") : track->name().c_str());
|
|
ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length());
|
|
return;
|
|
}
|
|
render_path_ui();
|
|
}
|
|
|
|
void editor_mode::render_track_guide()
|
|
{
|
|
// a card of the tool in use: what it is for, and how the mouse and the keys drive it, drawn
|
|
auto const &modes{track_modes()};
|
|
auto const tab{false == m_track_window_open ? track_tab::path : m_track_tab == track_tab::route ? track_tab::straights : m_track_tab};
|
|
auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == tab; })};
|
|
auto const family{found != modes.end() ? found->family : 0};
|
|
auto const accent{family_colour(family)};
|
|
auto const font{ImGui::GetFontSize()};
|
|
auto const pad{std::round(font * 0.6f)};
|
|
auto *list{ImGui::GetWindowDrawList()};
|
|
auto const top{ImGui::GetCursorScreenPos()};
|
|
auto const width{ImGui::GetContentRegionAvail().x};
|
|
auto const right{top.x + width};
|
|
list->ChannelsSplit(2);
|
|
list->ChannelsSetCurrent(1);
|
|
ImGui::BeginGroup();
|
|
ImGui::Dummy(ImVec2(width, pad * 0.6f));
|
|
ImGui::Indent(pad + 3.0f);
|
|
auto const row{ImGui::GetCursorScreenPos()};
|
|
auto const iconside{std::round(font * 1.7f)};
|
|
if (found != modes.end())
|
|
editor_icons::draw(list, found->picture, ImVec2(row.x + iconside * 0.5f, row.y + iconside * 0.5f), accent);
|
|
ImGui::Dummy(ImVec2(iconside, iconside));
|
|
ImGui::SameLine(0.0f, pad);
|
|
ImGui::BeginGroup();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
|
ImGui::SetWindowFontScale(1.15f);
|
|
ImGui::TextUnformatted(track_mode_name().c_str());
|
|
ImGui::SetWindowFontScale(1.0f);
|
|
ImGui::PopStyleColor();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(accent));
|
|
ImGui::TextUnformatted(family_name(family));
|
|
ImGui::PopStyleColor();
|
|
ImGui::EndGroup();
|
|
if (found != modes.end() && found->key != nullptr)
|
|
{
|
|
auto const height{std::round(font * 1.35f)};
|
|
auto const cap{std::max(std::round(ImGui::CalcTextSize(found->key).x + height * 0.6f), height)};
|
|
gesture_key(*list, ImVec2(right - pad - cap, row.y + std::round((iconside - height) * 0.5f)), height, found->key);
|
|
}
|
|
ImGui::PushTextWrapPos(right - pad - ImGui::GetWindowPos().x);
|
|
if (found != modes.end())
|
|
{
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
|
ImGui::TextWrapped("%s", STR_C(found->tooltip));
|
|
ImGui::PopStyleColor();
|
|
}
|
|
// the gestures, each drawn: the mouse with the button lit, the keys as caps
|
|
auto const hints{track_key_hints(false)};
|
|
auto const gesture{std::round(font * 1.25f)};
|
|
std::vector<std::vector<gesture_word>> drawn;
|
|
float column{0.0f};
|
|
for (auto const &hint : hints)
|
|
{
|
|
drawn.push_back(gesture_picture(hint.raw != nullptr ? hint.raw : hint.key));
|
|
column = std::max(column, gesture_width(gesture, drawn.back()));
|
|
}
|
|
column = std::min(column, font * 6.0f);
|
|
if (false == hints.empty())
|
|
ImGui::Dummy(ImVec2(0.0f, pad * 0.3f));
|
|
for (std::size_t i = 0; i < hints.size() && i < 7; ++i)
|
|
{
|
|
auto const at{ImGui::GetCursorScreenPos()};
|
|
auto const line{ImGui::GetTextLineHeight()};
|
|
auto const height{std::max(gesture, line)};
|
|
list->PushClipRect(at, ImVec2(at.x + column, at.y + height), true);
|
|
gesture_draw(*list, ImVec2(at.x, at.y + std::round((height - gesture) * 0.5f)), gesture, drawn[i], ImGui::GetColorU32(ImGuiCol_Text));
|
|
list->PopClipRect();
|
|
ImGui::Dummy(ImVec2(column, height));
|
|
ImGui::SameLine(0.0f, pad);
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + std::round((height - line) * 0.5f));
|
|
if (hints[i].raw == nullptr || false == gesture_keys_only(hints[i].raw))
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.83f, 0.3f, 1.0f), "%s", hints[i].key);
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::TextWrapped("%s", hints[i].action.c_str());
|
|
}
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
|
|
ImGui::TextUnformatted(STR_C("F1: all the shortcuts"));
|
|
ImGui::PopStyleColor();
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
if (ImGui::IsItemClicked())
|
|
m_shortcuts_open = true;
|
|
ImGui::Unindent(pad + 3.0f);
|
|
ImGui::Dummy(ImVec2(width, pad * 0.5f));
|
|
ImGui::EndGroup();
|
|
auto const bottom{ImGui::GetItemRectMax().y};
|
|
list->ChannelsSetCurrent(0);
|
|
auto const rounding{std::round(font * 0.35f)};
|
|
list->AddRectFilled(top, ImVec2(right, bottom), (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 28), rounding);
|
|
list->AddRect(top, ImVec2(right, bottom), (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 70), rounding);
|
|
list->AddRectFilled(top, ImVec2(top.x + 4.0f, bottom), accent, rounding, ImDrawFlags_RoundCornersLeft);
|
|
list->ChannelsMerge();
|
|
if (false == m_track_mode_notice.empty())
|
|
{
|
|
ImGui::Spacing();
|
|
ImGui::PushTextWrapPos(ImGui::GetWindowContentRegionMax().x - 8.0f);
|
|
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", m_track_mode_notice.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
ImGui::Spacing();
|
|
}
|
|
|
|
void editor_mode::render_track_name(TTrack &Track)
|
|
{
|
|
auto &edit{m_track_name};
|
|
auto const current{Track.name() == "none" ? std::string{} : Track.name()};
|
|
auto const sync = [&]() {
|
|
edit.track = &Track;
|
|
edit.synced = current;
|
|
std::snprintf(edit.text, sizeof(edit.text), "%s", current.c_str());
|
|
edit.uses = editor_track::uses_of_name(Track);
|
|
};
|
|
if (edit.track != &Track)
|
|
{
|
|
sync();
|
|
edit.status.clear();
|
|
}
|
|
else if (edit.synced != current && edit.synced == edit.text)
|
|
sync();
|
|
|
|
std::string why;
|
|
if (false == scene::Layers.editable(&Track, &why))
|
|
{
|
|
ImGui::Text(STR_C("Name: %s"), current.empty() ? STR_C("(no name)") : current.c_str());
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled(STR_C("(can't be changed: %s)"), why.c_str());
|
|
return;
|
|
}
|
|
auto const apply = [&]() {
|
|
std::string name{edit.text};
|
|
std::string reason;
|
|
if (false == editor_track::name_valid(Track, name, reason))
|
|
return;
|
|
auto const before{editor_track::capture(Track)};
|
|
editor_track::rename(Track, name);
|
|
push_track_snapshot({{&Track, before}});
|
|
edit.status = format(STR_C("Renamed from %s"), current.empty() ? STR_C("(no name)") : current.c_str());
|
|
sync();
|
|
};
|
|
if (begin_fields("##tracknamefields", 0.3f))
|
|
{
|
|
field_row("Name##trackname");
|
|
if (ImGui::InputTextWithHint("##trackname", STR_C("(no name)"), edit.text, sizeof(edit.text), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CharsNoBlank))
|
|
apply();
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("%s", STR_C("Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name"));
|
|
ImGui::EndTable();
|
|
}
|
|
std::string typed{edit.text};
|
|
if (typed == edit.synced)
|
|
{
|
|
if (false == edit.status.empty())
|
|
{
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.f), "%s", edit.status.c_str());
|
|
}
|
|
return;
|
|
}
|
|
std::string reason;
|
|
TTrack *owner{nullptr};
|
|
auto const valid{editor_track::name_valid(Track, typed, reason, &owner)};
|
|
ImGui::SameLine();
|
|
if (valid)
|
|
{
|
|
auto const warned{false == edit.uses.events.empty() || false == edit.uses.loose.empty()};
|
|
if (ImGui::SmallButton(warned ? STR_C("Rename anyway") : STR_C("Rename")))
|
|
apply();
|
|
std::vector<std::string> notes;
|
|
if (typed != edit.text)
|
|
notes.push_back(format(STR_C("Saved as %s: the scenery keeps the names in lower case"), typed.c_str()));
|
|
if (edit.uses.cells > 0 || edit.uses.includes > 0)
|
|
notes.push_back(format(STR_C("The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them"), edit.uses.cells, edit.uses.includes));
|
|
if (warned)
|
|
notes.push_back(STR_C("The events listed below work until the scenery is loaded again; then they need the new name in their files"));
|
|
if (false == notes.empty() && ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("%s", lines_of(notes).c_str());
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton(STR_C("Keep the old")))
|
|
{
|
|
std::snprintf(edit.text, sizeof(edit.text), "%s", edit.synced.c_str());
|
|
return;
|
|
}
|
|
if (false == valid)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", STR_C(reason.c_str()));
|
|
if (owner != nullptr)
|
|
{
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton(STR_C("Look at it")))
|
|
focus_track(*owner, 0, owner->location());
|
|
}
|
|
return;
|
|
}
|
|
auto const listed = [](std::vector<std::string> const &Names) {
|
|
std::string result;
|
|
for (std::size_t i = 0; i < Names.size() && i < 4; ++i)
|
|
result += (i > 0 ? ", " : "") + Names[i];
|
|
if (Names.size() > 4)
|
|
result += format(" (+%d)", static_cast<int>(Names.size() - 4));
|
|
return result;
|
|
};
|
|
if (false == edit.uses.events.empty())
|
|
ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events aim at the path by the old name and the editor doesn't write them: %s"), listed(edit.uses.events).c_str());
|
|
if (false == edit.uses.loose.empty())
|
|
ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events belong to the path by the old name: %s"), listed(edit.uses.loose).c_str());
|
|
}
|
|
|
|
void editor_mode::render_turnout_ui()
|
|
{
|
|
auto *track{selected_track()};
|
|
if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2)
|
|
return;
|
|
auto const set{double_of(*track)};
|
|
if (set.has_value())
|
|
track = set->first;
|
|
auto const &main{track->m_paths[0]};
|
|
auto const &diverging{track->m_paths[1]};
|
|
auto const origin{main.points[segment_data::point::start]};
|
|
auto const chord{plan_of(main.points[segment_data::point::end] - origin)};
|
|
auto const &control2{diverging.points[segment_data::point::control2]};
|
|
auto const control{glm::length(plan_of(control2)) > 1e-6 ? control2 : diverging.points[segment_data::point::start] - diverging.points[segment_data::point::end]};
|
|
if (glm::length(chord) < 1e-3 || glm::length(plan_of(control)) < 1e-6)
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), STR_C("The geometry of this switch is degenerate, it can't be edited here"));
|
|
return;
|
|
}
|
|
glm::dvec2 const axis{glm::normalize(chord)};
|
|
glm::dvec2 const normal{-axis.y, axis.x};
|
|
auto const length{glm::dot(chord, axis)};
|
|
glm::dvec2 const end{diverging.points[segment_data::point::end].x - origin.x, diverging.points[segment_data::point::end].z - origin.z};
|
|
auto const offset{glm::dot(end, normal)};
|
|
glm::dvec2 const tangent{glm::normalize(glm::dvec2{-control.x, -control.z})};
|
|
auto const angle{std::atan2(std::abs(tangent.x * axis.y - tangent.y * axis.x), glm::dot(tangent, axis))};
|
|
std::string about;
|
|
if (set.has_value())
|
|
{
|
|
auto const &second{set->second->m_paths.front()};
|
|
about = format(STR_C("Double switch %s + %s: length %.3f m, the second switch starts %.3f m on"), set->first->name().c_str(), set->second->name().c_str(), glm::dot(plan_of(second.points[segment_data::point::end] - origin), axis), glm::dot(plan_of(second.points[segment_data::point::start] - origin), axis)) + "\n" +
|
|
format(STR_C("First switch: diverging %s, end offset %.3f m"), offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset));
|
|
}
|
|
else
|
|
ImGui::Text(STR_C("Length %.3f m, diverging %s, end offset %.3f m"), length, offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset));
|
|
{
|
|
// the switch in plan at one scale: the main track white, the diverging one yellow, the angle at its end, the length under
|
|
auto const font{ImGui::GetFontSize()};
|
|
auto const width{ImGui::GetContentRegionAvail().x};
|
|
auto const height{std::round(font * 4.2f)};
|
|
auto const top{ImGui::GetCursorScreenPos()};
|
|
ImGui::Dummy(ImVec2(width, height));
|
|
if (false == about.empty() && ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("%s", about.c_str());
|
|
auto *list{ImGui::GetWindowDrawList()};
|
|
list->AddRectFilled(top, ImVec2(top.x + width, top.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), std::round(font * 0.35f));
|
|
auto const local = [&](glm::dvec3 const &Point) {
|
|
glm::dvec2 const relative{Point.x - origin.x, Point.z - origin.z};
|
|
return glm::dvec2{glm::dot(relative, axis), glm::dot(relative, normal)};
|
|
};
|
|
std::vector<std::vector<glm::dvec2>> lines;
|
|
std::vector<TTrack *> switches{track};
|
|
if (set.has_value())
|
|
switches.push_back(set->second);
|
|
double far_along{0.0}, low_across{0.0}, high_across{0.0};
|
|
for (auto *each : switches)
|
|
for (auto const &path : each->m_paths)
|
|
{
|
|
geometry::bezier const curve{path};
|
|
std::vector<glm::dvec2> points;
|
|
for (int i = 0; i <= 24; ++i)
|
|
{
|
|
auto const point{local(curve.point(i / 24.0))};
|
|
far_along = std::max(far_along, point.x);
|
|
low_across = std::min(low_across, point.y);
|
|
high_across = std::max(high_across, point.y);
|
|
points.push_back(point);
|
|
}
|
|
lines.push_back(std::move(points));
|
|
}
|
|
auto const pad{std::round(font * 0.7f)};
|
|
auto const label{ImGui::GetTextLineHeight()};
|
|
auto const room{height - 2.0f * pad - label};
|
|
auto const scale{static_cast<float>(std::min((width - 2.0 * pad) / std::max(1.0, far_along), room / std::max(0.5, high_across - low_across)))};
|
|
auto const base{top.y + pad + (room - static_cast<float>(high_across - low_across) * scale) * 0.5f - static_cast<float>(low_across) * scale};
|
|
auto const screen = [&](glm::dvec2 const &Point) { return ImVec2(top.x + pad + static_cast<float>(Point.x) * scale, base + static_cast<float>(Point.y) * scale); };
|
|
// the diverging tracks first, the main ones over them
|
|
for (auto const main : {false, true})
|
|
for (auto const &line : lines)
|
|
{
|
|
if ((std::abs(line.back().y - line.front().y) < 0.05) != main)
|
|
continue;
|
|
std::vector<ImVec2> drawn;
|
|
for (auto const &point : line)
|
|
drawn.push_back(screen(point));
|
|
list->AddPolyline(drawn.data(), static_cast<int>(drawn.size()), main ? IM_COL32(235, 235, 235, 255) : IM_COL32(255, 210, 60, 255), 0, 2.0f);
|
|
}
|
|
// the point of the blades, and the angle at the end of the diverging track
|
|
list->AddCircleFilled(screen(glm::dvec2{0.0, 0.0}), 3.0f, IM_COL32(230, 80, 230, 255));
|
|
if (angle > 1e-6)
|
|
{
|
|
auto const text{format("1:%.1f", 1.0 / std::tan(angle))};
|
|
auto const size{ImGui::CalcTextSize(text.c_str())};
|
|
auto const at{screen(glm::dvec2{far_along, offset})};
|
|
list->AddText(ImVec2(std::round(at.x - size.x - 2.0f), std::round(offset > 0.0 ? at.y - size.y - 1.0f : at.y + 1.0f)), IM_COL32(255, 210, 60, 255), text.c_str());
|
|
}
|
|
if (diverging.radius > 0.0f)
|
|
{
|
|
auto const text{format("R %.0f", static_cast<double>(diverging.radius))};
|
|
auto const size{ImGui::CalcTextSize(text.c_str())};
|
|
auto const at{screen(glm::dvec2{far_along * 0.45, offset * 0.25})};
|
|
list->AddText(ImVec2(std::round(at.x - size.x * 0.5f), std::round(offset > 0.0 ? at.y + 3.0f : at.y - size.y - 3.0f)), IM_COL32(255, 210, 60, 200), text.c_str());
|
|
}
|
|
auto const text{format("%.2f m", far_along)};
|
|
auto const size{ImGui::CalcTextSize(text.c_str())};
|
|
auto const y{top.y + height - pad * 0.4f - label};
|
|
list->AddLine(ImVec2(top.x + pad, y + label * 0.5f), ImVec2(top.x + width - pad, y + label * 0.5f), ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5f));
|
|
list->AddRectFilled(ImVec2(top.x + (width - size.x) * 0.5f - 4.0f, y), ImVec2(top.x + (width + size.x) * 0.5f + 4.0f, y + label), ImGui::GetColorU32(ImGuiCol_FrameBg));
|
|
list->AddText(ImVec2(std::round(top.x + (width - size.x) * 0.5f), y), ImGui::GetColorU32(ImGuiCol_TextDisabled), text.c_str());
|
|
}
|
|
if (auto const known{switch_store::of(*track)}; known.has_value())
|
|
ImGui::TextUnformatted(switch_store::label(*known).c_str());
|
|
// the name, the number, the rail and the kilometre of the switch, with what the scenery keeps of it
|
|
if (ImGui::CollapsingHeader(STR_C("Details##switchdetails")))
|
|
{
|
|
render_track_name(*track);
|
|
render_switch_record(*track);
|
|
if (angle > 1e-6)
|
|
ImGui::Text(STR_C("Angle 1:%.2f (%.4f deg), R in the entry %.0f m"), 1.0 / std::tan(angle), glm::degrees(angle), diverging.radius);
|
|
}
|
|
auto const drive_ui = [&](TTrack &Switch) {
|
|
ImGui::PushID(&Switch);
|
|
if (set.has_value())
|
|
ImGui::TextUnformatted(Switch.name().c_str());
|
|
if (false == EditorIncludes.scanned())
|
|
ImGui::TextDisabled("%s", STR_C("Looking for the templates..."));
|
|
else if (auto const drive{find_switch_drive(Switch, false)})
|
|
{
|
|
auto const family{drive->family};
|
|
auto const variant{drive->index};
|
|
if (begin_fields("##standingdrive", 0.3f))
|
|
{
|
|
field_row("Drive##standing");
|
|
if (ImGui::BeginCombo("##standing", m_switch.drives[family].name.c_str()))
|
|
{
|
|
for (int i = 0; i < static_cast<int>(m_switch.drives.size()); ++i)
|
|
if (ImGui::Selectable(m_switch.drives[i].name.c_str(), family == i) && i != family)
|
|
replace_switch_drive(Switch, i, false);
|
|
ImGui::EndCombo();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("%s\n%s", STR_C("Puts another drive in place of the drive of this switch, on the same side"), format(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str()).c_str());
|
|
ImGui::EndTable();
|
|
}
|
|
int side{((variant >> 1) == (variant & 1)) ? 0 : 1};
|
|
if (segmented("standingside", side, {{0, STR_C("diverging side"), "Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"},
|
|
{1, STR_C("other side"), "Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"}}))
|
|
flip_switch_drive(Switch);
|
|
}
|
|
else if ((simulation::Events.FindEvent(Switch.name() + "+") != nullptr && simulation::Events.FindEvent(Switch.name() + "-") != nullptr) || (m_track_name.track == &Switch && m_track_name.uses.includes > 0))
|
|
ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str());
|
|
else
|
|
{
|
|
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", STR_C("No drive: the switch can't be switched during the game"));
|
|
render_switch_drive_choice();
|
|
if (m_switch.drive >= 0 && ImGui::Button(STR_C("Put the drive by the switch")))
|
|
place_switch_drive(Switch);
|
|
}
|
|
ImGui::PopID();
|
|
};
|
|
drive_ui(*track);
|
|
if (set.has_value())
|
|
drive_ui(*set->second);
|
|
if (track_features::crossings)
|
|
{
|
|
bool frogs{track->SwitchExtension->m_frogs};
|
|
bool movable{track->SwitchExtension->m_movablenose};
|
|
char const *const frogtip{"Cuts the rails of the switch at its frog and lays wing rails and check rails.\n"
|
|
"Off for a switch whose include has its own model of them"};
|
|
char const *const nosetip{"The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails"};
|
|
if (frogs)
|
|
toggle_chips("switchfrogs", {{STR_C("Frog and check rails made from the rails"), &frogs, frogtip}, {STR_C("Movable nose"), &movable, nosetip}});
|
|
else
|
|
toggle_chips("switchfrogs", {{STR_C("Frog and check rails made from the rails"), &frogs, frogtip}});
|
|
if (frogs != track->SwitchExtension->m_frogs || movable != track->SwitchExtension->m_movablenose)
|
|
{
|
|
auto const before{editor_track::capture(*track)};
|
|
track->SwitchExtension->m_frogs = frogs;
|
|
track->SwitchExtension->m_movablenose = movable;
|
|
push_track_snapshot({{track, before}});
|
|
editor_track::commit_parameters(*track);
|
|
}
|
|
}
|
|
if (false == m_switch.status.empty())
|
|
ImGui::TextWrapped("%s", m_switch.status.c_str());
|
|
|
|
auto &tool{m_switch};
|
|
if (false == tool.collected)
|
|
{
|
|
tool.templates = editor_track::standard_switch_templates();
|
|
tool.collected = true;
|
|
}
|
|
if (tool.templates.empty())
|
|
return;
|
|
m_turnout_template = std::clamp(m_turnout_template, 0, static_cast<int>(tool.templates.size()) - 1);
|
|
auto const fits = [&](editor_track::switch_template const &Template) { return false == Template.double_slip && Template.twofold() == set.has_value(); };
|
|
if (false == fits(tool.templates[m_turnout_template]))
|
|
{
|
|
auto const ordinary{std::find_if(tool.templates.begin(), tool.templates.end(), fits)};
|
|
if (ordinary == tool.templates.end())
|
|
return;
|
|
m_turnout_template = static_cast<int>(ordinary - tool.templates.begin());
|
|
}
|
|
ImGui::Separator();
|
|
auto const finish_set = [&](std::string const &Status) {
|
|
switch_store::follow();
|
|
records_to_history();
|
|
straight_refresh();
|
|
if (false == Status.empty())
|
|
ui()->set_status(Status, true);
|
|
};
|
|
auto const replace = [&](editor_track::switch_template const &Template, int const Side) {
|
|
auto const grade{length > 0.0 ? (main.points[segment_data::point::end].y - origin.y) / length : 0.0};
|
|
auto paths{editor_track::place_switch(Template, origin, axis, Side, grade)};
|
|
if (set.has_value())
|
|
{
|
|
if (paths.size() < 5)
|
|
return;
|
|
auto const before{double_paths(*set)};
|
|
for (std::size_t i = 0; i < paths.size(); ++i)
|
|
if (paths[i].rolls[0] == 0.f && paths[i].rolls[1] == 0.f)
|
|
paths[i].rolls = before[i < 4 ? i & ~std::size_t{1} : 4].rolls;
|
|
std::string status;
|
|
if (false == move_double_paths(*set, paths, status))
|
|
{
|
|
ui()->set_status(status, true);
|
|
return;
|
|
}
|
|
record_double(*set->first, *set->second, Template);
|
|
finish_set(status);
|
|
return;
|
|
}
|
|
if (paths.size() < 2)
|
|
return;
|
|
for (int i = 0; i < 2; ++i)
|
|
if (paths[i].rolls[0] == 0.f && paths[i].rolls[1] == 0.f)
|
|
paths[i].rolls = track->m_paths[i].rolls;
|
|
std::string status;
|
|
if (false == move_switch_paths(*track, paths, status))
|
|
{
|
|
ui()->set_status(status, true);
|
|
return;
|
|
}
|
|
record_switch(*track, Template);
|
|
records_to_history();
|
|
straight_refresh();
|
|
if (false == status.empty())
|
|
ui()->set_status(status, true);
|
|
};
|
|
int const side{offset > 0.0 ? 1 : -1};
|
|
if (set.has_value() && ImGui::Button(STR_C("Flip the side")))
|
|
{
|
|
auto const mirror = [&](glm::dvec3 const &Vector) {
|
|
auto const across{glm::dot(plan_of(Vector), normal)};
|
|
return glm::dvec3{Vector.x - 2.0 * across * normal.x, Vector.y, Vector.z - 2.0 * across * normal.y};
|
|
};
|
|
auto paths{double_paths(*set)};
|
|
for (auto &path : paths)
|
|
path.transform([&](glm::dvec3 const &Point) { return origin + mirror(Point - origin); }, mirror);
|
|
std::string status;
|
|
if (false == move_double_paths(*set, paths, status))
|
|
ui()->set_status(status, true);
|
|
else
|
|
{
|
|
if (auto entry{switch_store::named(*set->first)})
|
|
{
|
|
switch_store::place(*entry, *set->first);
|
|
switch_store::classify(*entry, *set->first);
|
|
switch_store::put(*entry, alignment_store::layer_of(*set->first));
|
|
}
|
|
finish_set(status);
|
|
}
|
|
}
|
|
else if (false == set.has_value() && ImGui::Button(STR_C("Flip the side")))
|
|
{
|
|
editor_track::switch_template own;
|
|
own.source = track;
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
auto &local{own.local[i]};
|
|
local = track->m_paths[i];
|
|
auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), normal) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; };
|
|
local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal);
|
|
if (side < 0)
|
|
{
|
|
local.rolls = {-local.rolls[0], -local.rolls[1]};
|
|
for (auto &piece : local.pieces)
|
|
piece.rolls = {-piece.rolls[0], -piece.rolls[1]};
|
|
}
|
|
}
|
|
replace(own, -side);
|
|
}
|
|
item_tooltip("Mirrors the switch: the diverging track goes to the other side");
|
|
double low{0.0}, high{0.0}, reach{length};
|
|
auto const slides{switch_slides(*track) && (set.has_value() ? switch_slides(*set->second) && double_room(*set, low, high, reach, m_switch.status) : switch_room(*track, low, high, m_switch.status))};
|
|
ImGui::PushID("turnout");
|
|
if (ImGui::TreeNode(STR_C("More settings")))
|
|
{
|
|
ImGui::PushItemWidth(-1.0f);
|
|
if (ImGui::BeginCombo("##turnouttemplate", switch_template_label(tool.templates[m_turnout_template]).c_str()))
|
|
{
|
|
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
|
|
{
|
|
if (false == fits(tool.templates[i]))
|
|
continue;
|
|
auto const label{switch_template_label(tool.templates[i])};
|
|
if (ImGui::Selectable(label.c_str(), i == m_turnout_template))
|
|
m_turnout_template = i;
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
if (ImGui::Button(STR_C("Replace the geometry with the template")))
|
|
replace(tool.templates[m_turnout_template], side);
|
|
item_tooltip("Keeps the start and the direction of the switch, takes the shape of the chosen template");
|
|
if (slides)
|
|
{
|
|
ImGui::PushItemWidth(100.0f);
|
|
ImGui::InputDouble("##switchslide", &tool.slide_typed, 0.0, 0.0, "%.2f m");
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(STR_C("Slide along the straight")))
|
|
slide_switch(*track, std::clamp(tool.slide_typed, low, high - reach));
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("%s\n%s", STR_C("Moves the switch along its straight by the distance typed, + towards the frog; the straight paths on both sides get shorter or longer. Dragging the switch with the mouse does the same"),
|
|
format(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - reach).c_str());
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
ImGui::PopID();
|
|
ImGui::Separator();
|
|
ImGui::TextDisabled("%s", STR_C("The textures, the speed and the other properties of the switch are in the inspector"));
|
|
}
|
|
|
|
void editor_mode::render_path_events(TTrack &Track)
|
|
{
|
|
auto *track{&Track};
|
|
if (m_track_events_for != track && false == ImGui::IsAnyItemActive())
|
|
{
|
|
m_track_events_for = track;
|
|
m_track_event[0] = '\0';
|
|
m_track_events_missing.clear();
|
|
}
|
|
auto const count{[&]() {
|
|
std::size_t count{0};
|
|
for (int list = 0; list < 6; ++list)
|
|
count += editor_track::events(*track, list).size();
|
|
return count;
|
|
}()};
|
|
if (false == ImGui::CollapsingHeader(format("%s (%zu)###trackevents", STR_C("Events"), count).c_str()))
|
|
return;
|
|
auto const before{editor_track::capture(*track)};
|
|
auto const changed = [&](editor_track::state const &Before) {
|
|
push_track_snapshot({{track, Before}});
|
|
track->mark_dirty();
|
|
};
|
|
ImGui::TextDisabled("%s", STR_C("What the track sets off as the vehicles run on it"));
|
|
item_tooltip("event0: a vehicle with a crew stands on the path, event1: runs along it towards its start, event2: towards its end.\n"
|
|
"eventall0/1/2: the same for any vehicle");
|
|
auto const editable_events{track->Dynamics.empty()};
|
|
for (int list = 0; list < 6; ++list)
|
|
{
|
|
auto &events{editor_track::events(*track, list)};
|
|
for (std::size_t i = 0; i < events.size(); ++i)
|
|
{
|
|
auto const &event{events[i]};
|
|
// bound by the name of the path, not written in the file
|
|
if (event.first.empty())
|
|
{
|
|
ImGui::TextDisabled(STR_C("%s %s (by the name of the path)"), editor_track::event_keyword(list), event.second != nullptr ? STR_C("auto") : "?");
|
|
continue;
|
|
}
|
|
ImGui::PushID(list * 1000 + static_cast<int>(i));
|
|
if (ImGui::SmallButton("x") && editable_events)
|
|
{
|
|
events.erase(events.begin() + i);
|
|
editor_track::bind_events(*track, m_track_events_missing);
|
|
changed(before);
|
|
ImGui::PopID();
|
|
break;
|
|
}
|
|
ImGui::SameLine();
|
|
if (event.second == nullptr)
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("%s %s (no such event)"), editor_track::event_keyword(list), event.first.c_str());
|
|
else
|
|
ImGui::Text("%s %s", editor_track::event_keyword(list), event.first.c_str());
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
ImGui::PushItemWidth(100.0f);
|
|
ImGui::Combo("##eventlist", &m_track_event_list, "event0\0event1\0event2\0eventall0\0eventall1\0eventall2\0");
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
ImGui::PushItemWidth(-60.0f);
|
|
auto const entered{ImGui::InputText("##eventname", m_track_event.data(), m_track_event.size(), ImGuiInputTextFlags_EnterReturnsTrue)};
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
if ((ImGui::Button(STR_C("Add")) || entered) && m_track_event[0] != '\0')
|
|
{
|
|
if (false == editable_events)
|
|
{
|
|
ui()->set_status(STR_C("The events can't change while vehicles stand on the path"), true);
|
|
}
|
|
else
|
|
{
|
|
editor_track::events(*track, m_track_event_list).emplace_back(m_track_event.data(), nullptr);
|
|
editor_track::bind_events(*track, m_track_events_missing);
|
|
changed(before);
|
|
m_track_event[0] = '\0';
|
|
}
|
|
}
|
|
item_tooltip("Name of the event to add to the chosen list. An event which doesn't exist is kept, and reported");
|
|
if (false == m_track_events_missing.empty())
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No such events: %s"), m_track_events_missing.c_str());
|
|
}
|
|
|
|
void editor_mode::render_path_ui()
|
|
{
|
|
auto *track = selected_track();
|
|
if (track == nullptr)
|
|
{
|
|
ImGui::TextDisabled(STR_C("No path selected"));
|
|
return;
|
|
}
|
|
|
|
if (false == editor_track::is_supported(*track))
|
|
{
|
|
ImGui::TextDisabled(STR_C("Editing of this path type isn't supported"));
|
|
return;
|
|
}
|
|
render_track_name(*track);
|
|
ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length());
|
|
if (track->eType == tt_Normal && false == track->m_paths.empty())
|
|
{
|
|
// the track at a glance: tiles of its length, radius, grade, cant and speed, going on to the next row at the edge
|
|
auto const &path{track->m_paths.front()};
|
|
geometry::bezier const curve{path};
|
|
auto const length{curve.plan_length()};
|
|
auto const first{curve.first(0.5)};
|
|
auto const second{curve.second(0.5)};
|
|
auto const speed{std::hypot(first.x, first.z)};
|
|
auto const bend{speed > 1e-9 ? std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed) : 0.0};
|
|
auto const straight{editor_track::is_straight(*track, m_straights.tolerance) || bend < 1e-5};
|
|
auto const rise{path.points[segment_data::point::end].y - path.points[segment_data::point::start].y};
|
|
auto const roll{std::max(std::abs(path.rolls[0]), std::abs(path.rolls[1]))};
|
|
metric_tiles({{format("%.2f m", track->Length()), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)},
|
|
{straight ? std::string{STR_C("straight")} : format("R %.0f m", 1.0 / bend), STR_C("plan"), straight ? IM_COL32(255, 255, 255, 230) : overlay_color::grip},
|
|
{format("%+.1f mm/m", length > 1e-6 ? rise / length * 1000.0 : 0.0), STR_C("grade"), ImGui::GetColorU32(ImGuiCol_Text)},
|
|
{format("%.0f mm", 1500.0 * std::sin(glm::radians(static_cast<double>(roll)))), STR_C("cant"), IM_COL32(240, 170, 60, 255)},
|
|
{editor_track::velocity(*track) > 0.0 ? format("%.0f km/h", editor_track::velocity(*track)) : std::string{"-"}, STR_C("speed"), ImGui::GetColorU32(ImGuiCol_Text)}});
|
|
}
|
|
std::string reason;
|
|
bool const geometry = editor_track::can_edit_geometry(*track, reason);
|
|
if (false == geometry)
|
|
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), STR_C("Geometry locked: %s"), reason.c_str());
|
|
if (track->eType == tt_Switch && m_track_tab == track_tab::path)
|
|
{
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton(STR_C("Edit the switch (T)")))
|
|
show_track_tab(track_tab::turnout);
|
|
item_tooltip("A switch: the turnout tool sets its geometry, side and drive");
|
|
}
|
|
std::vector<TTrack *> const tracks{track};
|
|
render_track_select_tools(tracks);
|
|
render_track_style(tracks);
|
|
render_track_properties(tracks);
|
|
render_path_events(*track);
|
|
// the shape is given by dragging in the view, the numbers are for the exact values
|
|
if (m_track_tab != track_tab::path)
|
|
return;
|
|
|
|
auto const before = editor_track::capture(*track);
|
|
enum class rebuild { none, parameters, geometry };
|
|
auto const finish_edit = [&](rebuild const Rebuild) {
|
|
if (ImGui::IsItemActivated())
|
|
m_track_field_before = before;
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
{
|
|
push_track_snapshot({{track, m_track_field_before}});
|
|
switch (Rebuild)
|
|
{
|
|
case rebuild::geometry: editor_track::commit({track}); break;
|
|
case rebuild::parameters: editor_track::commit_parameters(*track); break;
|
|
default: track->mark_dirty(); break;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (ImGui::CollapsingHeader(track->eType == tt_Switch ? STR_C("Geometry of the paths") : STR_C("Geometry")))
|
|
{
|
|
for (int i = 0; i < static_cast<int>(track->m_paths.size()); ++i)
|
|
{
|
|
ImGui::PushID(i);
|
|
if (track->m_paths.size() > 1)
|
|
ImGui::TextDisabled(STR_C("Path %d"), i + 1);
|
|
for (auto const kind : kPointKinds)
|
|
{
|
|
editor_track::point_ref const point{i, kind};
|
|
if (kind != editor_track::point_kind::start)
|
|
ImGui::SameLine();
|
|
std::string label = point_label(kind);
|
|
if (editor_track::is_end(kind))
|
|
label += editor_track::is_connected(*track, point) ? " (+)" : " (-)";
|
|
if (ImGui::Selectable(label.c_str(), m_track_point == point, 0, ImVec2(90.0f, 0.0f)))
|
|
m_track_point = (m_track_point == point ? editor_track::point_ref{} : point);
|
|
}
|
|
item_tooltip("(+) joined with another track, (-) a free end");
|
|
|
|
auto &path = track->m_paths[i];
|
|
if (false == geometry)
|
|
{
|
|
auto const &start = path.points[segment_data::point::start];
|
|
auto const &end = path.points[segment_data::point::end];
|
|
ImGui::TextDisabled(STR_C("Start (%.2f, %.2f, %.2f) End (%.2f, %.2f, %.2f)"), start.x, start.y, start.z, end.x, end.y, end.z);
|
|
ImGui::PopID();
|
|
continue;
|
|
}
|
|
ImGui::PushItemWidth(-110.0f);
|
|
if (m_track_point.valid() && m_track_point.path == i)
|
|
{
|
|
auto const kind{m_track_point.kind};
|
|
if (editor_track::is_end(kind))
|
|
{
|
|
auto position = path.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end];
|
|
if (ImGui::DragScalarN("x, y, z##point", ImGuiDataType_Double, glm::value_ptr(position), 3, 0.01f, nullptr, nullptr, "%.3f"))
|
|
editor_track::move_point(*track, m_track_point, position);
|
|
item_tooltip("Drag to move, Ctrl+click to type the value");
|
|
finish_edit(rebuild::geometry);
|
|
}
|
|
else
|
|
{
|
|
auto &vector{path.points[kind == editor_track::point_kind::control1 ? segment_data::point::control1 : segment_data::point::control2]};
|
|
ImGui::DragScalarN(STR_C("vector##point"), ImGuiDataType_Double, glm::value_ptr(vector), 3, 0.01f, nullptr, nullptr, "%.3f");
|
|
item_tooltip("Control vector, relative to its end; zero for a straight");
|
|
finish_edit(rebuild::geometry);
|
|
}
|
|
}
|
|
if (ImGui::TreeNode(STR_C("Cant and radius")))
|
|
{
|
|
if (begin_fields("##cantradius", 0.3f))
|
|
{
|
|
field_row("Roll (deg)");
|
|
ImGui::DragFloat2("##roll", path.rolls.data(), 0.01f, -15.0f, 15.0f, "%.2f");
|
|
item_tooltip("Cant at the start and at the end as the roll angle of the track");
|
|
finish_edit(rebuild::geometry);
|
|
field_row("Radius");
|
|
ImGui::InputFloat("##radius", &path.radius, 0.0f, 0.0f, "%.1f");
|
|
item_tooltip("Radius stored with the path in the scenery; the shape itself comes from the control vectors");
|
|
finish_edit(rebuild::geometry);
|
|
ImGui::EndTable();
|
|
}
|
|
if (ImGui::Button(STR_C("Straighten"), ImVec2(-FLT_MIN, 0.0f)))
|
|
{
|
|
push_track_snapshot({{track, before}});
|
|
path.points[segment_data::point::control1] = glm::dvec3{};
|
|
path.points[segment_data::point::control2] = glm::dvec3{};
|
|
path.radius = 0.0f;
|
|
editor_track::commit({track});
|
|
}
|
|
item_tooltip("Removes the control vectors and the radius, the path becomes a straight between its ends");
|
|
ImGui::TreePop();
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::PopID();
|
|
}
|
|
if (geometry && ImGui::TreeNode(STR_C("Snapping of the ends")))
|
|
{
|
|
if (begin_fields("##snapfields", 0.45f))
|
|
{
|
|
field_row("Snap radius (m)");
|
|
ImGui::DragFloat("##snapradius", &m_track_snap_radius, 0.05f, 0.0f, 50.0f, "%.2f");
|
|
item_tooltip("A dragged free end within this distance of another free end joins it on release");
|
|
ImGui::EndTable();
|
|
}
|
|
toggle_chips("snapflags", {{STR_C("Align with snapped path"), &m_track_align_tangent, "On joining, the end also takes over the direction of the other path, so the joint has no kink"},
|
|
{STR_C("Drag connected ends"), &m_track_drag_connected, "Dragging a joined end, or moving and turning the whole path or switch,\nmoves the ends of the neighbouring paths with it"}});
|
|
ImGui::TreePop();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void editor_mode::delete_selected_track()
|
|
{
|
|
auto *track{selected_track()};
|
|
if (track == nullptr)
|
|
return;
|
|
if (false == track->Dynamics.empty())
|
|
{
|
|
ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true);
|
|
return;
|
|
}
|
|
if (auto const set{double_of(*track)})
|
|
{
|
|
for (auto *part : {set->first, set->link, set->second})
|
|
if (false == part->Dynamics.empty())
|
|
{
|
|
ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true);
|
|
return;
|
|
}
|
|
if (auto const known{switch_store::named(*set->first)})
|
|
switch_store::forget(known->name);
|
|
auto main{set->first->m_paths.front()};
|
|
main.points[segment_data::point::end] = set->second->m_paths.front().points[segment_data::point::end];
|
|
auto const velocity{speed_check::path_velocity(*set->first, 0)};
|
|
for (auto *part : {set->first, set->link, set->second})
|
|
editor_track::retire(*part);
|
|
std::vector<TTrack *> created;
|
|
if (false == Global.shiftState)
|
|
{
|
|
auto *straight{editor_track::create_path(*set->first, main)};
|
|
editor_track::velocity(*straight, velocity);
|
|
editor_track::commit({straight});
|
|
created.push_back(straight);
|
|
ui()->set_status(STR_C("The double switch is replaced by its main track, Shift+Del removes all of it"), false);
|
|
}
|
|
push_track_snapshot({}, created, {set->first, set->link, set->second});
|
|
if (std::any_of(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), [&](TTrack const *Each) { return Each == set->first || Each == set->link || Each == set->second; }))
|
|
m_route = {};
|
|
m_node = nullptr;
|
|
ui()->set_node(nullptr);
|
|
m_track_point = {};
|
|
straight_refresh();
|
|
return;
|
|
}
|
|
// retired first, so the infrastructure it carried and moves elsewhere is in the same undo step
|
|
if (auto const known{switch_store::of(*track)})
|
|
switch_store::forget(known->name);
|
|
editor_track::retire(*track);
|
|
// a switch leaves its main track behind, so the line isn't broken, unless Shift asks for all of it
|
|
std::vector<TTrack *> created;
|
|
if (track->eType == tt_Switch && false == track->m_paths.empty() && false == Global.shiftState)
|
|
{
|
|
auto *main{editor_track::create_path(*track, track->m_paths.front())};
|
|
editor_track::velocity(*main, speed_check::path_velocity(*track, 0));
|
|
editor_track::commit({main});
|
|
created.push_back(main);
|
|
ui()->set_status(STR_C("The switch is replaced by its main track, Shift+Del removes all of it"), false);
|
|
}
|
|
push_track_snapshot({}, created, {track});
|
|
// the model and the events of a turntable can't stay without its track
|
|
if (track->eType == tt_Table)
|
|
for (auto const instance : turntable_includes(*track))
|
|
{
|
|
scene::Layers.removed(instance, true);
|
|
m_history.back().instances.push_back(instance);
|
|
}
|
|
if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end())
|
|
m_route = {};
|
|
m_node = nullptr;
|
|
ui()->set_node(nullptr);
|
|
m_track_point = {};
|
|
straight_refresh();
|
|
}
|
|
|
|
std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All) const
|
|
{
|
|
std::vector<key_hint> hints;
|
|
bool gizmo{false};
|
|
auto const *selected{selected_track()};
|
|
if (m_lay.active)
|
|
{
|
|
if (m_lay.points.empty())
|
|
hints = {{"LMB", "start, on the ground or at a free end"}, {"Drag from a free end", "lays the shape to the cursor"}, {"Shift+click a straight", "a parallel track (N)"}, {"Alt", "the direction in steps of 15 degrees"}, {"Esc", "stop laying"}};
|
|
else
|
|
{
|
|
bool const chained{m_lay.start.track != nullptr};
|
|
hints = {{"LMB", chained ? "lay the shape up to here" : "lay the straight up to here"}, {"LMB on a free end", "join and lay"}};
|
|
if (chained || m_lay.mouse_snap.track != nullptr)
|
|
hints.insert(hints.end(), {{"Ctrl+wheel", "radius"}, {"Shift+wheel", "another shape"}});
|
|
hints.insert(hints.end(), {{"Alt", "the direction in steps of 15 degrees"}, {"Enter", "lay up to the cursor"}, {"Backspace", m_lay.chain.empty() ? "take back the point" : "take back the last piece"}, {"Esc", "cancel"}});
|
|
}
|
|
}
|
|
else if (false == m_straights.detour.empty())
|
|
{
|
|
char const *const steps[] = {"leave the axis", "shifted", "start back", "back on the axis"};
|
|
auto const step{std::min<std::size_t>(m_straights.detour.size(), 3)};
|
|
hints = {{m_straights.tool == 2 ? "LMB" : "Shift+click", format("point %zu of 4: %s", step + 1, steps[step])}, {"Esc", "cancel"}};
|
|
}
|
|
else if (m_extend.active)
|
|
hints = {{"Drag", "lays the shape to the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "another shape"}, {"Release", "build"}, {"Esc", "cancel"}};
|
|
else if (m_switch.placing)
|
|
hints = m_switch.connection >= 0 && false == m_switch.curved ?
|
|
std::vector<key_hint>{{"Drag onto the other track", "the connection there"}, {"Shift+wheel", "another kind"}, {"Ctrl+wheel", "R of the aligning curve"}, {"Alt+wheel", "where a double crossover meets"}, {"Esc", "cancel"}} :
|
|
std::vector<key_hint>{{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}};
|
|
else if (m_track_tab == track_tab::lay)
|
|
hints = {{"LMB", "start, on the ground or at a free end"}};
|
|
else if (m_track_tab == track_tab::turnout && (selected_track() == nullptr || selected_track()->eType == tt_Switch))
|
|
{
|
|
switch (current_switch_kind())
|
|
{
|
|
case switch_kind::connection: hints = {{"Press on a straight", "and drag onto the other track"}, {"Shift+wheel", "another kind"}, {"Ctrl+wheel", "R of the aligning curve"}}; break;
|
|
case switch_kind::slip:
|
|
case switch_kind::crossing: hints = {{"LMB", "at a crossing of two straights"}}; break;
|
|
default: hints = {{"LMB on a track", "the straight or the curve for the switch"}}; break;
|
|
}
|
|
if (selected != nullptr)
|
|
{
|
|
hints.push_back({"Gizmo", "move the selected switch"});
|
|
hints.push_back({"Del", "remove the switch, its main track stays"});
|
|
gizmo = true;
|
|
}
|
|
}
|
|
else if (m_track_tab == track_tab::turnout && m_switch.armed < 0)
|
|
hints = {{"List in the panel", "the type of the switch"}, {"LMB on a track", "another track"}};
|
|
else if (m_track_tab == track_tab::turnout)
|
|
hints = {{"Press+drag on the track", "along: direction, sideways: side"}, {"List in the panel", "the type of the switch"}, {"LMB on another track", "choose it"}};
|
|
else if (m_track_tab == track_tab::profile && m_profile.route.spans.empty())
|
|
hints = {{"LMB on a track", "the grade line of its whole line"}, {"Shift+LMB on more tracks", "the route between the outermost ones"}, {"Shift+drag a box", "the route over the tracks in it"}};
|
|
else if (m_track_tab == track_tab::profile)
|
|
hints = {{"Drag a point", "changes the grade"}, {"Double click", "adds a point"}, {"LMB on another track", "its whole line"}, {"Shift+LMB on more tracks", "the route between the outermost ones"}, {"Shift+drag a box", "the route over the tracks in it"}, {"Apply", "writes the heights into the tracks"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::speed)
|
|
hints = {{"LMB on a row", "show the path"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::lineside)
|
|
hints = {{"LMB on a track", "the line the objects go along"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::parallel)
|
|
hints = {{"LMB on a track", "the new track goes beside it"}, {"Click on a side in the panel", "the side of the new track"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::signals && (m_signal.placing || m_signal.moving))
|
|
hints = {{"Drag along", m_signal.moving ? "moves the signal along the track" : "the trains it faces run that way"}, {"Release", "done"}, {"Esc", "cancel"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::signals && signal_armed() == nullptr)
|
|
hints = {{"List in the panel", "the type of the signal"}, {"Drag a signal", "moves it along the track"}, {"LMB on a signal", "selects it"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::signals)
|
|
{
|
|
hints = {{"LMB by a track", "places the signal on that side"}, {"Press+drag along", "the trains it faces run that way"}, {"Drag a signal", "moves it along the track"}, {"LMB on a signal", "selects it"}};
|
|
if (m_signal.selected != 0)
|
|
hints.push_back({"Del", "removes the selected signal with its event"});
|
|
}
|
|
else if (m_track_window_open && m_track_tab == track_tab::turntable && m_turntable.placing)
|
|
hints = {{"Drag", "turns the bridge"}, {"Release", "places the turntable"}, {"Esc", "cancel"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::turntable && m_turntable.table != nullptr)
|
|
hints = {{"LMB around it", "leads a track out"}, {"LMB on a free end", "fits that track to the turntable"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::turntable)
|
|
hints = {{"LMB", "places a new turntable"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::joints)
|
|
hints = {{"LMB on a row", "show the place"}, {"Fix", "joins the ends, aligns them"}};
|
|
else if (m_track_window_open && m_track_tab == track_tab::infra)
|
|
hints = {{"LMB", "select a path"}};
|
|
else if (m_track_tab == track_tab::straights && m_straights.tool == 1)
|
|
hints = {{"Press+drag on the straight", "breaks it there and turns the rest"}, {"Esc", "cancel"}};
|
|
else if (m_track_tab == track_tab::straights && m_straights.tool == 2)
|
|
hints = {{"LMB on the straight", "point 1 of 4: leave the axis"}, {"Esc", "cancel"}};
|
|
else if (m_track_tab == track_tab::straights && m_straights.dragging && m_straights.handle == 2)
|
|
{
|
|
auto const offer{straight_parallel_offer(m_straights.drag_line, m_straights.preview_start - m_straights.drag_line.start)};
|
|
if (offer.snaps)
|
|
hints = {{"Release", format(STR_C("parallel at %.2f m"), offer.spacing)}, {"Esc", "cancel"}};
|
|
else if (offer.nearby)
|
|
hints = {{"Near another straight", format(STR_C("snap at %.2f m (%.1f°)"), offer.spacing, offer.angle)}, {"Esc", "cancel"}};
|
|
else
|
|
hints = {{"Close to another straight", "snaps parallel at 4.00 / 4.50 / 4.75 m"}, {"Esc", "cancel"}};
|
|
}
|
|
else if (m_track_tab == track_tab::straights && m_straights.current.tracks.empty())
|
|
hints = {{"LMB on a straight", "its ends and the middle to drag"}, {"LMB on a curve", "its vertex and radius to drag"}};
|
|
else if (m_track_tab == track_tab::straights)
|
|
{
|
|
hints = {{"Drag an end", "the curves at it follow"}, {"Drag the middle", "shifts sideways, snaps parallel"}, {"Ctrl+drag", "breaks the straight"}, {"Shift+click x4", "shifts a piece around an obstacle"}, {"Alt+click", "adds another straight to move together"}};
|
|
}
|
|
else if (m_track_tab == track_tab::route && m_route.chain.tracks.empty())
|
|
hints = {{"LMB on a straight", "its ends and the middle to drag"}, {"LMB on a curve", "its vertex and radius to drag"}};
|
|
else if (m_track_tab == track_tab::route)
|
|
hints = {{"Drag a vertex", "the curve follows"}, {"Drag the radius grip", "changes the radius"}, {"Drag in the curvature band", "R up and down, transitions sideways"}, {"Apply", "writes the design into the tracks"}, {"Esc", "release the vertex"}};
|
|
else if (selected == nullptr)
|
|
hints = {{"LMB on a track", "selects it"}, {"Drag on the ground", "selects the tracks in a box"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}};
|
|
else if (m_track_tab == track_tab::path && m_track_set.size() >= 2 && in_track_set(selected))
|
|
{
|
|
hints = {{"Inspector", "the properties of all of them at once"}, {"Gizmo", "moves them together"}, {"Shift+LMB", "adds or takes out a track"}, {"Ctrl+Shift+V", "pastes the copied properties"}, {"Esc", "deselects"}, {"Del", "deletes them"}};
|
|
gizmo = true;
|
|
}
|
|
else if (m_point_drag.moved)
|
|
hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}};
|
|
else if (m_track_point.valid())
|
|
{
|
|
char const *action{"moves the point along the axes"};
|
|
if (false == editor_track::is_end(m_track_point.kind))
|
|
action = m_gizmo_op == gizmo_operation::rotate ? "turns the control vector sideways or up, cant at its end, the joined track keeps the tangent" : m_gizmo_op == gizmo_operation::scale ? "length of the control vector" : "moves the control point, the joined track keeps the tangent";
|
|
else if (m_gizmo_op == gizmo_operation::rotate)
|
|
action = "rings about the point: direction, grade, cant; the joined tracks follow";
|
|
else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, true) == gizmo_operation::scale)
|
|
action = "lengthens the track at this end, the curve keeps its radius";
|
|
hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Esc", "releases the point"}};
|
|
gizmo = true;
|
|
}
|
|
else
|
|
{
|
|
char const *action{"moves the whole track"};
|
|
if (m_gizmo_op == gizmo_operation::rotate)
|
|
action = "rings: direction, grade, cant; the joined tracks follow";
|
|
else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, false) == gizmo_operation::scale)
|
|
action = "lengthens the track at its free end, the curve keeps its radius";
|
|
hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}};
|
|
gizmo = true;
|
|
}
|
|
if (auto const *meaning{typed_meaning()}; meaning != nullptr)
|
|
hints.insert(hints.begin() + std::min<std::size_t>(1, hints.size()), {"0-9", meaning});
|
|
if (All)
|
|
{
|
|
if (gizmo && m_gizmo_snap > 0.0f)
|
|
hints.push_back({"Ctrl", format("gizmo snaps by %.2f m", m_gizmo_snap)});
|
|
hints.push_back({"K", "split the path under the cursor"});
|
|
hints.push_back({"O", "top view"});
|
|
hints.push_back({"Esc", "back to Select"});
|
|
if (Global.EditorOrtho)
|
|
hints.push_back({"Hold the wheel", "pans the view"});
|
|
hints.push_back({"Ctrl+Z", "undo"});
|
|
hints.push_back({"Ctrl+Y", "redo"});
|
|
hints.push_back({"L T G C B P V J I", "modes: lay, switch, straight, curve, objects, profile, speed, joints, infra"});
|
|
}
|
|
for (auto &hint : hints)
|
|
{
|
|
hint.raw = hint.key;
|
|
hint.key = STR_C(hint.key);
|
|
hint.action = STR(hint.action);
|
|
}
|
|
return hints;
|
|
}
|
|
|
|
// values of the change in progress, shown next to the cursor
|
|
std::string editor_mode::track_readout() const
|
|
{
|
|
auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); };
|
|
auto const azimuth = [](glm::dvec3 const &From, glm::dvec3 const &To) {
|
|
auto const degrees{glm::degrees(std::atan2(To.x - From.x, To.z - From.z))};
|
|
return degrees < 0.0 ? degrees + 360.0 : degrees;
|
|
};
|
|
if (m_extend.active)
|
|
{
|
|
double length{0.0};
|
|
double radius{0.0};
|
|
auto const pieces{extend_pieces()};
|
|
for (auto const &piece : pieces)
|
|
{
|
|
length += bezier{piece}.plan_length();
|
|
if (piece.radius != 0.0f && (radius == 0.0 || std::abs(piece.radius) < radius))
|
|
radius = std::abs(piece.radius);
|
|
}
|
|
editor_track::snap_target target;
|
|
auto const snapped{extend_snap(target) && target.track != nullptr};
|
|
editor_track::branch_link link;
|
|
std::string reason;
|
|
if (plan_branch_link(link, reason))
|
|
return link_summary(target, link, editor_track::branch_radius(m_extend.track->m_paths[1]));
|
|
if (snapped && pieces.empty())
|
|
return format(STR_C("no track fits to the end of %s"), name(target.track).c_str());
|
|
auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m %s", length, STR_C("straight"))};
|
|
if (snapped)
|
|
text += "\n" + format(STR_C("joins %s"), name(target.track).c_str());
|
|
if (false == reason.empty())
|
|
text += "\n" + reason;
|
|
return text + "\n" + track_shaping(snapped);
|
|
}
|
|
if (m_lay.active)
|
|
{
|
|
auto const &lay{m_lay};
|
|
std::string text;
|
|
if (lay.points.empty())
|
|
text = lay.mouse_snap.track != nullptr ? format(STR_C("starts at the end of %s"), name(lay.mouse_snap.track).c_str()) : std::string{STR_C("start")};
|
|
else if (false == lay.preview.empty())
|
|
{
|
|
double radius{0.0};
|
|
bool straight{false};
|
|
for (auto const &piece : lay.preview)
|
|
{
|
|
auto const r{piece_plan_radius(piece)};
|
|
if (r < 0.5)
|
|
straight = true;
|
|
else if (radius == 0.0 || r < radius)
|
|
radius = r;
|
|
}
|
|
if (radius > 0.0)
|
|
text = straight ? format("L %.1f m %s + R %.0f m", lay.preview_length, STR_C("straight"), radius) : format("L %.1f m R %.0f m", lay.preview_length, radius);
|
|
else
|
|
text = format("L %.1f m %s", lay.preview_length, STR_C("straight"));
|
|
}
|
|
if (false == lay.points.empty() && lay.mouse_snap.track != nullptr)
|
|
text += "\n" + format(STR_C("joins %s"), name(lay.mouse_snap.track).c_str());
|
|
if (false == lay.preview_error.empty())
|
|
text += "\n" + lay.preview_error;
|
|
if (lay.points.size() == 1 && (lay.start.track != nullptr || lay.mouse_snap.track != nullptr))
|
|
text += "\n" + track_shaping(lay.mouse_snap.track != nullptr && lay.start.track != nullptr);
|
|
if (ImGui::GetIO().KeyAlt && false == lay.points.empty())
|
|
text += "\n" + format(STR_C("azimuth %.0f deg"), azimuth(lay.points.back(), lay.mouse));
|
|
if (false == lay.status.empty() && ImGui::GetTime() - lay.status_time < 4.0)
|
|
text += "\n" + lay.status;
|
|
return text;
|
|
}
|
|
auto const &state{m_straights};
|
|
if (state.tool_placed && state.tool_mouse)
|
|
{
|
|
auto const &line{state.tool_line};
|
|
auto const offset{plan_of(state.tool_handle - state.tool_point)};
|
|
if (state.tool == 1)
|
|
{
|
|
if (glm::length(offset) < 1e-3)
|
|
return {};
|
|
auto const turn{glm::degrees(signed_angle(line.direction, glm::normalize(offset)))};
|
|
return format(STR_C("turn %+.3f deg R %.0f m"), turn, straight_tool_radius(line));
|
|
}
|
|
glm::dvec2 const normal{-line.direction.y, line.direction.x};
|
|
return format(STR_C("shift %+.3f m over %.2f m"), glm::dot(offset, normal), glm::dot(offset, line.direction));
|
|
}
|
|
if (state.dragging && state.fitting)
|
|
{
|
|
auto const &result{m_route.result};
|
|
if (false == result.valid)
|
|
return result.errors.empty() ? std::string{STR_C("no curve fits")} : result.errors.front();
|
|
auto text{format(STR_C("curve L %.2f m"), result.length)};
|
|
for (auto const &curve : result.curves)
|
|
text += format(STR_C(" deflection %.3f deg"), glm::degrees(curve.deflection));
|
|
return text;
|
|
}
|
|
if (state.dragging)
|
|
{
|
|
auto const &grabbed{state.drag_line};
|
|
if (state.handle == 2)
|
|
{
|
|
glm::dvec2 const normal{-grabbed.direction.y, grabbed.direction.x};
|
|
auto const offset{state.preview_start - grabbed.start};
|
|
auto text{format(STR_C("shift %+.3f m"), glm::dot(plan_of(offset), normal))};
|
|
auto const offer{straight_parallel_offer(grabbed, offset)};
|
|
if (offer.snaps)
|
|
text += "\n" + format(STR_C("parallel %.2f m"), offer.spacing);
|
|
else if (offer.nearby)
|
|
text += "\n" + format(STR_C("close: %.2f m %.1f°"), offer.spacing, offer.angle);
|
|
return text;
|
|
}
|
|
return format(STR_C("L %.3f m (%+.3f) azimuth %.4f deg"), plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length,
|
|
azimuth(state.preview_start, state.preview_end));
|
|
}
|
|
if (m_track_set_using && m_track_gizmo_drag.operation == gizmo_operation::rotate)
|
|
return format(STR_C("turned %+.2f deg"), glm::degrees(m_track_gizmo_drag.angle));
|
|
auto const *track{selected_track()};
|
|
if (m_track_gizmo_using && track != nullptr)
|
|
{
|
|
auto const &drag{m_track_gizmo_drag};
|
|
if (drag.control)
|
|
{
|
|
auto const control{editor_track::point_position(*track, m_track_point) - editor_track::point_position(*track, editor_track::end_of(m_track_point))};
|
|
auto text{format(STR_C("control %.2f m azimuth %.2f deg"), glm::length(control), azimuth(glm::dvec3{0.0}, control))};
|
|
if (drag.operation == gizmo_operation::rotate && drag.ring == 1)
|
|
text += format(STR_C(" turned %+.2f deg"), glm::degrees(drag.angle));
|
|
if (drag.operation == gizmo_operation::rotate && drag.ring == 0)
|
|
text += format(STR_C(" grade %+.1f per mille"), 1000.0 * grade_of(control) * (glm::dot(level(control), drag.basis[2]) >= 0.0 ? 1.0 : -1.0));
|
|
if (drag.operation == gizmo_operation::rotate && drag.ring == 2)
|
|
text += " " + format(STR_C("cant %.0f mm (%+.0f mm)"), 1500.0 * std::sin(glm::radians(drag.roll0 + drag.roll)), 1500.0 * (std::sin(glm::radians(drag.roll0 + drag.roll)) - std::sin(glm::radians(drag.roll0))));
|
|
if (drag.fixed != glm::dvec3{})
|
|
text += "\n" + STR("along the switch");
|
|
return text;
|
|
}
|
|
if (drag.operation == gizmo_operation::scale && drag.path >= 0)
|
|
{
|
|
auto text{format("L %.3f m (%+.3f)", drag.stretch, drag.stretch - drag.length)};
|
|
if (drag.path < static_cast<int>(drag.paths.size()) && std::abs(drag.paths[drag.path].radius) > 0.5f)
|
|
text += format(" R %.1f m", std::abs(drag.paths[drag.path].radius));
|
|
return text;
|
|
}
|
|
if (drag.operation == gizmo_operation::rotate && drag.ring == 0)
|
|
return format(STR_C("grade changed by %+.1f per mille"), 1000.0 * drag.grade);
|
|
if (drag.operation == gizmo_operation::rotate && drag.ring == 2)
|
|
return format(STR_C("cant %.0f mm (%+.0f mm)"), 1500.0 * std::sin(glm::radians(drag.roll0 + drag.roll)), 1500.0 * (std::sin(glm::radians(drag.roll0 + drag.roll)) - std::sin(glm::radians(drag.roll0))));
|
|
if (drag.operation == gizmo_operation::rotate)
|
|
return format(STR_C("turned %+.2f deg"), glm::degrees(drag.angle));
|
|
if (m_track_point.valid() && m_track_point.path < static_cast<int>(track->m_paths.size()))
|
|
{
|
|
auto const &path{track->m_paths[m_track_point.path]};
|
|
auto text{format("L %.3f m", bezier{path}.plan_length())};
|
|
if (path.radius != 0.0f)
|
|
text += format(" R %.1f m", std::abs(path.radius));
|
|
if (m_track_snap.track != nullptr)
|
|
text += "\n" + format(STR_C("joins %s"), name(m_track_snap.track).c_str());
|
|
return text;
|
|
}
|
|
return format(STR_C("moved %.3f m"), glm::distance(editor_track::pivot(*track), m_track_pivot));
|
|
}
|
|
return {};
|
|
}
|
|
|
|
std::string editor_mode::track_shaping(bool const Joined) const
|
|
{
|
|
if (false == Joined)
|
|
{
|
|
char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"};
|
|
auto text{std::string{STR_C(names[static_cast<int>(m_track_layout)])}};
|
|
if (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc)
|
|
text += format(" R %.0f m", m_lay.radius);
|
|
return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: arc / straight");
|
|
}
|
|
auto const &join{m_join_last};
|
|
char const *const kinds[]{"", "straight", "arc", "curve", "reverse curve", "free curve"};
|
|
std::string text{STR_C(kinds[static_cast<int>(join.kind)])};
|
|
if (join.radius > 0.0)
|
|
text += format(" R %.0f m", join.radius) + (m_join_radius > 0.0 ? std::string{} : std::string{" "} + STR("(auto)"));
|
|
if (join.cant > 0.0)
|
|
text += format(STR_C(" cant %.0f mm"), join.cant);
|
|
if (join.transition > 0.0)
|
|
text += format(STR_C(" transitions %.0f m"), join.transition);
|
|
if (join.speed > 0.0 && (join.cant > 0.0 || join.transition > 0.0))
|
|
text += format(" V %.0f km/h", join.speed);
|
|
if (join.variants > 1)
|
|
text += format(" %d/%d", (m_join_variant % join.variants + join.variants) % join.variants + 1, join.variants);
|
|
return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: another shape");
|
|
}
|
|
|
|
void editor_mode::draw_empty_scenery_hint()
|
|
{
|
|
if ((m_track_tab != track_tab::path && m_track_tab != track_tab::lay) || (m_lay.active && false == m_lay.points.empty()) || m_first_steps_open || ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel))
|
|
return;
|
|
auto const &paths{simulation::Paths.sequence()};
|
|
if (std::any_of(paths.begin(), paths.end(), [](TTrack const *Track) { return Track != nullptr && false == Track->m_editorremoved; }))
|
|
return;
|
|
auto *list{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
|
|
auto const low{editor_ui::view_min()};
|
|
auto const high{editor_ui::view_max()};
|
|
auto const font{ImGui::GetFontSize()};
|
|
auto const pad{std::round(font * 1.0f)};
|
|
auto const picture{std::round(ImGui::GetTextLineHeight() * 1.3f)};
|
|
auto const title{STR_C("The scenery has no tracks yet")};
|
|
std::vector<std::pair<char const *, char const *>> rows;
|
|
if (m_track_tab == track_tab::path)
|
|
rows.emplace_back("L", "the lay tool");
|
|
rows.emplace_back("LMB", "on the ground: the first point, then each next one lays a piece");
|
|
rows.emplace_back("Ctrl+wheel", "the radius of the curve while laying");
|
|
rows.emplace_back("F1", "all the shortcuts, and the tutorial");
|
|
auto const width{std::min(high.x - low.x - 2.0f * pad, std::round(font * 30.0f))};
|
|
auto const column{std::round(picture * 5.0f)};
|
|
auto const wrap{width - 2.0f * pad - column};
|
|
float height{pad + font * 2.4f + pad * 0.5f};
|
|
for (auto const &row : rows)
|
|
height += std::max(picture, ImGui::CalcTextSize(STR_C(row.second), nullptr, false, wrap).y) + pad * 0.45f;
|
|
height += pad * 0.6f;
|
|
ImVec2 const top{std::round((low.x + high.x - width) * 0.5f), std::round((low.y + high.y - height) * 0.5f)};
|
|
ImVec2 const bottom{top.x + width, top.y + height};
|
|
auto const accent{family_colour(0)};
|
|
list->AddRectFilled(top, bottom, IM_COL32(16, 20, 26, 215), std::round(font * 0.6f));
|
|
list->AddRect(top, bottom, (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 160), std::round(font * 0.6f), 0, 1.5f);
|
|
auto const icon{std::round(font * 2.4f)};
|
|
editor_icons::draw(list, editor_icons::icon::lay_track, ImVec2(top.x + pad + icon * 0.5f, top.y + pad + icon * 0.5f), accent);
|
|
auto const big{font * 1.25f};
|
|
list->AddText(nullptr, big, ImVec2(top.x + pad + icon + pad * 0.6f, std::round(top.y + pad + (icon - big) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text), title);
|
|
auto y{top.y + pad + icon + pad * 0.5f};
|
|
for (auto const &row : rows)
|
|
{
|
|
auto const text{STR_C(row.second)};
|
|
auto const size{ImGui::CalcTextSize(text, nullptr, false, wrap)};
|
|
auto const line{std::max(picture, size.y)};
|
|
gesture_draw(*list, ImVec2(top.x + pad, std::round(y + (line - picture) * 0.5f)), picture, gesture_picture(row.first), ImGui::GetColorU32(ImGuiCol_Text));
|
|
list->AddText(nullptr, 0.0f, ImVec2(top.x + pad + column, std::round(y + (line - size.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text, 0.85f), text, nullptr, wrap);
|
|
y += line + pad * 0.45f;
|
|
}
|
|
}
|
|
|
|
void editor_mode::draw_track_hints()
|
|
{
|
|
ImGuiIO const &io = ImGui::GetIO();
|
|
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
|
|
float const margin{12.0f};
|
|
float const gap{14.0f};
|
|
float const pad{4.0f};
|
|
auto const chip = [&](ImVec2 const At, char const *Text, ImU32 const Background, ImU32 const Foreground) {
|
|
auto const size{ImGui::CalcTextSize(Text)};
|
|
drawlist->AddRectFilled(ImVec2(At.x, At.y - 2.0f), ImVec2(At.x + size.x + 2.0f * pad, At.y + size.y + 2.0f), Background, 3.0f);
|
|
drawlist->AddText(ImVec2(At.x + pad, At.y), Foreground, Text);
|
|
return size.x + 2.0f * pad;
|
|
};
|
|
auto hints{track_key_hints(false)};
|
|
auto const picture{std::round(ImGui::GetTextLineHeight() * 1.15f)};
|
|
std::vector<std::vector<gesture_word>> pictures;
|
|
for (auto const &hint : hints)
|
|
pictures.push_back(gesture_picture(hint.raw != nullptr ? hint.raw : hint.key));
|
|
auto const picture_width = [&](std::size_t const Index) { return pictures[Index].empty() ? 0.0f : gesture_width(picture, pictures[Index]) + 5.0f; };
|
|
auto const width_of = [&](std::size_t const Index) {
|
|
auto const chip_width{hints[Index].raw != nullptr && gesture_keys_only(hints[Index].raw) ? 0.0f : ImGui::CalcTextSize(hints[Index].key).x + 2.0f * pad + 5.0f};
|
|
return picture_width(Index) + chip_width + ImGui::CalcTextSize(hints[Index].action.c_str()).x;
|
|
};
|
|
|
|
auto const mode{track_mode_name()};
|
|
auto const left{editor_ui::view_min().x + margin};
|
|
auto const available{editor_ui::view_max().x - editor_ui::view_min().x - 2.0f * margin};
|
|
float width{ImGui::CalcTextSize(mode.c_str()).x + 2.0f * pad};
|
|
std::size_t shown{0};
|
|
for (; shown < std::min<std::size_t>(hints.size(), 3); ++shown)
|
|
{
|
|
auto const next{width + gap + width_of(shown)};
|
|
if (next > available)
|
|
break;
|
|
width = next;
|
|
}
|
|
auto const height{ImGui::GetTextLineHeight()};
|
|
auto const bottom{m_profile.strip_top > editor_ui::view_min().y + 4.0f * height ? std::min(editor_ui::view_max().y, m_profile.strip_top) : editor_ui::view_max().y};
|
|
auto const y{bottom - margin - height};
|
|
ImVec2 const from{left - 6.0f, y - pad - 2.0f};
|
|
ImVec2 const to{left + width + 6.0f, y + height + pad};
|
|
auto const hovered{io.MousePos.x > from.x - 40.0f && io.MousePos.x < to.x + 40.0f && io.MousePos.y > from.y - 40.0f && io.MousePos.y < to.y + 40.0f};
|
|
auto const fade = [hovered](ImU32 const Colour) { return hovered ? (Colour & ~IM_COL32_A_MASK) | (((Colour >> IM_COL32_A_SHIFT) & 0xFF) / 4 << IM_COL32_A_SHIFT) : Colour; };
|
|
drawlist->AddRectFilled(from, to, fade(IM_COL32(0, 0, 0, 160)), 4.0f);
|
|
auto x{left};
|
|
auto const &modes{track_modes()};
|
|
auto const tab{false == m_track_window_open ? track_tab::path : m_track_tab == track_tab::route ? track_tab::straights : m_track_tab};
|
|
auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == tab; })};
|
|
auto const family{(family_colour(found != modes.end() ? found->family : 0) & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 235)};
|
|
x += chip(ImVec2(x, y), mode.c_str(), fade(family), fade(IM_COL32(255, 255, 255, 255)));
|
|
for (std::size_t i = 0; i < shown; ++i)
|
|
{
|
|
x += gap;
|
|
if (false == pictures[i].empty())
|
|
x += gesture_draw(*drawlist, ImVec2(x, std::round(y + (height - picture) * 0.5f)), picture, pictures[i], fade(IM_COL32(255, 255, 255, 230))) + 5.0f;
|
|
if (hints[i].raw == nullptr || false == gesture_keys_only(hints[i].raw))
|
|
x += chip(ImVec2(x, y), hints[i].key, fade(IM_COL32(255, 210, 60, 220)), fade(IM_COL32(20, 20, 20, 255))) + 5.0f;
|
|
drawlist->AddText(ImVec2(x, y), fade(IM_COL32(255, 255, 255, 230)), hints[i].action.c_str());
|
|
x += ImGui::CalcTextSize(hints[i].action.c_str()).x;
|
|
}
|
|
|
|
auto readout{track_readout()};
|
|
if (auto const *meaning{typed_meaning()}; meaning != nullptr && false == m_typed.empty())
|
|
readout += (readout.empty() ? "" : "\n") + std::string{"= "} + m_typed + "_ " + STR(meaning);
|
|
if (readout.empty() || ImGui::GetIO().WantCaptureMouse)
|
|
return;
|
|
auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())};
|
|
auto const size{ImGui::CalcTextSize(readout.c_str())};
|
|
ImVec2 at{io.MousePos.x + 20.0f, io.MousePos.y + 20.0f};
|
|
at.x = std::min(at.x, io.DisplaySize.x - size.x - 10.0f);
|
|
at.y = std::min(at.y, io.DisplaySize.y - size.y - 10.0f);
|
|
foreground->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), IM_COL32(0, 0, 0, 190), 4.0f);
|
|
foreground->AddRect(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), overlay_color::marked, 4.0f);
|
|
foreground->AddText(at, IM_COL32(255, 255, 255, 255), readout.c_str());
|
|
}
|
|
|
|
void editor_mode::draw_build_overlay() const
|
|
{
|
|
screen_projection const projection;
|
|
ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport());
|
|
auto const drawpath = [&](segment_data const &Path, ImU32 const Color) {
|
|
for (auto const &piece : Path.pieces.empty() ? std::vector<segment_data>{Path} : Path.pieces)
|
|
{
|
|
bezier const curve{piece};
|
|
auto previous{curve.point(0.0)};
|
|
for (int i = 1; i <= 24; ++i)
|
|
{
|
|
auto const next{curve.point(i / 24.0)};
|
|
projection.line(drawlist, previous, next, Color, 3.0f);
|
|
previous = next;
|
|
}
|
|
}
|
|
};
|
|
if (false == GfxRenderer->Mouse_Hit() && false == ImGui::GetIO().WantCaptureMouse)
|
|
{
|
|
auto const centre{m_lay.active ? m_lay.mouse : cursor_ground()};
|
|
double const cell{10.0};
|
|
int const reach{10};
|
|
glm::dvec3 const origin{std::round(centre.x / cell) * cell, centre.y, std::round(centre.z / cell) * cell};
|
|
for (int i = -reach; i <= reach; ++i)
|
|
{
|
|
auto const major{std::fmod(std::abs(origin.x / cell + i), 10.0) < 0.5};
|
|
auto const colour{major ? IM_COL32(255, 255, 255, 70) : IM_COL32(255, 255, 255, 35)};
|
|
for (int j = -reach; j < reach; ++j)
|
|
projection.line(drawlist, origin + glm::dvec3{i * cell, 0.0, j * cell}, origin + glm::dvec3{i * cell, 0.0, (j + 1) * cell}, colour, 1.0f);
|
|
}
|
|
for (int j = -reach; j <= reach; ++j)
|
|
{
|
|
auto const major{std::fmod(std::abs(origin.z / cell + j), 10.0) < 0.5};
|
|
auto const colour{major ? IM_COL32(255, 255, 255, 70) : IM_COL32(255, 255, 255, 35)};
|
|
for (int i = -reach; i < reach; ++i)
|
|
projection.line(drawlist, origin + glm::dvec3{i * cell, 0.0, j * cell}, origin + glm::dvec3{(i + 1) * cell, 0.0, j * cell}, colour, 1.0f);
|
|
}
|
|
}
|
|
if (m_track_tab == track_tab::path && false == m_lay.active && false == m_track_gizmo_using)
|
|
if (auto const spot{split_spot()}; spot.track != nullptr)
|
|
{
|
|
ImVec2 at;
|
|
if (projection.project(spot.point, at))
|
|
{
|
|
drawlist->AddCircle(at, 6.0f, overlay_color::highlight, 12, 1.5f);
|
|
drawlist->AddText({at.x + 9.0f, at.y - 18.0f}, overlay_color::highlight, STR_C("K: split here"));
|
|
}
|
|
}
|
|
auto const &display{ImGui::GetIO().DisplaySize};
|
|
// in the 3D view only near the camera, where they can be told apart; from above all of them
|
|
glm::dvec3 const eye{Global.pCamera.Pos};
|
|
auto const in_reach = [&](glm::dvec3 const &Point) { return Global.EditorOrtho || glm::distance(Point, eye) <= kDotsRange; };
|
|
for (auto const *track : simulation::Paths.sequence())
|
|
{
|
|
if (track == nullptr || track->m_editorremoved || track->m_paths.empty())
|
|
continue;
|
|
for (auto const &path : track->m_paths)
|
|
for (auto const point : {segment_data::point::start, segment_data::point::end})
|
|
{
|
|
ImVec2 screen;
|
|
if (in_reach(path.points[point]) && projection.project(path.points[point], screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y)
|
|
drawlist->AddCircleFilled(screen, 3.5f, kJointColor);
|
|
}
|
|
}
|
|
if (m_extend.active)
|
|
{
|
|
if (m_extend.track != nullptr)
|
|
draw_free_ends(m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track});
|
|
crossover_plan plan;
|
|
editor_track::snap_target target;
|
|
if (extend_snap(target))
|
|
{
|
|
ImVec2 screen;
|
|
if (projection.project(target.position, screen))
|
|
drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f);
|
|
for (auto const &piece : extend_pieces())
|
|
drawpath(piece, overlay_color::marked);
|
|
}
|
|
else if (plan_crossover(plan))
|
|
{
|
|
for (auto const &piece : plan.paths)
|
|
drawpath(piece, overlay_color::marked);
|
|
drawpath(plan.insert, overlay_color::grip);
|
|
}
|
|
else
|
|
{
|
|
for (auto const &piece : extend_pieces())
|
|
drawpath(piece, overlay_color::marked);
|
|
}
|
|
}
|
|
if (m_switch.placing && m_switch.connection >= 0 && false == m_switch.curved)
|
|
{
|
|
connection::request request;
|
|
connection::result plan;
|
|
auto const valid{plan_connection(request, plan)};
|
|
if (valid)
|
|
{
|
|
for (auto const &entry : plan.switches)
|
|
for (auto const &path : entry.paths)
|
|
drawpath(path, overlay_color::marked);
|
|
for (auto const &insert : plan.inserts)
|
|
drawpath(insert, overlay_color::grip);
|
|
if (plan.crossing)
|
|
for (int i = 0; i < 4; i += 2)
|
|
{
|
|
segment_data path;
|
|
path.points[segment_data::point::start] = plan.ends[i];
|
|
path.points[segment_data::point::end] = plan.ends[i + 1];
|
|
drawpath(path, overlay_color::grip);
|
|
}
|
|
}
|
|
else
|
|
for (auto const &piece : switch_preview())
|
|
drawpath(piece, overlay_color::invalid);
|
|
ImVec2 screen;
|
|
auto const text{valid ? connection_readout(plan) : plan.error.empty() ? std::string{STR_C("Drag onto the other track")} : plan.error};
|
|
if (projection.project(m_switch.mouse, screen))
|
|
{
|
|
auto const size{ImGui::CalcTextSize(text.c_str())};
|
|
drawlist->AddRectFilled(ImVec2(screen.x + 14.0f, screen.y + 10.0f), ImVec2(screen.x + 22.0f + size.x, screen.y + 14.0f + size.y), IM_COL32(0, 0, 0, 180), 3.0f);
|
|
drawlist->AddText(ImVec2(screen.x + 18.0f, screen.y + 12.0f), valid ? (plan.warning.empty() ? overlay_color::highlight : overlay_color::warning) : overlay_color::invalid, text.c_str());
|
|
}
|
|
}
|
|
else if (m_switch.placing)
|
|
{
|
|
for (auto const &piece : switch_preview())
|
|
drawpath(piece, overlay_color::marked);
|
|
switch_link plan;
|
|
auto const linked{plan_switch_link(plan)};
|
|
ImVec2 screen;
|
|
if (plan.target.track != nullptr && projection.project(plan.target.position, screen))
|
|
{
|
|
drawlist->AddCircle(screen, 14.0f, linked ? IM_COL32(255, 60, 255, 255) : overlay_color::invalid, 20, 3.0f);
|
|
if (false == linked && false == plan.reason.empty())
|
|
drawlist->AddText({screen.x + 18.0f, screen.y - 8.0f}, overlay_color::invalid, plan.reason.c_str());
|
|
}
|
|
}
|
|
if (m_lay.active)
|
|
{
|
|
auto const &lay{m_lay};
|
|
auto const piece_colour = [&](segment_data const &Path) {
|
|
if (false == lay.preview_error.empty())
|
|
return overlay_color::warning;
|
|
return piece_plan_radius(Path) < 0.5 ? IM_COL32(255, 255, 255, 230) : overlay_color::marked;
|
|
};
|
|
for (auto const &piece : lay.preview)
|
|
drawpath(piece, piece_colour(piece));
|
|
for (std::size_t i = 0, n = lay.preview.size(); i < n;)
|
|
{
|
|
auto const radius{piece_plan_radius(lay.preview[i])};
|
|
bool const curved{radius >= 0.5};
|
|
std::size_t j{i + 1};
|
|
double length{bezier{lay.preview[i]}.plan_length()};
|
|
double sharpest{radius};
|
|
for (; j < n; ++j)
|
|
{
|
|
auto const next{piece_plan_radius(lay.preview[j])};
|
|
if ((next >= 0.5) != curved)
|
|
break;
|
|
length += bezier{lay.preview[j]}.plan_length();
|
|
if (curved && next < sharpest)
|
|
sharpest = next;
|
|
}
|
|
double along{0.0};
|
|
glm::dvec3 middle{bezier{lay.preview[i]}.point(0.5)};
|
|
for (std::size_t k = i; k < j; ++k)
|
|
{
|
|
auto const piece{bezier{lay.preview[k]}.plan_length()};
|
|
if (along + piece >= length * 0.5)
|
|
{
|
|
middle = bezier{lay.preview[k]}.point(piece > 1e-6 ? std::clamp((length * 0.5 - along) / piece, 0.0, 1.0) : 0.5);
|
|
break;
|
|
}
|
|
along += piece;
|
|
}
|
|
ImVec2 screen;
|
|
if (projection.project(middle, screen))
|
|
{
|
|
auto const label{curved ? format("R %.0f", sharpest) : std::string{STR_C("straight")}};
|
|
auto const size{ImGui::CalcTextSize(label.c_str())};
|
|
drawlist->AddRectFilled(ImVec2(screen.x + 6.0f, screen.y - 8.0f), ImVec2(screen.x + 14.0f + size.x, screen.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f);
|
|
drawlist->AddText(ImVec2(screen.x + 10.0f, screen.y - 8.0f), curved ? overlay_color::marked : overlay_color::highlight, label.c_str());
|
|
}
|
|
i = j;
|
|
}
|
|
for (std::size_t i = 0; i + 1 < lay.points.size(); ++i)
|
|
projection.line(drawlist, lay.points[i], lay.points[i + 1], IM_COL32(255, 255, 255, 90), 1.0f);
|
|
for (auto const &point : lay.points)
|
|
{
|
|
ImVec2 screen;
|
|
if (projection.project(point, screen))
|
|
drawlist->AddCircleFilled(screen, 5.0f, overlay_color::grip);
|
|
}
|
|
ImVec2 screen;
|
|
if (lay.mouse_snap.track != nullptr && projection.project(lay.mouse_snap.position, screen))
|
|
drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f);
|
|
if (lay.start.track != nullptr && projection.project(lay.start.position, screen))
|
|
drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 60, 255, 255), 20, 2.0f);
|
|
if (false == m_extend.active)
|
|
draw_free_ends(nullptr, kRailCategory, {});
|
|
}
|
|
if (false == m_straights.detour.empty())
|
|
{
|
|
auto const outline{detour_outline()};
|
|
for (std::size_t i = 0; i + 1 < outline.size(); ++i)
|
|
projection.line(drawlist, outline[i], outline[i + 1], overlay_color::marked, 3.0f);
|
|
for (std::size_t i = 0; i < outline.size() && i < m_straights.detour.size(); ++i)
|
|
{
|
|
ImVec2 screen;
|
|
if (projection.project(outline[i], screen))
|
|
drawlist->AddCircleFilled(screen, 6.0f, overlay_color::invalid);
|
|
}
|
|
}
|
|
if ((m_point_drag.moved || (m_track_gizmo_using && m_track_point.valid())) && editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty())
|
|
if (auto const *track{selected_track()}; track != nullptr)
|
|
draw_free_ends(track, track->iCategoryFlag & 15, std::vector<TTrack const *>(m_track_drag.begin(), m_track_drag.end()));
|
|
}
|