mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 23:23:30 +02:00
Besides the steps of 15 degrees, Alt snaps the laid straight to the ratios of the switches (1:4.8 to 1:26.5) of the straight track the straight crosses, or of one within 30 m of where it starts; the label at the cursor says which ratio to which track. The first click with Alt within 2 m of a straight track puts the start on its axis, so a branch or a crossing at 1:9 starts exactly on the track.
2246 lines
98 KiB
C++
2246 lines
98 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, or 1:9, 1:6.6... to a straight track"}, {"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, or 1:9, 1:6.6... to a straight track"}, {"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" + (lay.skew.empty() ? format(STR_C("azimuth %.0f deg"), azimuth(lay.points.back(), lay.mouse)) : lay.skew);
|
|
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 (auto const *selected{selected_track()}; false == m_switch.placing && m_track_tab == track_tab::turnout && m_switch.connection < 0 && m_switch.armed >= 0 && m_switch.armed < static_cast<int>(m_switch.templates.size()) &&
|
|
m_switch.templates[m_switch.armed].double_slip && selected != nullptr && selected == m_straights.current_for && false == m_straights.current.tracks.empty() &&
|
|
false == ImGui::GetIO().WantCaptureMouse)
|
|
{
|
|
// where the slip goes for a click there, before the click
|
|
auto const &line{m_straights.current};
|
|
auto const cursor{cursor_on_straight(line)};
|
|
auto const along{std::clamp(line.along(cursor), 0.0, line.length)};
|
|
if (plan_distance(line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along, cursor) <= kSwitchReach)
|
|
{
|
|
slip_plan plan;
|
|
auto const valid{plan_double_slip(line, cursor, m_switch.templates[m_switch.armed], plan)};
|
|
std::string text{plan.problem};
|
|
glm::dvec3 marker{cursor};
|
|
ImU32 colour{overlay_color::invalid};
|
|
if (valid && plan.replaces != nullptr)
|
|
{
|
|
marker = plan.replaces->location();
|
|
colour = overlay_color::highlight;
|
|
text = STR_C("Click: this double slip is replaced by the one chosen");
|
|
}
|
|
else if (valid)
|
|
{
|
|
for (int i = 0; i < 4; i += 2)
|
|
{
|
|
segment_data path;
|
|
path.points[segment_data::point::start] = plan.points[i];
|
|
path.points[segment_data::point::end] = plan.points[i + 1];
|
|
drawpath(path, overlay_color::marked);
|
|
}
|
|
marker = {plan.crossing.x, line.start.y + line.grade * plan.alongfirst, plan.crossing.y};
|
|
colour = overlay_color::highlight;
|
|
text = plan.note.empty() ? std::string{STR_C("Click: it goes here")} : plan.note;
|
|
}
|
|
ImVec2 screen;
|
|
if (projection.project(marker, screen))
|
|
{
|
|
drawlist->AddCircle(screen, 12.0f, colour, 20, 2.5f);
|
|
drawlist->AddText({screen.x + 16.0f, screen.y - 8.0f}, colour, 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()));
|
|
}
|