17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-29 10:27:32 +01:00

refactor(editor): split the track mode into its gizmo, kept records, switch drive, switch slide and track bed files; path geometry shared in editorGeometry instead of four copies of the reversed path, track shape rules (stretching, rolls at joints, middle, switch axis, drive spot) in editor_track, fitting the track to the ground in ground_follow with the terrain query passed in, the record layer and the switch classification in the stores; long gizmo, record watch and switch slide functions broken up, out-parameters replaced by optional results

This commit is contained in:
maj00r
2026-10-11 22:03:59 +02:00
parent f34056ff5a
commit c5ac698702
21 changed files with 2614 additions and 2095 deletions

View File

@@ -178,6 +178,8 @@ set(SOURCES
"editor/editorTrack.cpp"
"editor/editorRoad.cpp"
"editor/editorAlignment.cpp"
"editor/editorTrackShape.cpp"
"editor/editorGroundFollow.cpp"
"editor/editorAlignmentStore.cpp"
"editor/editorSwitchStore.cpp"
"editor/editorSpeed.cpp"
@@ -190,6 +192,11 @@ set(SOURCES
"editor/editorIcons.cpp"
"application/editormode.cpp"
"application/editormodetrack.cpp"
"application/editormodetrackgizmo.cpp"
"application/editormoderecords.cpp"
"application/editormodeswitchdrive.cpp"
"application/editormodeswitchslide.cpp"
"application/editormodetrackbed.cpp"
"application/editormodetrackux.cpp"
"application/editormoderoad.cpp"
"application/editormodeprofile.cpp"

View File

@@ -707,8 +707,27 @@ class editor_mode : public application_mode, private editor_track::observer
void track_gizmo_control(TTrack &Track, glm::dvec3 Control);
void track_gizmo_point(TTrack &Track, glm::dvec3 const &Position);
void track_gizmo_switches(TTrack &Track);
void track_gizmo_conform(TTrack &Track) const;
bool track_gizmo_clustered(TTrack const *Track) const;
void track_gizmo_spill(TTrack &Track, std::size_t const Pieces);
void track_gizmo_carry(track_gizmo_drag::switch_joint &Link, glm::dvec3 const &Point, glm::dvec3 const &From, glm::dvec3 const &To, double const Grade);
void track_gizmo_roll_switch(track_gizmo_drag::switch_joint const &Link, double const Roll);
void track_gizmo_take_joints(TTrack &Track);
void track_gizmo_take_points(TTrack &Track);
void track_gizmo_take_partners(TTrack &Track);
void track_gizmo_take_switch_joints(TTrack &Track, bool const Controlmode);
track_gizmo_drag::switch_joint track_gizmo_link(TTrack &Track, editor_track::point_ref const &Own, std::pair<TTrack *, editor_track::point_ref> const &Connection, std::function<bool(TTrack const *)> const &Movable);
void track_gizmo_take_switches(TTrack &Track);
void track_gizmo_measure_rotation(TTrack &Track, bool const Pointmode);
void track_gizmo_measure(TTrack &Track, bool const Pointmode, gizmo_operation const Operation);
void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation);
void track_gizmo_turn_direction(TTrack &Track, double const Angle);
void track_gizmo_turn_grade(TTrack &Track, double const Angle);
void track_gizmo_turn_cant(TTrack &Track, double const Angle);
void track_gizmo_turn(TTrack &Track, int const Axis, double const Angle);
void track_gizmo_rotated(TTrack &Track);
void track_gizmo_scaled(TTrack &Track);
void track_gizmo_moved_point(TTrack &Track, glm::dvec3 const &Position);
void track_gizmo_length(TTrack &Track, double Length);
void track_gizmo_end(TTrack &Track);
bool m_track_dirty{false};
@@ -807,7 +826,7 @@ class editor_mode : public application_mode, private editor_track::observer
void route_take_stored(int const Index);
void record_switch(TTrack &Track, editor_track::switch_template const &Shape);
void record_slip(std::vector<TTrack *> const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base);
void render_switch_record(TTrack &Track);
void render_switch_record(TTrack const &Track) const;
void route_store();
bool digitize_drawn(std::vector<TTrack *> const &Created, bool const Alone = true);
bool digitize_chain(editor_track::chain const &Chain);
@@ -956,6 +975,7 @@ class editor_mode : public application_mode, private editor_track::observer
};
switch_tool m_switch;
bool switch_room(TTrack &Track, double &Low, double &High, std::string &Reason);
bool switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status);
bool move_switch_paths(TTrack &Track, std::vector<segment_data> const &Paths, std::string &Status);
bool slide_switch(TTrack &Track, double const Distance);
bool switch_slides(TTrack const &Track) const;
@@ -1406,10 +1426,16 @@ class editor_mode : public application_mode, private editor_track::observer
};
// includes of the templates whose file names the test accepts, with the location in p2..p4 and the rotation in p5
std::vector<standing_template> standing_templates(std::function<bool(std::string const &)> const &Accept) const;
bool find_switch_drive(TTrack const &Switch, standing_template &Drive, int &Family, int &Index);
struct switch_drive
{
standing_template standing;
int family{-1};
int index{0};
};
std::optional<switch_drive> find_switch_drive(TTrack const &Switch);
std::string missing_drive_variant(int const Family, bool const Rightswitch, bool const Driveright) const;
bool replace_switch_drive(TTrack &Switch, int Family, bool Flip);
void apply_switch_drive_choice(TTrack &Switch);
bool switch_drive_spot(TTrack const &Switch, glm::dvec3 &Location, double &Yaw, glm::dvec2 &Tilt, bool &Rightswitch) const;
void switch_drive_captured(TTrack const &Track);
void switch_drives_committed(std::vector<TTrack *> const &Tracks);
// places them in the active layer, as one step of the undo. returns: how many were placed

View File

@@ -0,0 +1,322 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp"
#include "editor/editorGeometry.hpp"
#include "world/Track.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
namespace
{
using geometry::bezier;
using geometry::plan_of;
using geometry::signed_angle;
using geometry::turned;
using path_ends = std::pair<glm::dvec3, glm::dvec3>;
struct record_drift
{
int followed{0};
int departed{0};
double departure{0.0};
double cant{0.0};
std::string layer;
};
glm::dvec2 chord_direction(glm::dvec3 const &Start, glm::dvec3 const &End)
{
auto const direction{plan_of(End - Start)};
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0};
}
path_ends const &ends_before(std::vector<TTrack *> const &Moving, std::vector<path_ends> const &Before, TTrack const *Track)
{
return Before[static_cast<std::size_t>(std::find(Moving.begin(), Moving.end(), Track) - Moving.begin())];
}
bool lies_on(int const Record, TTrack const &Track)
{
bezier const curve{Track.m_paths.front()};
std::array<double, 3> constexpr samples{0.0, 0.5, 1.0};
return std::ranges::all_of(samples, [&](double const T) { return alignment_store::on_record(Record, curve.point(T), alignment_store::tolerance).has_value(); });
}
bool covers_record(int const Record, std::vector<TTrack *> const &Moving)
{
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Normal && false == track->m_paths.empty() && std::find(Moving.begin(), Moving.end(), track) == Moving.end() && lies_on(Record, *track))
return false;
return true;
}
bool carried_rigidly(int const Record, std::vector<TTrack *> const &Tracks, std::vector<TTrack *> const &Moving, std::vector<path_ends> const &Before, record_drift &Drift)
{
auto const &first{ends_before(Moving, Before, Tracks.front())};
auto const &path{Tracks.front()->m_paths.front()};
auto const &start{path.points[segment_data::point::start]};
auto const from{chord_direction(first.first, first.second)};
auto const to{chord_direction(start, path.points[segment_data::point::end])};
auto const angle{signed_angle(from, to)};
auto const carried = [&](glm::dvec3 const &Old, glm::dvec3 const &Current) { return glm::distance(plan_of(start) + turned(plan_of(Old) - plan_of(first.first), angle), plan_of(Current)) <= 0.01; };
for (auto const *track : Tracks)
{
auto const &was{ends_before(Moving, Before, track)};
auto const &now{track->m_paths.front()};
if (false == carried(was.first, now.points[segment_data::point::start]) || false == carried(was.second, now.points[segment_data::point::end]))
return false;
}
if (glm::distance(first.first, start) > 1e-4 || std::abs(angle) > 1e-7)
{
alignment_store::transform(Record, first.first, from, start, to);
++Drift.followed;
}
return true;
}
void measure_departure(int const Record, std::vector<TTrack *> const &Tracks, record_drift &Drift)
{
auto const &records{alignment_store::records()};
auto const gauge{records[Record].design.norms.gauge};
for (auto const *track : Tracks)
{
if (track->m_editorremoved)
continue;
auto const &path{track->m_paths.front()};
bezier const curve{path};
double largest{0.0};
for (auto const t : {0.0, 0.5, 1.0})
{
auto const found{alignment_store::on_record(Record, curve.point(t), 1000.0)};
largest = std::max(largest, found.has_value() ? found->offset : 1000.0);
}
if (largest > alignment_store::tolerance)
{
++Drift.departed;
Drift.departure = std::max(Drift.departure, largest);
Drift.layer = scene::Layers.layer(records[Record].layer).name;
continue;
}
for (auto const end : {0, 1})
if (auto const found{alignment_store::on_record(Record, curve.point(end), alignment_store::tolerance)})
{
auto const actual{gauge * std::sin(std::abs(glm::radians(static_cast<double>(path.rolls[end]))))};
Drift.cant = std::max(Drift.cant, std::abs(actual - alignment_store::cant_at(Record, found->chainage)));
}
}
}
std::vector<std::string> drift_notes(record_drift const &Drift)
{
std::vector<std::string> notes;
if (Drift.followed > 0)
notes.push_back(STR("The kept design of the line went along with its paths"));
if (Drift.departed > 0)
notes.push_back(format(STR_C("%d path(s) left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the route design lays them by the design again or forgets it"), Drift.departed, Drift.layer.c_str(), Drift.departure));
if (Drift.cant > 5.0)
notes.push_back(format(STR_C("The cant differs from the kept design by up to %.0f mm"), Drift.cant));
return notes;
}
switch_store::record switch_entry(editor_track::switch_template const &Shape)
{
switch_store::record entry;
if (Shape.source == nullptr)
{
entry.type = Shape.type;
entry.rail = Shape.rail;
entry.radius = Shape.radius;
entry.ratio = Shape.ratio;
entry.a = Shape.a;
entry.b = Shape.b;
entry.source = "designed";
}
else if (auto const known{switch_store::of(*Shape.source)}; known.has_value() && known->kind == "switch")
{
entry = *known;
entry.number = "-";
}
else
{
entry.type = "-";
entry.radius = Shape.radius;
entry.ratio = Shape.ratio;
entry.source = "measured";
}
return entry;
}
template <std::size_t Size>
bool edit_dashed(char const *Label, std::string &Value)
{
std::array<char, Size> text{};
std::snprintf(text.data(), text.size(), "%s", Value == "-" ? "" : Value.c_str());
if (false == ImGui::InputText(Label, text.data(), text.size(), ImGuiInputTextFlags_EnterReturnsTrue) && false == ImGui::IsItemDeactivatedAfterEdit())
return false;
Value = text[0] != '\0' ? text.data() : "-";
return true;
}
} // namespace
void editor_mode::records_watch_begin()
{
auto &watch{m_record_watch};
watch = {};
watch.active = true;
watch.moving = m_track_drag;
for (auto const &link : m_track_gizmo_drag.switches)
for (auto const &member : link.cluster)
if (std::find(watch.moving.begin(), watch.moving.end(), member.first) == watch.moving.end())
watch.moving.push_back(member.first);
for (auto *track : watch.moving)
{
auto const &path{track->m_paths.front()};
watch.before.emplace_back(path.points[segment_data::point::start], path.points[segment_data::point::end]);
if (track->eType != tt_Normal)
continue;
if (auto const found{alignment_store::on_track(*track)})
watch.on[found->record].push_back(track);
}
for (auto const &[record, tracks] : watch.on)
if (covers_record(record, watch.moving))
watch.whole.insert(record);
}
// what the drag of the gizmo did to the kept alignments and switches: an alignment all of whose paths were moved or turned as a
// whole goes along with them, paths pulled out of an alignment or given another cant are told about, as are switches bent out
// of the shape of their type; switches moved as a whole take their records along
void editor_mode::records_watch_end()
{
auto &watch{m_record_watch};
if (false == watch.active)
return;
watch.active = false;
record_drift drift;
for (auto const &[record, tracks] : watch.on)
if (watch.whole.count(record) == 0 || false == carried_rigidly(record, tracks, watch.moving, watch.before, drift))
measure_departure(record, tracks, drift);
auto notes{drift_notes(drift)};
switch_store::follow();
for (auto const *track : watch.moving)
if (track->eType == tt_Switch && false == track->m_editorremoved)
if (auto const known{switch_store::named(*track)}; known.has_value() && false == switch_store::of(*track).has_value())
notes.push_back(format(STR_C("Switch %s lost the shape of its type %s: Ctrl+Z takes it back"), track->name().c_str(), switch_store::label(*known).c_str()));
records_to_history();
if (false == notes.empty())
{
std::string text;
for (auto const &note : notes)
text += (text.empty() ? "" : "\n") + note;
ui()->set_status(text, drift.departed > 0 || drift.cant > 5.0 || notes.size() > static_cast<std::size_t>(drift.followed > 0 ? 1 : 0));
WriteLog("Editor: " + text, logtype::generic);
}
watch = {};
}
void editor_mode::records_to_history()
{
auto alignments{alignment_store::take_changes()};
auto switches{switch_store::take_changes()};
if (m_history.empty() || m_history.back().action != EditorSnapshot::Action::TrackEdit)
return;
auto &last{m_history.back()};
if (alignments.has_value() && false == last.alignments.has_value())
last.alignments = std::move(alignments);
if (switches.has_value() && false == last.switch_records.has_value())
last.switch_records = std::move(switches);
}
// the switch is kept in the scenery with its type: of the catalogue template, of the switch of the scenery it was copied
// from, or measured; on a curve it is an arc switch, one-sided when it diverges to the side the curve turns to
void editor_mode::record_switch(TTrack &Track, editor_track::switch_template const &Shape)
{
if (Track.eType != tt_Switch || Track.m_paths.size() < 2 || Track.name().empty() || scene::Layers.empty())
return;
auto entry{switch_entry(Shape)};
entry.kind = "switch";
entry.name = Track.name();
entry.parts = {entry.name};
switch_store::place(entry, Track);
switch_store::classify(entry, Track);
switch_store::put(entry, alignment_store::layer_of(Track));
}
void editor_mode::record_slip(std::vector<TTrack *> const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base)
{
if (Parts.empty() || scene::Layers.empty())
return;
switch_store::record entry;
entry.kind = Kind;
entry.name = Base;
entry.type = Type;
entry.radius = Radius;
entry.ratio = Angle > 1e-9 ? 1.0 / std::tan(Angle) : 0.0;
for (auto const *part : Parts)
if (part != nullptr && false == part->name().empty() && (Kind == "crossing" || part->eType == tt_Switch))
entry.parts.push_back(part->name());
if (entry.parts.empty())
return;
auto const *first{simulation::Paths.find(entry.parts.front())};
if (first == nullptr || first->m_paths.empty())
return;
switch_store::place(entry, *first);
switch_store::put(entry, alignment_store::layer_of(*first));
}
void editor_mode::render_switch_record(TTrack const &Track) const
{
auto const known{switch_store::of(Track)};
if (false == known.has_value())
{
ImGui::TextDisabled("%s", STR_C("The scenery keeps no type for this switch, it's read from its shape"));
return;
}
auto entry{*known};
ImGui::Text(STR_C("Type kept in the scenery: %s"), switch_store::label(entry).c_str());
if (entry.kind == "switch" && entry.main_radius != 0.0)
ImGui::TextDisabled(STR_C("Diverging track R %.0f m: the R %.0f m of the type bent with the main track"), switch_store::branch_radius(entry), entry.radius);
if (entry.source == "measured")
ImGui::TextDisabled("%s", STR_C("Copied from a switch of the scenery: the size is measured from its shape"));
ImGui::PushItemWidth(100.0f);
bool changed{edit_dashed<32>(STR_C("Number##switchnumber"), entry.number)};
item_tooltip("Number of the switch in the station, as the plans of the station give it");
ImGui::SameLine();
changed |= edit_dashed<16>(STR_C("Rail##switchrail"), entry.rail);
item_tooltip("Rail of the switch: 60E1, 49E1...");
auto km{entry.km.has_value() ? *entry.km / 1000.0 : 0.0};
if (ImGui::InputDouble(STR_C("Km of the start##switchkm"), &km, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue))
{
entry.km = km * 1000.0;
changed = true;
}
item_tooltip("Chainage of the start of the switch; taken from the horizontal alignment kept in the scenery when the switch lies on one");
ImGui::PopItemWidth();
if (changed)
{
switch_store::put(entry, entry.layer);
switch_store::settle();
}
}

View File

@@ -0,0 +1,21 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include "imgui/imgui.h"
#include "utilities/translation.h"
float constexpr kDragPixels{5.0f};
inline void item_tooltip(char const *Text)
{
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C(Text));
}

View File

@@ -46,6 +46,7 @@ namespace
using geometry::bezier;
using geometry::plan_distance;
using geometry::plan_of;
using geometry::reversed;
double const kSweepRouteReach{2000.0}; // m each way through the switches
double const kSweepOutlineStep{1.0};
@@ -114,17 +115,6 @@ bool platform_distance()
return true;
}
segment_data reversed(segment_data const &Path)
{
segment_data result{Path};
result.points[segment_data::point::start] = Path.points[segment_data::point::end];
result.points[segment_data::point::end] = Path.points[segment_data::point::start];
result.points[segment_data::point::control1] = Path.points[segment_data::point::control2];
result.points[segment_data::point::control2] = Path.points[segment_data::point::control1];
result.rolls = {-Path.rolls[1], -Path.rolls[0]};
return result;
}
bool same_piece(segment_data const &A, segment_data const &B)
{
for (int i = 0; i < 4; ++i)

View File

@@ -0,0 +1,309 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorIncludeInfo.hpp"
#include "world/Track.h"
#include "scene/scenelayers.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <iterator>
#include <map>
namespace
{
std::string drive_file(std::string Text)
{
std::replace(Text.begin(), Text.end(), '\\', '/');
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char Character) { return static_cast<char>(std::tolower(Character)); });
return Text;
}
int drive_side(std::string const &Side)
{
if (Side == "left")
return 0;
return Side == "right" ? 1 : -1;
}
std::optional<std::string> pose_value(std::string const &Role, glm::dvec3 const &Local, editor_track::drive_spot const &Spot)
{
if (Role == "pos.x")
return editor_includes::number(Local.x);
if (Role == "pos.y")
return editor_includes::number(Local.y);
if (Role == "pos.z")
return editor_includes::number(Local.z);
if (Role == "rot.y")
return editor_includes::number(Spot.yaw);
if (Role == "rot.x")
return editor_includes::number(Spot.tilt.x);
if (Role == "rot.z")
return editor_includes::number(Spot.tilt.y);
return std::nullopt;
}
double angle_value(std::vector<std::string> const &Values, std::size_t const Index)
{
double value{0.0};
if (Index < Values.size())
std::sscanf(Values[Index].c_str(), "%lf", &value);
return value;
}
} // namespace
void editor_mode::scan_switch_drives()
{
auto &tool{m_switch};
if (tool.drives_scanned)
return;
tool.drives_scanned = true;
tool.drives.clear();
if (false == EditorIncludes.scanned())
EditorIncludes.scan();
for (auto const &entry : EditorIncludes.entries())
{
if (entry.category != "switch drive")
continue;
include_info info;
std::string error;
if (false == editor_includes::load(entry.file, info, error) || editor_includes::parameter_with_role(info, "track") == 0)
continue;
auto const hand{drive_side(info.switch_hand)};
auto const drive{drive_side(info.switch_drive)};
if (hand < 0 || drive < 0)
continue;
auto const name{info.name.empty() ? entry.file : info.name};
auto family{std::find_if(tool.drives.begin(), tool.drives.end(), [&](switch_tool::drive_family const &Family) { return Family.name == name; })};
if (family == tool.drives.end())
{
tool.drives.push_back({name, {}});
family = std::prev(tool.drives.end());
}
family->files[hand * 2 + drive] = entry.file;
family->heights[hand * 2 + drive] = info.switch_height;
family->preferred |= info.switch_default;
}
std::sort(tool.drives.begin(), tool.drives.end(), [](switch_tool::drive_family const &A, switch_tool::drive_family const &B) { return A.name < B.name; });
auto const preferred{std::find_if(tool.drives.begin(), tool.drives.end(), [](switch_tool::drive_family const &Family) { return Family.preferred; })};
if (tool.drives.empty())
tool.drive = -1;
else
tool.drive = preferred != tool.drives.end() ? static_cast<int>(preferred - tool.drives.begin()) : 0;
}
void editor_mode::switch_drive_captured(TTrack const &Track)
{
if (Track.eType != tt_Switch || m_infra_suspended)
return;
auto const found{find_switch_drive(Track)};
if (false == found.has_value())
return;
auto const instance{found->standing.instance};
if (std::any_of(m_drive_buffer.begin(), m_drive_buffer.end(), [&](auto const &Entry) { return Entry.first == instance; }))
return;
m_drive_buffer.emplace_back(instance, scene::Layers.directive(instance));
}
void editor_mode::switch_drives_committed(std::vector<TTrack *> const &Tracks)
{
if (m_infra_suspended)
return;
for (auto const *track : Tracks)
{
if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch)
continue;
auto const found{find_switch_drive(*track)};
auto const spot{editor_track::drive_spot_of(*track)};
if (false == found.has_value() || false == spot.has_value())
continue;
auto const &drive{found->standing};
auto const &included{scene::Layers.instance(drive.instance)};
include_info info;
std::string error;
if (included.file == nullptr || false == editor_includes::load(*included.file, info, error))
continue;
auto const local{spot->location + glm::dvec3{0.0, m_switch.drives[found->family].heights[found->index], 0.0} - included.context.offset};
auto values{drive.values};
for (auto const &parameter : info.parameters)
{
if (parameter.id < 1 || parameter.id > static_cast<int>(values.size()))
continue;
if (auto const value{pose_value(parameter.role, local, *spot)})
values[parameter.id - 1] = *value;
}
auto const directive{editor_includes::compose_directive(*included.file, values)};
if (directive != scene::Layers.directive(drive.instance))
set_include_directive(drive.instance, directive);
}
}
std::string editor_mode::missing_drive_variant(int const Family, bool const Rightswitch, bool const Driveright) const
{
return format(STR_C("The drive %s has no variant for a %s switch with the drive on the %s"), m_switch.drives[Family].name.c_str(), Rightswitch ? STR_C("right") : STR_C("left"), Driveright ? STR_C("right") : STR_C("left"));
}
bool editor_mode::place_switch_drive(TTrack &Switch)
{
auto &tool{m_switch};
scan_switch_drives();
if (tool.drive < 0 || tool.drive >= static_cast<int>(tool.drives.size()) || Switch.eType != tt_Switch || Switch.m_paths.size() < 2)
return false;
auto const spot{editor_track::drive_spot_of(Switch)};
if (false == spot.has_value())
return false;
bool const driveright{(tool.drive_side == 0) == spot->rightswitch};
auto const &family{tool.drives[tool.drive]};
auto const index{(spot->rightswitch ? 2 : 0) + (driveright ? 1 : 0)};
template_item item;
item.file = family.files[index];
if (item.file.empty())
{
tool.status = missing_drive_variant(tool.drive, spot->rightswitch, driveright);
return false;
}
item.location = spot->location + glm::dvec3{0.0, family.heights[index], 0.0};
item.yaw = spot->yaw;
item.described = true;
item.track = Switch.name();
item.tilt = spot->tilt;
if (auto const present{find_switch_drive(Switch)})
return replace_switch_drive(Switch, tool.drive, (present->index & 1) != (driveright ? 1 : 0));
std::string error;
if (place_templates({item}, error) == 0)
{
tool.status = STR_C("The drive couldn't be placed: ") + error;
return false;
}
tool.status = format(STR_C("Switch %s with the drive %s: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), item.file.c_str(), Switch.name().c_str(), Switch.name().c_str());
return true;
}
std::optional<editor_mode::switch_drive> editor_mode::find_switch_drive(TTrack const &Switch)
{
auto const &tool{m_switch};
scan_switch_drives();
std::map<std::string, std::pair<int, int>, std::less<>> variants;
for (int family = 0; family < static_cast<int>(tool.drives.size()); ++family)
for (int index = 0; index < 4; ++index)
if (false == tool.drives[family].files[index].empty())
variants[drive_file(tool.drives[family].files[index])] = {family, index};
auto const name{drive_file(Switch.name())};
auto const standing{standing_templates([&](std::string const &File) { return variants.contains(File); })};
auto const found{std::find_if(standing.begin(), standing.end(), [&](standing_template const &Standing) { return drive_file(Standing.values[0]) == name; })};
if (found == standing.end())
return std::nullopt;
auto const &[family, index]{variants[found->file]};
return switch_drive{*found, family, index};
}
void editor_mode::apply_switch_drive_choice(TTrack &Switch)
{
if (m_switch.drive >= 0)
{
place_switch_drive(Switch);
return;
}
if (auto const found{find_switch_drive(Switch)}; found.has_value() && remove_templates({found->standing.instance}) > 0)
m_switch.status = format(STR_C("Switch %s without a drive now"), Switch.name().c_str());
}
bool editor_mode::flip_switch_drive(TTrack &Switch)
{
return replace_switch_drive(Switch, -1, true);
}
bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool const Flip)
{
auto &tool{m_switch};
auto const present{find_switch_drive(Switch)};
if (false == present.has_value())
{
tool.status = format(STR_C("No drive of the switch %s among the described drives"), Switch.name().c_str());
return false;
}
auto const &found{present->standing};
auto const family{Family >= 0 ? Family : present->family};
auto const index{Flip ? present->index ^ 1 : present->index};
template_item item;
item.file = tool.drives[family].files[index];
if (item.file.empty())
{
tool.status = Flip ? format(STR_C("The drive %s has no variant with the drive on the other side"), tool.drives[family].name.c_str()) : missing_drive_variant(family, (index & 2) != 0, (index & 1) != 0);
return false;
}
if (drive_file(item.file) == found.file)
return false;
auto const spot{editor_track::drive_spot_of(Switch)};
item.location = spot.has_value() ? spot->location + glm::dvec3{0.0, tool.drives[family].heights[index], 0.0} : found.location;
item.yaw = found.yaw;
item.described = true;
item.track = Switch.name();
item.tilt = {angle_value(found.values, 5), angle_value(found.values, 6)};
if (remove_templates({found.instance}) == 0)
{
tool.status = STR_C("The drive of this switch can't be changed in its file");
return false;
}
std::string error;
if (place_templates({item}, error) == 0)
{
scene::Layers.removed(found.instance, false);
m_history.pop_back();
tool.status = STR_C("The drive couldn't be placed: ") + error;
return false;
}
join_last_steps();
tool.status = format(index & 1 ? STR_C("Switch %s: the drive %s, on the right now") : STR_C("Switch %s: the drive %s, on the left now"), Switch.name().c_str(), item.file.c_str());
return true;
}
bool editor_mode::render_switch_drive_choice()
{
auto &tool{m_switch};
scan_switch_drives();
if (tool.drives.empty())
{
ImGui::TextDisabled("%s", STR_C("No switch drives: no template is described with category: switch drive"));
return false;
}
auto const label = [&](int const Index) { return Index < 0 ? std::string{STR_C("no drive")} : tool.drives[Index].name; };
ImGui::PushItemWidth(200.0f);
bool changed{false};
if (ImGui::BeginCombo(STR_C("Drive"), label(tool.drive).c_str()))
{
for (int i = -1; i < static_cast<int>(tool.drives.size()); ++i)
if (ImGui::Selectable(label(i).c_str(), tool.drive == i))
{
tool.drive = i;
changed = true;
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
item_tooltip("Templates described with category: switch drive, the side of the switch and of the drive, and a parameter with the role track for the name of the switch.\nIts events <name>+ and <name>- switch it with the keys by the switch and in the map during the game");
if (tool.drive >= 0)
{
ImGui::SameLine();
changed |= ImGui::RadioButton(STR_C("diverging side"), &tool.drive_side, 0);
ImGui::SameLine();
changed |= ImGui::RadioButton(STR_C("other side"), &tool.drive_side, 1);
}
return changed;
}

View File

@@ -0,0 +1,348 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "application/editorprojection.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp"
#include "editor/editorGeometry.hpp"
#include "world/Track.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <optional>
#include <tuple>
namespace
{
using geometry::bezier;
using geometry::has_controls;
using geometry::plan_of;
struct switch_end_move
{
glm::dvec3 from{0.0};
glm::dvec3 to{0.0};
double shift{0.0};
};
std::string track_label(TTrack const &Track)
{
return Track.name().empty() ? std::string{"(noname)"} : Track.name();
}
std::optional<std::vector<switch_end_move>> switch_end_moves(TTrack const &Track, segment_data const &Main, bool const Online, std::pair<double, double> const &Room, double const Offset, std::string &Status)
{
auto const axis{editor_track::main_axis(Track)};
std::vector<switch_end_move> ends;
for (auto const index : {segment_data::point::start, segment_data::point::end})
{
auto const from{Track.m_paths.front().points[index]};
auto const to{Main.points[index]};
if (false == Online || glm::distance(from, to) < 0.005 || axis->lateral(to) > Offset)
continue;
auto const target{axis->along(to)};
if (target < Room.first - 0.01 || target > Room.second + 0.01)
{
Status = STR_C("The switch would run into another switch or past the end of the straight");
return std::nullopt;
}
ends.push_back({from, to, index == segment_data::point::start ? axis->along(from) - target : target - axis->along(from)});
}
std::stable_sort(ends.begin(), ends.end(), [](switch_end_move const &A, switch_end_move const &B) { return A.shift > B.shift; });
return ends;
}
std::string switch_rejoin(TTrack &Track, std::vector<editor_track::joint> const &Joints, std::vector<bool> const &Handled, editor_track::edit &Edit)
{
std::vector<editor_track::joint> following;
std::string loose;
for (std::size_t i = 0; i < Joints.size(); ++i)
{
auto const &joint{Joints[i]};
if (Handled[i] || joint.other->m_editorremoved)
continue;
auto const &theirs{joint.other->m_paths[joint.theirs.path]};
auto const shift{glm::distance(editor_track::point_position(Track, joint.own), editor_track::point_position(*joint.other, joint.theirs))};
if (shift > 0.3 * bezier{theirs}.plan_length())
{
loose += (loose.empty() ? "" : ", ") + track_label(*joint.other);
continue;
}
following.push_back(joint);
Edit.capture(*joint.other);
}
editor_track::follow(Track, following);
return loose;
}
} // namespace
bool editor_mode::switch_room(TTrack &Track, double &Low, double &High, std::string &Reason)
{
Low = 0.0;
High = 0.0;
if (Track.eType != tt_Switch || Track.m_paths.size() < 2 || has_controls(Track.m_paths.front()))
return false;
auto const axis{editor_track::main_axis(Track)};
if (false == axis.has_value())
return false;
auto const line{editor_track::find_straight(Track, m_straights.tolerance)};
if (false == tracks_editable(line.tracks, Reason))
return false;
std::vector<std::tuple<double, double, bool>> spans;
for (std::size_t i = 0; i < line.tracks.size(); ++i)
{
if (line.tracks[i] == &Track)
continue;
auto const &path{line.tracks[i]->m_paths[line.paths[i]]};
auto const a{axis->along(path.points[segment_data::point::start])};
auto const b{axis->along(path.points[segment_data::point::end])};
spans.emplace_back(std::min(a, b), std::max(a, b), line.tracks[i]->eType == tt_Normal);
}
std::sort(spans.begin(), spans.end());
double low{0.0};
double high{axis->length};
for (auto const &[from, to, plain] : spans)
if (from > axis->length - 0.01)
{
if (false == plain || from > high + 0.01)
break;
high = std::max(high, to);
}
for (auto span{spans.rbegin()}; span != spans.rend(); ++span)
{
auto const &[from, to, plain] = *span;
if (to < 0.01)
{
if (false == plain || to < low - 0.01)
break;
low = std::min(low, from);
}
}
Low = low;
High = high;
return true;
}
bool editor_mode::switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status)
{
if (Shift > 0.0)
{
auto const a{Line.along(From)};
auto const b{Line.along(To)};
if (cut_straight(Line, std::min(a, b), std::max(a, b), Edit.states, Edit.created, Edit.removed, nullptr))
return true;
Status = STR_C("The switch would overlap another switch, move it onto plain track");
return false;
}
TTrack const *style{nullptr};
bool filler{false};
for (auto const &[other, point] : editor_track::connected_points(Track, From))
{
if (other->m_editorremoved)
continue;
if (other->eType == tt_Normal && std::find(Line.tracks.begin(), Line.tracks.end(), other) != Line.tracks.end())
{
Edit.capture(*other);
editor_track::move_point(*other, point, To);
continue;
}
filler = true;
if (style == nullptr && other->eType == tt_Normal)
style = other;
}
if (filler)
{
segment_data path;
path.points[segment_data::point::start] = To;
path.points[segment_data::point::end] = From;
auto *piece{editor_track::create_path(style != nullptr ? *style : Track, path)};
Edit.created.push_back(piece);
Edit.changed.push_back(piece);
}
return true;
}
bool editor_mode::move_switch_paths(TTrack &Track, std::vector<segment_data> const &Paths, std::string &Status)
{
if (Paths.size() < 2 || Track.m_paths.size() < 2 || false == editor_track::main_axis(Track).has_value())
return false;
double low{0.0};
double high{0.0};
std::string reason;
bool const online{switch_room(Track, low, high, reason)};
if (false == online && false == reason.empty())
{
Status = reason;
return false;
}
auto const ends{switch_end_moves(Track, Paths.front(), online, {low, high}, m_straights.tolerance.offset, Status)};
if (false == ends.has_value())
return false;
auto const joints{editor_track::joints(Track)};
std::vector<glm::dvec3> positions;
for (auto const &joint : joints)
positions.push_back(editor_track::point_position(Track, joint.own));
std::vector<bool> handled(joints.size(), false);
editor_track::edit edit;
edit.capture(Track);
auto const line{online ? editor_track::find_straight(Track, m_straights.tolerance) : editor_track::straight{}};
for (auto const &end : *ends)
{
for (std::size_t i = 0; i < joints.size(); ++i)
if (glm::distance(positions[i], end.from) < geometry::same_point)
handled[i] = true;
if (false == switch_vacate(Track, end.from, end.to, end.shift, line, edit, Status))
return false;
}
Track.m_paths.assign(Paths.begin(), Paths.begin() + 2);
auto const loose{switch_rejoin(Track, joints, handled, edit)};
editor_track::commit(edit.changed);
push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed));
if (false == loose.empty())
Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose;
return true;
}
bool editor_mode::slide_switch(TTrack &Track, double const Distance)
{
if (Track.m_paths.size() < 2 || std::abs(Distance) < 0.005)
return false;
auto const paths{editor_track::slid(Track, Distance)};
if (paths.empty())
return false;
std::string status;
if (false == move_switch_paths(Track, paths, status))
{
m_switch.status = status;
ui()->set_status(status, true);
return false;
}
switch_store::follow();
records_to_history();
straight_refresh();
m_switch.status = status.empty() ? format(STR_C("Switch %s moved %.2f m along the straight"), Track.name().c_str(), Distance) : status;
ui()->set_status(m_switch.status, false == status.empty());
return true;
}
bool editor_mode::switch_slides(TTrack const &Track) const
{
return Track.eType == tt_Switch && Track.m_paths.size() == 2 && false == Track.m_editorremoved && false == has_controls(Track.m_paths.front());
}
void editor_mode::switch_slide_start(TTrack &Track)
{
auto &slide{m_switch.slide};
slide = {};
std::string reason;
if (false == switch_room(Track, slide.low, slide.high, reason))
{
if (false == reason.empty())
ui()->set_status(reason, true);
return;
}
auto const axis{editor_track::main_axis(Track)};
auto const &mouse{ImGui::GetIO().MousePos};
slide.active = true;
slide.track = &Track;
slide.pressed = {mouse.x, mouse.y};
slide.grip = axis->along(cursor_level(axis->origin.y));
auto const line{editor_track::find_straight(Track, m_straights.tolerance)};
for (std::size_t i = 0; i < line.tracks.size(); ++i)
{
if (line.tracks[i] == &Track)
continue;
auto const &path{line.tracks[i]->m_paths[line.paths[i]]};
for (auto const index : {segment_data::point::start, segment_data::point::end})
slide.joints.push_back(axis->along(path.points[index]));
}
}
void editor_mode::switch_slide_update()
{
auto &slide{m_switch.slide};
if (false == slide.active)
return;
auto const axis{slide.track != nullptr && false == slide.track->m_editorremoved && slide.track->m_paths.size() >= 2 ? editor_track::main_axis(*slide.track) : std::nullopt};
if (false == axis.has_value())
{
slide = {};
return;
}
auto const length{axis->length};
auto const distance{std::clamp(axis->along(cursor_level(axis->origin.y)) - slide.grip, slide.low, slide.high - length)};
double best{1.0};
double snapped{distance};
for (auto const joint : slide.joints)
for (auto const end : {0.0, length})
if (auto const gap{std::abs(joint - (end + distance))}; gap < best && std::abs(joint - end) > 0.01)
{
best = gap;
snapped = joint - end;
}
slide.distance = std::clamp(snapped, slide.low, slide.high - length);
}
void editor_mode::switch_slide_finish()
{
auto &slide{m_switch.slide};
if (false == slide.active)
return;
auto *track{slide.track};
auto const distance{slide.distance};
auto const &mouse{ImGui::GetIO().MousePos};
auto const dragged{glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) > kDragPixels};
slide = {};
if (track != nullptr && false == track->m_editorremoved && dragged)
slide_switch(*track, distance);
}
void editor_mode::draw_switch_slide() const
{
auto const &slide{m_switch.slide};
if (false == slide.active || slide.track == nullptr || slide.track->m_paths.size() < 2)
return;
auto const &mouse{ImGui::GetIO().MousePos};
if (glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) <= kDragPixels)
return;
auto const paths{editor_track::slid(*slide.track, slide.distance)};
if (paths.empty())
return;
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
for (auto const &path : paths)
{
bezier const curve{path};
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, overlay_color::marked, 3.0f);
previous = next;
}
}
ImVec2 at;
auto const start{paths.front().points[segment_data::point::start]};
if (false == projection.project(start, at))
return;
auto const km{alignment_store::chainage_at(start)};
auto const label{km.has_value() ? format("%+.2f m km %.3f", slide.distance, *km / 1000.0) : format("%+.2f m", slide.distance)};
auto const size{ImGui::CalcTextSize(label.c_str())};
drawlist->AddRectFilled(ImVec2(at.x + 10.0f, at.y - 8.0f), ImVec2(at.x + 18.0f + size.x, at.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f);
drawlist->AddText(ImVec2(at.x + 14.0f, at.y - 8.0f), overlay_color::highlight, label.c_str());
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "editor/editorGeometry.hpp"
#include "editor/editorGroundFollow.hpp"
#include "utilities/Globals.h"
#include "world/Track.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace
{
using geometry::plan_distance;
using geometry::plan_of;
} // namespace
glm::dvec3 editor_mode::cursor_terrain(double const Height)
{
auto point{cursor_level(Height)};
for (int i = 0; i < 5 && std::isfinite(point.x) && std::isfinite(point.z); ++i)
{
std::vector<char> found;
auto ground{ground_heights({glm::dvec3{point.x, std::max(point.y, static_cast<double>(Global.pCamera.Pos.y)), point.z}}, true, &found).front()};
if (found[0] == 0)
ground = nearby_ground(point);
if (std::isnan(ground))
return point;
auto const next{cursor_level(ground)};
auto const settled{plan_distance(next, point) < 0.01};
point = {next.x, ground, next.z};
if (settled)
break;
}
return point;
}
double editor_mode::nearby_ground(glm::dvec3 const &Point) const
{
auto const ceiling{std::max(Point.y, static_cast<double>(Global.pCamera.Pos.y))};
for (double const radius : {10.0, 25.0, 50.0, 100.0, 200.0, 400.0})
{
std::vector<glm::dvec3> probes;
for (int i = 0; i < 12; ++i)
{
auto const angle{glm::two_pi<double>() * i / 12.0};
probes.push_back({Point.x + radius * std::cos(angle), ceiling, Point.z + radius * std::sin(angle)});
}
std::vector<char> found;
auto const heights{ground_heights(probes, true, &found)};
double sum{0.0};
int count{0};
for (std::size_t i = 0; i < probes.size(); ++i)
if (found[i] != 0)
{
sum += heights[i];
++count;
}
if (count > 0)
return sum / count;
}
return std::numeric_limits<double>::quiet_NaN();
}
std::pair<double, double> editor_mode::trackbed(TTrack const *Style)
{
return Style != nullptr ? profile_ballast(*Style) : std::pair<double, double>{0.2, 0.5 * 1.435 + 0.5 + 1.1};
}
std::vector<double> editor_mode::bed_heights(std::vector<glm::dvec3> const &Points, std::vector<glm::dvec2> const &Directions, TTrack const *Style) const
{
auto const [depth, half]{trackbed(Style)};
std::vector<glm::dvec3> probes;
probes.reserve(Points.size() * 3);
for (std::size_t i = 0; i < Points.size(); ++i)
{
auto const &direction{Directions[i]};
auto const side{glm::length(direction) > 1e-6 ? glm::normalize(glm::dvec2{-direction.y, direction.x}) * half : glm::dvec2{0.0}};
probes.push_back(Points[i]);
probes.push_back(Points[i] + glm::dvec3{side.x, 0.0, side.y});
probes.push_back(Points[i] - glm::dvec3{side.x, 0.0, side.y});
}
std::vector<char> found;
auto const heights{ground_heights(probes, true, &found)};
std::vector<double> result(Points.size(), std::numeric_limits<double>::quiet_NaN());
for (std::size_t i = 0; i < Points.size(); ++i)
{
auto const axis{found[3 * i] != 0 ? heights[3 * i] : std::numeric_limits<double>::quiet_NaN()};
auto toe{std::numeric_limits<double>::infinity()};
for (std::size_t j = 3 * i + 1; j < 3 * i + 3; ++j)
if (found[j] != 0)
toe = std::min(toe, heights[j]);
if (std::isfinite(toe))
result[i] = std::isnan(axis) ? toe + depth : std::max(toe + depth, axis);
else if (false == std::isnan(axis))
result[i] = axis + depth;
}
return result;
}
double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) const
{
auto const height{bed_heights({Point}, {Direction}, Style).front()};
if (false == std::isnan(height))
return height;
auto const nearby{nearby_ground(Point)};
return std::isnan(nearby) ? Point.y : nearby + trackbed(Style).first;
}
void editor_mode::follow_ground(std::vector<segment_data> &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const
{
ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [&](std::vector<glm::dvec3> const &Points, std::vector<glm::dvec2> const &Directions) { return bed_heights(Points, Directions, Style); });
}
glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point)
{
auto const &tool{m_lay};
TTrack const *style{tool.start.track != nullptr ? tool.start.track : tool.copy_selected ? selected_track() : nullptr};
return {Point.x, bed_height(Point, tool.points.empty() ? glm::dvec2{0.0} : plan_of(Point - tool.points.back()), style), Point.z};
}

View File

@@ -0,0 +1,729 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "application/editorprojection.h"
#include "editor/editorGeometry.hpp"
#include "utilities/Globals.h"
#include "rendering/renderer.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include "imgui/ImGuizmo.h"
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <array>
#include <cmath>
namespace
{
using geometry::bezier;
using geometry::level;
using geometry::plan_of;
using geometry::turned;
bool mouse_along(glm::dvec3 const &Origin, glm::dvec3 const &Axis, double &Along)
{
screen_projection const projection;
glm::dvec3 origin;
glm::dvec3 direction;
projection.ray(ImGui::GetIO().MousePos, origin, direction);
auto const b{glm::dot(Axis, direction)};
auto const denominator{1.0 - b * b};
if (denominator < 1e-6)
return false;
auto const w{Origin - origin};
Along = (b * glm::dot(direction, w) - glm::dot(Axis, w)) / denominator;
return true;
}
glm::mat4 gizmo_along(glm::dvec3 const &Position, glm::dvec3 const &Axis)
{
auto gizmo{glm::translate(glm::mat4(1.0f), glm::vec3(Position))};
if (glm::length(Axis) < 1e-9)
return gizmo;
auto const z{glm::normalize(Axis)};
auto x{glm::cross(glm::dvec3{0.0, 1.0, 0.0}, z)};
x = glm::length(x) > 1e-6 ? glm::normalize(x) : glm::dvec3{1.0, 0.0, 0.0};
auto const y{glm::cross(z, x)};
gizmo[0] = glm::vec4(glm::vec3(x), 0.0f);
gizmo[1] = glm::vec4(glm::vec3(y), 0.0f);
gizmo[2] = glm::vec4(glm::vec3(z), 0.0f);
return gizmo;
}
void add_once(std::vector<TTrack *> &Tracks, TTrack *Track)
{
if (std::find(Tracks.begin(), Tracks.end(), Track) == Tracks.end())
Tracks.emplace_back(Track);
}
bool editable(TTrack const &Track)
{
std::string reason;
return editor_track::can_edit_geometry(Track, reason);
}
double snapped_grade(double const Grade)
{
return std::round(Grade * 1000.0) / 1000.0;
}
double snapped_cant(double const Roll0, double const Roll)
{
auto const cant{std::round(1500.0 * std::sin(glm::radians(Roll0 + Roll)) / 5.0) * 5.0 / 1500.0};
return glm::degrees(std::asin(std::clamp(cant, -1.0, 1.0))) - Roll0;
}
} // namespace
editor_mode::gizmo_operation editor_mode::track_gizmo_operation(TTrack const &Track, bool const Pointmode) const
{
if (m_gizmo_op == gizmo_operation::translate || (Pointmode && false == editor_track::is_end(m_track_point.kind)))
return m_gizmo_op;
if (m_gizmo_op == gizmo_operation::rotate)
return gizmo_operation::rotate;
return editor_track::stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}).has_value() ? gizmo_operation::scale : gizmo_operation::translate;
}
glm::mat4 editor_mode::track_gizmo_frame(TTrack const &Track, bool const Pointmode, gizmo_operation const Operation) const
{
auto anchor{Pointmode ? editor_track::point_position(Track, m_track_point) : editor_track::pivot(Track)};
bool const controlmode{Pointmode && false == editor_track::is_end(m_track_point.kind)};
glm::dvec3 axis{0.0};
if (Operation == gizmo_operation::scale)
{
if (controlmode)
axis = editor_track::control_vector(Track, m_track_point);
else if (auto const end{editor_track::stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{})})
std::tie(anchor, axis) = editor_track::end_axis(Track.m_paths[end->path], end->atend);
}
else if (Operation == gizmo_operation::rotate)
{
if (controlmode)
axis = level(editor_track::control_vector(Track, m_track_point));
else if (Pointmode)
axis = level(bezier{Track.m_paths[m_track_point.path]}.first(m_track_point.kind == editor_track::point_kind::start ? 0.0 : 1.0));
else
{
auto const middle{editor_track::middle(Track)};
anchor = middle.point;
axis = level(middle.tangent);
}
}
return gizmo_along(anchor - glm::dvec3{GfxRenderer->Camera_Position()}, axis);
}
void editor_mode::track_gizmo_hold()
{
auto &drag{m_track_gizmo_drag};
drag.held.clear();
for (auto *track : m_track_drag)
{
if (drag.operation == gizmo_operation::scale && false == drag.control && track == selected_track())
continue;
auto held{editor_track::hold_controls(*track)};
if (false == held.empty())
drag.held.emplace_back(track, std::move(held));
}
}
void editor_mode::track_gizmo_settle()
{
for (auto &[track, held] : m_track_gizmo_drag.held)
editor_track::settle_controls(*track, held);
m_track_gizmo_drag.held.clear();
}
void editor_mode::track_gizmo_conform(TTrack &Track) const
{
if (Track.eType != tt_Switch)
return;
editor_track::conform_switch(Track);
editor_track::follow(Track, m_track_joints);
editor_track::follow_grade(Track, m_track_joints);
}
bool editor_mode::track_gizmo_clustered(TTrack const *Track) const
{
return std::any_of(m_track_gizmo_drag.switches.begin(), m_track_gizmo_drag.switches.end(), [&](auto const &Link) { return std::any_of(Link.cluster.begin(), Link.cluster.end(), [&](auto const &Member) { return Member.first == Track; }); });
}
void editor_mode::track_gizmo_stretch(TTrack &Track, double const Length)
{
auto &drag{m_track_gizmo_drag};
auto const pieces{editor_track::stretched(drag.paths[drag.path], drag.atend, Length)};
if (pieces.empty())
return;
auto paths{drag.paths};
paths.erase(paths.begin() + drag.path);
paths.insert(paths.begin() + drag.path, pieces.begin(), pieces.end());
Track.m_paths = std::move(paths);
drag.pieces = pieces.size();
drag.stretch = Length;
if (drag.point.valid())
m_track_point = editor_track::stretch_remap(drag.point, drag.path, drag.pieces);
auto joints{m_track_joints};
for (auto &joint : joints)
joint.own = editor_track::stretch_remap(joint.own, drag.path, drag.pieces);
editor_track::follow(Track, joints);
track_gizmo_spill(Track, pieces.size());
m_track_dirty = true;
}
void editor_mode::track_gizmo_spill(TTrack &Track, std::size_t const Pieces)
{
auto &drag{m_track_gizmo_drag};
std::vector<segment_data> rest;
if (Track.eType == tt_Normal && Pieces > 1)
{
auto const first{Track.m_paths.begin() + drag.path};
auto const keep{drag.atend ? first : first + (Pieces - 1)};
for (auto piece = first; piece != first + Pieces; ++piece)
if (piece != keep)
rest.push_back(*piece);
auto const kept{*keep};
Track.m_paths.erase(first, first + Pieces);
Track.m_paths.insert(Track.m_paths.begin() + drag.path, kept);
if (drag.point.valid())
m_track_point = drag.point;
}
while (drag.extra.size() > rest.size())
{
auto *surplus{drag.extra.back()};
drag.extra.pop_back();
std::erase(m_track_drag, surplus);
editor_track::retire(*surplus);
}
for (std::size_t i = 0; i < rest.size(); ++i)
{
if (i < drag.extra.size())
{
drag.extra[i]->m_paths = {rest[i]};
continue;
}
auto *created{editor_track::create_path(Track, rest[i])};
drag.extra.push_back(created);
m_track_drag.push_back(created);
editor_track::commit({created});
}
}
void editor_mode::track_gizmo_control(TTrack &Track, glm::dvec3 Control)
{
auto const &drag{m_track_gizmo_drag};
if (drag.fixed != glm::dvec3{})
Control = drag.fixed * std::max(0.5, glm::dot(Control, drag.fixed));
if (glm::length(Control) < 0.5)
Control = glm::length(Control) > 1e-9 ? glm::normalize(Control) * 0.5 : drag.axis * 0.5;
editor_track::move_point(Track, drag.point, editor_track::point_position(Track, editor_track::end_of(drag.point)) + Control);
editor_track::align_controls(Track, drag.point, drag.partners);
track_gizmo_conform(Track);
m_track_dirty = true;
}
void editor_mode::track_gizmo_point(TTrack &Track, glm::dvec3 const &Position)
{
editor_track::move_point(Track, m_track_point, Position);
track_gizmo_conform(Track);
auto const moved{editor_track::point_position(Track, m_track_point)};
for (auto const &[track, point] : m_track_drag_points)
if (false == track_gizmo_clustered(track))
editor_track::move_point(*track, point, moved);
}
void editor_mode::track_gizmo_carry(track_gizmo_drag::switch_joint &Link, glm::dvec3 const &Point, glm::dvec3 const &From, glm::dvec3 const &To, double const Grade)
{
auto const c{glm::dot(From, To)};
auto const s{From.x * To.z - From.z * To.x};
auto const turn = [&](glm::dvec3 const &Vector) { return glm::dvec3{Vector.x * c - Vector.z * s, Vector.y, Vector.z * c + Vector.x * s}; };
auto const tilt = [&](glm::dvec3 const &Vector) { return (Vector.x * To.x + Vector.z * To.z) * Grade; };
for (auto &[member, paths] : Link.cluster)
{
member->m_paths = paths;
for (auto &path : member->m_paths)
{
for (auto const index : {segment_data::point::start, segment_data::point::end})
{
auto const moved{turn(path.points[index] - Link.point)};
path.points[index] = Point + moved + glm::dvec3{0.0, tilt(moved), 0.0};
}
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
{
auto &control{path.points[index]};
if (control == glm::dvec3{})
continue;
control = turn(control);
control.y += tilt(control);
}
}
}
}
void editor_mode::track_gizmo_roll_switch(track_gizmo_drag::switch_joint const &Link, double const Roll)
{
auto &main{Link.track->m_paths[0]};
bool const start{Link.theirs.kind == editor_track::point_kind::start};
auto value{start ? -Roll : Roll};
if (Link.theirs.path != 0)
{
auto const t{start ? 0.0 : 1.0};
if (glm::dot(plan_of(bezier{Link.track->m_paths[Link.theirs.path]}.first(t)), plan_of(bezier{main}.first(t))) < 0.0)
value = -value;
}
main.rolls = {static_cast<float>(value), static_cast<float>(value)};
editor_track::conform_switch(*Link.track);
}
void editor_mode::track_gizmo_switches(TTrack &Track)
{
for (auto &link : m_track_gizmo_drag.switches)
{
if (link.own.path >= static_cast<int>(Track.m_paths.size()))
continue;
auto const point{editor_track::point_position(Track, link.own)};
auto const tangent{editor_track::outward(Track, link.own)};
auto const roll{editor_track::joint_roll(Track, link.own)};
auto const from{level(link.tangent)};
auto const to{level(tangent)};
auto const grade{geometry::grade_of(tangent) - geometry::grade_of(link.tangent)};
bool const changed{glm::distance(point, link.point) > 1e-9 || glm::distance(from, to) > 1e-12 || std::abs(grade) > 1e-12 || std::abs(roll - link.roll) > 1e-6};
if (false == changed && false == link.moved)
continue;
link.moved = true;
track_gizmo_carry(link, point, from, to, grade);
if (std::abs(roll - link.roll) > 1e-6)
track_gizmo_roll_switch(link, roll);
for (auto const &[member, joints] : link.joints)
{
editor_track::follow(*member, joints);
editor_track::follow_grade(*member, joints);
editor_track::match_rolls(*member, joints);
}
}
}
void editor_mode::track_gizmo_take_joints(TTrack &Track)
{
m_track_joints = editor_track::joints(Track);
for (auto const &joint : m_track_joints)
add_once(m_track_drag, joint.other);
}
void editor_mode::track_gizmo_take_points(TTrack &Track)
{
for (auto const &connection : editor_track::connected_points(Track, editor_track::point_position(Track, m_track_point)))
{
if (false == editable(*connection.first))
continue;
m_track_drag_points.emplace_back(connection);
add_once(m_track_drag, connection.first);
}
}
void editor_mode::track_gizmo_take_partners(TTrack &Track)
{
auto &drag{m_track_gizmo_drag};
auto const end{editor_track::end_of(m_track_point)};
for (auto const &partner : editor_track::partners(Track, end))
{
auto const &[other, theirs]{partner};
bool const own{other == &Track};
bool const fixed{own ? m_track_point.path != 0 : other->eType != tt_Normal || false == editable(*other)};
if (fixed)
{
auto const &path{other->m_paths[theirs.path]};
bool const start{theirs.kind == editor_track::point_kind::start};
auto const control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
auto const chord{path.points[start ? segment_data::point::end : segment_data::point::start] - path.points[start ? segment_data::point::start : segment_data::point::end]};
auto const along{control != glm::dvec3{} ? control : chord};
if (glm::length(along) > 1e-9)
drag.fixed = (own && theirs.kind == end.kind ? 1.0 : -1.0) * glm::normalize(along);
continue;
}
if (false == m_track_drag_connected && false == own)
continue;
drag.partners.push_back(partner);
add_once(m_track_drag, other);
}
}
void editor_mode::track_gizmo_take_switch_joints(TTrack &Track, bool const Controlmode)
{
auto const edited{editor_track::point_position(Track, editor_track::end_of(m_track_point))};
for (auto const &joint : editor_track::joints(Track))
{
if (Controlmode && glm::distance(editor_track::point_position(Track, joint.own), edited) < geometry::same_point)
continue;
m_track_joints.push_back(joint);
add_once(m_track_drag, joint.other);
}
}
editor_mode::track_gizmo_drag::switch_joint editor_mode::track_gizmo_link(TTrack &Track, editor_track::point_ref const &Own, std::pair<TTrack *, editor_track::point_ref> const &Connection, std::function<bool(TTrack const *)> const &Movable)
{
track_gizmo_drag::switch_joint link{Connection.first, Own, Connection.second, editor_track::point_position(Track, Own), editor_track::outward(Track, Own), editor_track::joint_roll(Track, Own)};
std::vector<TTrack *> members{Connection.first};
for (std::size_t k = 0; k < members.size(); ++k)
for (auto *next : editor_track::neighbours(*members[k]))
if (Movable(next) && std::find(members.begin(), members.end(), next) == members.end())
members.push_back(next);
for (auto *member : members)
{
link.cluster.emplace_back(member, std::vector<segment_data>{});
auto joints{editor_track::joints(*member)};
std::erase_if(joints, [&](editor_track::joint const &Joint) { return Joint.other == &Track; });
add_once(m_track_drag, member);
for (auto const &joint : joints)
add_once(m_track_drag, joint.other);
link.joints.emplace_back(member, std::move(joints));
}
return link;
}
void editor_mode::track_gizmo_take_switches(TTrack &Track)
{
auto &drag{m_track_gizmo_drag};
auto const movable = [&](TTrack const *Other) { return Other != &Track && Other->eType == tt_Switch && Other->m_paths.size() == 2 && editable(*Other) && false == track_gizmo_clustered(Other); };
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end})
{
editor_track::point_ref const own{i, kind};
for (auto const &connection : editor_track::connected_points(Track, editor_track::point_position(Track, own)))
if (movable(connection.first))
drag.switches.push_back(track_gizmo_link(Track, own, connection, movable));
}
}
void editor_mode::track_gizmo_measure_rotation(TTrack &Track, bool const Pointmode)
{
auto &drag{m_track_gizmo_drag};
auto const frame{track_gizmo_frame(Track, Pointmode, gizmo_operation::rotate)};
drag.basis = glm::dmat3(glm::mat3(frame));
drag.pivot = glm::dvec3{GfxRenderer->Camera_Position()} + glm::dvec3(frame[3]);
double along{0.0};
for (auto const &path : Track.m_paths)
{
drag.chainages.push_back(along);
drag.lengths.push_back(bezier{path}.plan_length());
along += drag.lengths.back();
}
auto const end{editor_track::switch_main(Track, editor_track::end_of(drag.point))};
if (Track.eType == tt_Switch && false == Track.m_paths.empty())
{
drag.pivot_chainage = editor_track::switch_station(Track, drag.pivot);
drag.roll0 = end.valid() && end.kind == editor_track::point_kind::end ? Track.m_paths[0].rolls[1] : Track.m_paths[0].rolls[0];
}
else if (end.valid() && end.path < static_cast<int>(drag.chainages.size()))
{
drag.pivot_chainage = drag.chainages[end.path] + (end.kind == editor_track::point_kind::end ? drag.lengths[end.path] : 0.0);
drag.roll0 = end.kind == editor_track::point_kind::start ? Track.m_paths[end.path].rolls[0] : Track.m_paths[end.path].rolls[1];
}
else
{
drag.pivot_chainage = along * 0.5;
drag.roll0 = Track.m_paths.empty() ? 0.0 : Track.m_paths.front().rolls[0];
}
}
void editor_mode::track_gizmo_measure(TTrack &Track, bool const Pointmode, gizmo_operation const Operation)
{
auto &drag{m_track_gizmo_drag};
if (drag.control)
{
drag.origin = editor_track::point_position(Track, m_track_point);
auto const control{editor_track::control_vector(Track, m_track_point)};
drag.length = glm::length(control);
drag.axis = drag.length > 1e-9 ? control / drag.length : glm::dvec3{0.0, 0.0, 1.0};
}
else if (auto const end{Operation == gizmo_operation::scale ? editor_track::stretch_end(Track, drag.point) : std::nullopt})
{
drag.path = end->path;
drag.atend = end->atend;
drag.paths = Track.m_paths;
std::tie(drag.origin, drag.axis) = editor_track::end_axis(Track.m_paths[drag.path], drag.atend);
drag.length = bezier{Track.m_paths[drag.path]}.plan_length();
}
drag.stretch = drag.length;
if (Operation == gizmo_operation::rotate)
track_gizmo_measure_rotation(Track, Pointmode);
}
void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation)
{
bool const controlmode{Pointmode && false == editor_track::is_end(m_track_point.kind)};
auto &drag{m_track_gizmo_drag};
m_track_gizmo_using = true;
m_track_pivot = editor_track::pivot(Track);
m_track_drag = {&Track};
m_track_drag_points.clear();
m_track_joints.clear();
m_track_snap = {};
drag = {};
drag.operation = Operation;
drag.control = controlmode;
drag.point = Pointmode ? m_track_point : editor_track::point_ref{};
bool const whole{false == Pointmode || (false == controlmode && Operation != gizmo_operation::translate)};
if (whole && m_track_drag_connected)
track_gizmo_take_joints(Track);
if (Pointmode && false == controlmode && Operation == gizmo_operation::translate && m_track_drag_connected)
track_gizmo_take_points(Track);
if (controlmode)
track_gizmo_take_partners(Track);
if (false == whole && Track.eType == tt_Switch && m_track_drag_connected)
track_gizmo_take_switch_joints(Track, controlmode);
if (m_track_drag_connected)
track_gizmo_take_switches(Track);
std::vector<std::pair<TTrack *, editor_track::state>> states;
for (auto *dragged : m_track_drag)
states.emplace_back(dragged, editor_track::capture(*dragged));
push_track_snapshot(std::move(states));
records_watch_begin();
track_gizmo_hold();
for (auto &link : drag.switches)
for (auto &[member, paths] : link.cluster)
paths = member->m_paths;
track_gizmo_measure(Track, Pointmode, Operation);
}
void editor_mode::track_gizmo_turn_direction(TTrack &Track, double const Angle)
{
auto &drag{m_track_gizmo_drag};
auto target{Angle};
if (Global.ctrlState)
target = glm::radians(std::round(glm::degrees(target) / 5.0) * 5.0);
auto const step{std::remainder(target - drag.angle, 2.0 * glm::pi<double>())};
if (std::abs(step) < 1e-9)
return;
drag.angle += step;
if (drag.control)
{
auto const control{editor_track::control_vector(Track, drag.point)};
auto const plan{turned(plan_of(control), -step)};
track_gizmo_control(Track, {plan.x, control.y, plan.y});
return;
}
editor_track::rotate_y(Track, drag.pivot, step);
editor_track::follow(Track, m_track_joints);
m_track_dirty = true;
}
void editor_mode::track_gizmo_turn_grade(TTrack &Track, double const Angle)
{
auto &drag{m_track_gizmo_drag};
auto target{-std::tan(std::clamp(Angle, -1.4, 1.4))};
if (Global.ctrlState)
target = snapped_grade(target);
auto const step{target - drag.grade};
if (std::abs(step) < 1e-9)
return;
drag.grade = target;
if (drag.control)
{
auto const control{editor_track::control_vector(Track, drag.point)};
auto const run{glm::length(plan_of(control))};
auto const along{glm::dot(level(control), drag.basis[2])};
track_gizmo_control(Track, {control.x, control.y + step * run * (along >= 0.0 ? 1.0 : -1.0), control.z});
return;
}
auto const count{editor_track::driven_paths(Track)};
for (std::size_t i = 0; i < count && i < drag.chainages.size(); ++i)
{
auto &path{Track.m_paths[i]};
auto const from{drag.chainages[i] - drag.pivot_chainage};
auto const to{from + drag.lengths[i]};
path.points[segment_data::point::start].y += step * from;
path.points[segment_data::point::end].y += step * to;
auto &control1{path.points[segment_data::point::control1]};
auto &control2{path.points[segment_data::point::control2]};
control1.y += step * glm::length(plan_of(control1));
control2.y -= step * glm::length(plan_of(control2));
}
editor_track::conform_switch(Track);
editor_track::follow(Track, m_track_joints);
editor_track::follow_grade(Track, m_track_joints);
m_track_dirty = true;
}
void editor_mode::track_gizmo_turn_cant(TTrack &Track, double const Angle)
{
auto &drag{m_track_gizmo_drag};
auto const end{editor_track::end_of(drag.point)};
auto const canted{editor_track::switch_main(Track, end)};
auto target{glm::degrees(drag.point.kind == editor_track::point_kind::control2 ? -Angle : Angle)};
if (Global.ctrlState)
target = snapped_cant(drag.roll0, target);
auto const step{static_cast<float>(target - drag.roll)};
if (std::abs(step) < 1e-6f)
return;
drag.roll = target;
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
{
auto &path{Track.m_paths[i]};
if (false == canted.valid() || (i == canted.path && canted.kind == editor_track::point_kind::start))
path.rolls[0] += step;
if (false == canted.valid() || (i == canted.path && canted.kind == editor_track::point_kind::end))
path.rolls[1] += step;
}
track_gizmo_conform(Track);
editor_track::match_rolls(Track, m_track_joints);
for (auto const &[other, theirs] : drag.partners)
if (other != &Track)
editor_track::roll_joined(*other, theirs, static_cast<float>(editor_track::joint_roll(Track, end)));
m_track_dirty = true;
}
void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const Angle)
{
auto const &drag{m_track_gizmo_drag};
if (drag.control && drag.fixed != glm::dvec3{})
return;
if (Axis == 1)
track_gizmo_turn_direction(Track, Angle);
else if (Axis == 0)
track_gizmo_turn_grade(Track, Angle);
else
track_gizmo_turn_cant(Track, Angle);
}
void editor_mode::track_gizmo_length(TTrack &Track, double Length)
{
auto &drag{m_track_gizmo_drag};
Length = std::max(drag.control ? 0.5 : 1.0, Length);
if (std::abs(Length - drag.stretch) < 1e-6)
return;
if (drag.control)
{
drag.stretch = Length;
track_gizmo_control(Track, drag.axis * Length);
}
else if (drag.path >= 0)
track_gizmo_stretch(Track, Length);
}
void editor_mode::track_gizmo_end(TTrack &Track)
{
if (m_track_snap.track != nullptr && m_track_gizmo_drag.operation == gizmo_operation::translate && m_track_point.valid())
editor_track::snap_point(Track, m_track_point, m_track_snap, m_track_align_tangent);
track_gizmo_settle();
m_track_dirty = true;
m_track_snap = {};
commit_track_drag(true);
if (false == m_track_gizmo_drag.extra.empty() && false == m_history.empty())
{
auto &created{m_history.back().created};
created.insert(created.end(), m_track_gizmo_drag.extra.begin(), m_track_gizmo_drag.extra.end());
m_track_gizmo_drag.extra.clear();
}
records_watch_end();
m_track_gizmo_using = false;
m_track_drag.clear();
m_track_drag_points.clear();
m_track_joints.clear();
}
void editor_mode::track_gizmo_rotated(TTrack &Track)
{
auto &drag{m_track_gizmo_drag};
auto const turn{glm::transpose(drag.basis) * glm::dmat3(glm::mat3(m_track_gizmo))};
std::array<double, 3> const angles{std::atan2(turn[1][2], turn[1][1]), std::atan2(turn[2][0], turn[0][0]), std::atan2(turn[0][1], turn[0][0])};
if (drag.ring < 0)
{
auto const largest{std::max_element(angles.begin(), angles.end(), [](double const A, double const B) { return std::abs(A) < std::abs(B); })};
if (std::abs(*largest) > 1e-5)
drag.ring = static_cast<int>(largest - angles.begin());
}
if (drag.ring >= 0)
track_gizmo_turn(Track, drag.ring, angles[drag.ring]);
}
void editor_mode::track_gizmo_scaled(TTrack &Track)
{
auto const &drag{m_track_gizmo_drag};
double along{0.0};
if (false == mouse_along(drag.origin, drag.axis, along))
return;
auto length{drag.length + along - drag.grabbed};
if (Global.ctrlState && m_gizmo_snap > 0.0f)
length = std::round(length / m_gizmo_snap) * m_gizmo_snap;
track_gizmo_length(Track, length);
}
void editor_mode::track_gizmo_moved_point(TTrack &Track, glm::dvec3 const &Position)
{
track_gizmo_point(Track, Position);
m_track_dirty = true;
if (false == editor_track::is_end(m_track_point.kind) || false == m_track_drag_points.empty())
return;
std::vector<TTrack const *> const exclude(m_track_drag.begin(), m_track_drag.end());
screen_projection const projection;
ImVec2 screen;
auto const position{editor_track::point_position(Track, m_track_point)};
m_track_snap = projection.project(position, screen) ? snap_free_end(position, {screen.x, screen.y}, &Track, Track.iCategoryFlag & 15, exclude) : editor_track::snap_target{};
}
void editor_mode::render_track_gizmo()
{
auto *track = selected_track();
if (track == nullptr || false == editable(*track))
{
m_track_gizmo_using = false;
return;
}
bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast<int>(track->m_paths.size());
bool const controlmode = pointmode && false == editor_track::is_end(m_track_point.kind);
gizmo_frame const frame;
auto &drag{m_track_gizmo_drag};
auto const operation{m_track_gizmo_using ? drag.operation : track_gizmo_operation(*track, pointmode)};
if (false == m_track_gizmo_using)
m_track_gizmo = track_gizmo_frame(*track, pointmode, operation);
glm::mat4 delta(1.0f);
auto gizmo{operation == gizmo_operation::scale ? ImGuizmo::SCALE_Z : ImGuizmo::TRANSLATE};
if (operation == gizmo_operation::rotate)
gizmo = ImGuizmo::ROTATE_X | ImGuizmo::ROTATE_Y | ImGuizmo::ROTATE_Z;
ImGuizmo::AllowAxisFlip(operation != gizmo_operation::scale);
frame.manipulate(gizmo, m_track_gizmo, operation == gizmo_operation::translate ? m_gizmo_snap : 0.0f, &delta, operation == gizmo_operation::translate ? ImGuizmo::WORLD : ImGuizmo::LOCAL);
ImGuizmo::AllowAxisFlip(true);
if (false == ImGuizmo::IsUsing())
{
if (m_track_gizmo_using)
track_gizmo_end(*track);
return;
}
if (false == m_track_gizmo_using)
{
track_gizmo_begin(*track, pointmode, operation);
if (operation == gizmo_operation::scale && false == mouse_along(drag.origin, drag.axis, drag.grabbed))
drag.grabbed = 0.0;
}
if (operation == gizmo_operation::rotate)
track_gizmo_rotated(*track);
else if (operation == gizmo_operation::scale)
track_gizmo_scaled(*track);
else if (controlmode)
track_gizmo_control(*track, frame.camera + glm::dvec3(m_track_gizmo[3]) - editor_track::point_position(*track, editor_track::end_of(m_track_point)));
else if (pointmode)
track_gizmo_moved_point(*track, frame.camera + glm::dvec3(m_track_gizmo[3]));
else if (glm::dvec3 const offset{delta[3]}; glm::length(offset) > 1e-9)
{
editor_track::translate(*track, offset);
editor_track::follow(*track, m_track_joints);
m_track_dirty = true;
}
track_gizmo_switches(*track);
commit_track_drag(false);
}

View File

@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editormodeshared.h"
#include "application/editorprojection.h"
#include "application/editoruilayer.h"
#include "editor/editorFormat.hpp"
@@ -44,7 +45,6 @@ using geometry::signed_angle;
double constexpr kRimTolerance{0.05};
double constexpr kMinimumRadius{80.0};
float constexpr kDragPixels{5.0f};
glm::dvec2 heading_vector(double const Heading)
{

View File

@@ -745,4 +745,15 @@ void restore(std::vector<record> Records)
settle();
}
scene::layer_handle layer_of(TTrack const &Track)
{
auto const usable = [](scene::layer_handle const Layer) { return scene::Layers.valid(Layer) && scene::Layers.accepts(Layer) && false == scene::Layers.layer(Layer).name.ends_with(".txtf"); };
auto layer{scene::Layers.resolve(Track.layer())};
if (false == usable(layer))
layer = scene::Layers.active();
if (false == usable(layer))
layer = 1;
return layer;
}
} // namespace alignment_store

View File

@@ -101,5 +101,8 @@ std::optional<std::vector<record>> take_changes();
void settle();
// puts the records back as they were, for undo and redo
void restore(std::vector<record> Records);
// layer the editor keeps the records of the track in: its own, unless it's a terrain file (.txtf), whose binary version
// (.btf) the editor doesn't read the records from
scene::layer_handle layer_of(TTrack const &Track);
} // namespace alignment_store

View File

@@ -159,4 +159,72 @@ inline double chord_length(segment_data const &Path)
return glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start]));
}
inline bool has_controls(segment_data const &Path)
{
return Path.points[segment_data::point::control1] != glm::dvec3{} || Path.points[segment_data::point::control2] != glm::dvec3{};
}
inline segment_data reversed(segment_data const &Path)
{
segment_data result{Path};
result.points[segment_data::point::start] = Path.points[segment_data::point::end];
result.points[segment_data::point::end] = Path.points[segment_data::point::start];
result.points[segment_data::point::control1] = Path.points[segment_data::point::control2];
result.points[segment_data::point::control2] = Path.points[segment_data::point::control1];
result.rolls = {-Path.rolls[1], -Path.rolls[0]};
return result;
}
inline std::vector<segment_data> straight_piece(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Length)
{
if (Length < 0.5)
return {};
segment_data path;
path.points[segment_data::point::start] = Point;
path.points[segment_data::point::end] = {Point.x + Direction.x * Length, Point.y + Grade * Length, Point.z + Direction.y * Length};
return {path};
}
inline segment_data part_of(segment_data const &Piece, double const From, double const To)
{
bezier const curve{Piece};
segment_data result{Piece};
result.points[segment_data::point::start] = curve.point(From);
result.points[segment_data::point::end] = curve.point(To);
if (has_controls(Piece))
{
result.points[segment_data::point::control1] = curve.first(From) * ((To - From) / 3.0);
result.points[segment_data::point::control2] = -curve.first(To) * ((To - From) / 3.0);
}
result.rolls = {glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast<float>(From)), glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast<float>(To))};
return result;
}
inline std::vector<segment_data> arc_through(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec2 const &Offset)
{
auto const along{glm::dot(Offset, Direction)};
auto const lateral{cross(Direction, Offset)};
auto const chord{glm::length(Offset)};
if (along <= 0.0 || chord < 0.5)
return {};
if (std::abs(lateral) < 0.005 * chord + 0.05)
return straight_piece(Point, Direction, Grade, along);
auto const angle{2.0 * std::atan2(std::abs(lateral), along)};
return arc_pieces(Point, Direction, Grade, chord / (2.0 * std::sin(angle * 0.5)), angle, lateral > 0.0 ? 1 : -1);
}
inline double hermite(double const Length, double const Height0, double const Grade0, double const Height1, double const Grade1, double const T, double &Grade)
{
auto const t2{T * T};
auto const t3{t2 * T};
Grade = (6.0 * t2 - 6.0 * T) * (Height0 - Height1) / Length + (3.0 * t2 - 4.0 * T + 1.0) * Grade0 + (3.0 * t2 - 2.0 * T) * Grade1;
return (2.0 * t3 - 3.0 * t2 + 1.0) * Height0 + (t3 - 2.0 * t2 + T) * Length * Grade0 + (3.0 * t2 - 2.0 * t3) * Height1 + (t3 - t2) * Length * Grade1;
}
inline glm::dvec3 level(glm::dvec3 const &Vector)
{
glm::dvec3 const flat{Vector.x, 0.0, Vector.z};
return glm::length(flat) > 1e-9 ? glm::normalize(flat) : glm::dvec3{0.0, 0.0, 1.0};
}
} // namespace geometry

View File

@@ -0,0 +1,201 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "editor/editorGroundFollow.hpp"
#include "editor/editorGeometry.hpp"
#include <algorithm>
#include <cmath>
#include <iterator>
namespace ground_follow
{
namespace
{
using geometry::bezier;
using geometry::has_controls;
using geometry::part_of;
using geometry::plan_of;
double constexpr kStep{2.0};
double constexpr kTolerance{0.1};
double constexpr kSpacing{10.0};
struct samples
{
std::vector<double> chainages;
std::vector<glm::dvec3> points;
std::vector<glm::dvec2> directions;
};
samples sample(std::vector<segment_data> const &Pieces, std::vector<double> const &Lengths, double const Total, ends const &Ends)
{
samples result;
double reached{0.0};
for (std::size_t i = 0; i < Pieces.size(); ++i)
{
bezier const curve{Pieces[i]};
auto const count{std::max(1, static_cast<int>(std::ceil(Lengths[i] / kStep)))};
for (int k = (i == 0 ? 1 : 0); k < count; ++k)
{
auto const along{Lengths[i] * k / count};
auto const t{curve.parameter(along)};
auto const chainage{reached + along};
auto point{curve.point(t)};
point.y = Ends.height0 + (Ends.height1 - Ends.height0) * chainage / Total + 50.0;
result.chainages.push_back(chainage);
result.points.push_back(point);
result.directions.push_back(plan_of(curve.first(t)));
}
reached += Lengths[i];
}
return result;
}
void fit(knot_line &Line, std::vector<double> const &Chainages, std::vector<double> const &Targets)
{
for (std::size_t pass = 0; pass < Chainages.size(); ++pass)
{
auto worst{kTolerance};
auto pick{Chainages.size()};
for (std::size_t j = 0; j < Chainages.size(); ++j)
{
if (std::isnan(Targets[j]))
continue;
double grade{0.0};
auto const miss{std::abs(Line.at(Chainages[j], grade) - Targets[j])};
if (miss <= worst || false == Line.room(Chainages[j], kSpacing))
continue;
worst = miss;
pick = j;
}
if (pick == Chainages.size())
return;
Line.insert(Chainages[pick], Targets[pick]);
}
}
std::vector<segment_data> cut(std::vector<segment_data> const &Pieces, std::vector<double> const &Lengths, std::vector<double> const &Knots, std::vector<std::pair<double, double>> &Spans)
{
std::vector<segment_data> result;
double from{0.0};
for (std::size_t i = 0; i < Pieces.size(); ++i)
{
bezier const curve{Pieces[i]};
auto const to{from + Lengths[i]};
double cut{from};
double parameter{0.0};
for (auto const knot : Knots)
{
if (knot < cut + 0.5 || knot > to - 0.5)
continue;
auto const next{curve.parameter(knot - from)};
result.push_back(part_of(Pieces[i], parameter, next));
Spans.emplace_back(cut, knot);
cut = knot;
parameter = next;
}
result.push_back(parameter > 0.0 ? part_of(Pieces[i], parameter, 1.0) : Pieces[i]);
Spans.emplace_back(cut, to);
from = to;
}
return result;
}
void lift(segment_data &Piece, std::pair<double, double> const &Span, knot_line const &Line)
{
auto &points{Piece.points};
auto const [a, b]{Span};
auto const length{b - a};
double begin{0.0};
double end{0.0};
auto &start{points[segment_data::point::start]};
auto &finish{points[segment_data::point::end]};
auto &control1{points[segment_data::point::control1]};
auto &control2{points[segment_data::point::control2]};
start.y = Line.at(a, begin);
finish.y = Line.at(b, end);
if (has_controls(Piece))
{
control1.y = begin * glm::length(plan_of(control1));
control2.y = -end * glm::length(plan_of(control2));
return;
}
auto const linear{(finish.y - start.y) / std::max(length, 1e-6)};
if (length < 1e-3 || (std::abs(begin - linear) < 1e-6 && std::abs(end - linear) < 1e-6))
return;
auto const along{glm::normalize(plan_of(finish - start)) * (length / 3.0)};
control1 = {along.x, begin * length / 3.0, along.y};
control2 = {-along.x, -end * length / 3.0, -along.y};
}
} // namespace
void knot_line::refresh()
{
auto const last{knots.size() - 1};
slopes.assign(knots.size(), 0.0);
for (std::size_t i = 0; i <= last; ++i)
{
auto const a{i == 0 ? 0 : i - 1};
auto const b{i == last ? last : i + 1};
slopes[i] = (heights[b] - heights[a]) / (knots[b] - knots[a]);
}
if (false == std::isnan(grade0))
slopes.front() = grade0;
if (false == std::isnan(grade1))
slopes.back() = grade1;
}
void knot_line::insert(double const Chainage, double const Height)
{
auto const where{std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin()};
knots.insert(knots.begin() + where, Chainage);
heights.insert(heights.begin() + where, Height);
refresh();
}
double knot_line::at(double const Chainage, double &Grade) const
{
auto const next{static_cast<std::size_t>(std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin())};
auto const i{std::clamp<std::size_t>(next, 1, knots.size() - 1) - 1};
auto const length{knots[i + 1] - knots[i]};
return geometry::hermite(length, heights[i], slopes[i], heights[i + 1], slopes[i + 1], std::clamp((Chainage - knots[i]) / length, 0.0, 1.0), Grade);
}
bool knot_line::room(double const Chainage, double const Spacing) const
{
auto const next{std::upper_bound(knots.begin(), knots.end(), Chainage)};
return next != knots.end() && *next - Chainage >= Spacing && Chainage - *std::prev(next) >= Spacing;
}
void follow(std::vector<segment_data> &Pieces, ends const &Ends, bed_query const &Bed)
{
std::vector<double> lengths;
double total{0.0};
for (auto const &piece : Pieces)
total += lengths.emplace_back(bezier{piece}.plan_length(256));
if (total < 1e-3)
return;
auto const points{sample(Pieces, lengths, total, Ends)};
auto const targets{Bed(points.points, points.directions)};
knot_line line{{0.0, total}, {Ends.height0, Ends.height1}, {}, Ends.grade0, Ends.grade1};
line.refresh();
fit(line, points.chainages, targets);
std::vector<std::pair<double, double>> spans;
auto result{cut(Pieces, lengths, line.knots, spans)};
for (std::size_t i = 0; i < result.size(); ++i)
lift(result[i], spans[i], line);
Pieces = std::move(result);
}
} // namespace ground_follow

View File

@@ -0,0 +1,47 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <functional>
#include <vector>
#include <glm/glm.hpp>
#include "world/Segment.h"
namespace ground_follow
{
using bed_query = std::function<std::vector<double>(std::vector<glm::dvec3> const &, std::vector<glm::dvec2> const &)>;
struct ends
{
double height0{0.0};
double grade0{0.0};
double height1{0.0};
double grade1{0.0};
};
struct knot_line
{
std::vector<double> knots;
std::vector<double> heights;
std::vector<double> slopes;
double grade0{0.0};
double grade1{0.0};
void refresh();
void insert(double const Chainage, double const Height);
double at(double const Chainage, double &Grade) const;
bool room(double const Chainage, double const Spacing) const;
};
void follow(std::vector<segment_data> &Pieces, ends const &Ends, bed_query const &Bed);
} // namespace ground_follow

View File

@@ -363,4 +363,29 @@ void restore(std::vector<record> Records)
settle();
}
void place(record &Entry, TTrack const &First)
{
auto const &main{First.m_paths.front()};
geometry::bezier const curve{main};
Entry.start = main.points[segment_data::point::start];
auto const direction{geometry::plan_of(curve.first(0.0))};
Entry.direction = glm::length(direction) > 1e-12 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0};
Entry.km = alignment_store::chainage_at(Entry.start);
}
void classify(record &Entry, TTrack const &Switch)
{
geometry::bezier const main{Switch.m_paths[0]};
auto const &diverging{Switch.m_paths[1]};
Entry.hand = geometry::cross(Entry.direction, geometry::plan_of(diverging.points[segment_data::point::end] - Entry.start)) > 0.0 ? 'R' : 'L';
auto const first{geometry::plan_of(main.first(0.0))};
auto const last{geometry::plan_of(main.first(1.0))};
auto const turn{glm::length(first) > 1e-12 && glm::length(last) > 1e-12 ? geometry::signed_angle(glm::normalize(first), glm::normalize(last)) : 0.0};
Entry.main_radius = std::abs(turn) > 1e-6 ? main.plan_length() / turn : 0.0;
if (std::abs(Entry.main_radius) > 20000.0)
Entry.main_radius = 0.0;
if (Entry.type == "Rz" || Entry.type == "Rlj" || Entry.type == "Rld")
Entry.type = Entry.main_radius == 0.0 ? "Rz" : (Entry.main_radius > 0.0) == (Entry.hand == 'R') ? "Rlj" : "Rld";
}
} // namespace switch_store

View File

@@ -60,6 +60,8 @@ std::vector<std::string> follow();
double branch_radius(record const &Record);
// keeps the record in the file of the layer, in place of the one with the same name
void put(record Entry, scene::layer_handle const Layer);
void place(record &Entry, TTrack const &First);
void classify(record &Entry, TTrack const &Switch);
void forget(std::string const &Name);
// on save: the records follow the renamed switches, the ones whose paths are gone are dropped
void tidy();

View File

@@ -44,6 +44,7 @@ using geometry::bezier;
using geometry::chord_length;
using geometry::cross;
using geometry::plan_of;
using geometry::reversed;
using geometry::signed_angle;
using geometry::turned;
@@ -94,17 +95,6 @@ double path_radius(segment_data const &Path, double const T)
return curvature > 1e-7 ? 1.0 / curvature : 0.0;
}
segment_data reversed(segment_data const &Path)
{
segment_data result{Path};
result.points[segment_data::point::start] = Path.points[segment_data::point::end];
result.points[segment_data::point::end] = Path.points[segment_data::point::start];
result.points[segment_data::point::control1] = Path.points[segment_data::point::control2];
result.points[segment_data::point::control2] = Path.points[segment_data::point::control1];
result.rolls = {-Path.rolls[1], -Path.rolls[0]};
return result;
}
bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool const Leaving, glm::dvec3 &Direction, double &Radius)
{
if (Neighbour == nullptr)

View File

@@ -126,6 +126,60 @@ class editor_track
static std::vector<std::pair<TTrack *, point_ref>> partners(TTrack &Track, point_ref const &End);
static void align_controls(TTrack const &Track, point_ref const &Control, std::vector<std::pair<TTrack *, point_ref>> const &Partners);
static point_ref end_of(point_ref const &Control);
static glm::dvec3 control_vector(TTrack const &Track, point_ref const &Control);
static glm::dvec3 outward(TTrack const &Track, point_ref const &Point);
static point_ref switch_main(TTrack const &Track, point_ref Point);
static std::size_t driven_paths(TTrack const &Track);
static double joint_roll(TTrack const &Track, point_ref const &Point);
static void roll_joined(TTrack &Other, point_ref const &Theirs, float const Roll);
static void match_rolls(TTrack const &Track, std::vector<joint> const &Joints);
struct path_end
{
int path{0};
bool atend{true};
};
static std::optional<path_end> stretch_end(TTrack const &Track, point_ref const &Point);
static std::vector<segment_data> stretched(segment_data const &Path, bool const Atend, double const Length);
static std::pair<glm::dvec3, glm::dvec3> end_axis(segment_data const &Path, bool const Atend);
static point_ref stretch_remap(point_ref Point, int const Path, std::size_t const Pieces);
struct middle_point
{
glm::dvec3 point{0.0};
glm::dvec3 tangent{0.0, 0.0, 1.0};
double chainage{0.0};
};
static middle_point middle(TTrack const &Track);
struct edit
{
std::vector<std::pair<TTrack *, state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
std::vector<TTrack *> changed;
void capture(TTrack &Track);
};
struct drive_spot
{
glm::dvec3 location{0.0};
double yaw{0.0};
glm::dvec2 tilt{0.0};
bool rightswitch{false};
};
static std::optional<drive_spot> drive_spot_of(TTrack const &Switch);
struct straight_axis
{
glm::dvec3 origin{0.0};
glm::dvec2 direction{0.0, 1.0};
double length{0.0};
double along(glm::dvec3 const &Point) const;
double lateral(glm::dvec3 const &Point) const;
};
static std::optional<straight_axis> main_axis(TTrack const &Switch);
static std::vector<segment_data> slid(TTrack const &Switch, double const Distance);
struct snap_target
{

286
editor/editorTrackShape.cpp Normal file
View File

@@ -0,0 +1,286 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "editor/editorTrack.hpp"
#include "editor/editorGeometry.hpp"
#include "world/Track.h"
#include "utilities/Globals.h"
#include <algorithm>
#include <cmath>
namespace
{
using geometry::arc_pieces;
using geometry::bezier;
using geometry::cross;
using geometry::grade_of;
using geometry::plan_distance;
using geometry::plan_of;
using geometry::reversed;
using geometry::signed_angle;
using geometry::straight_piece;
std::vector<segment_data> scaled(segment_data const &Path, bool const Atend, double const Length, double const Original)
{
auto const &start{Path.points[segment_data::point::start]};
auto const &end{Path.points[segment_data::point::end]};
auto const scale{Length / std::max(Original, 1e-6)};
segment_data piece{Path};
piece.points[segment_data::point::end] = start + (end - start) * scale;
piece.points[segment_data::point::control1] *= scale;
piece.points[segment_data::point::control2] *= scale;
piece.radius *= static_cast<float>(scale);
return {Atend ? piece : reversed(piece)};
}
void spread_rolls(std::vector<segment_data> &Pieces, segment_data const &Path, double const Original, float const Radius)
{
double chainage{0.0};
for (auto &piece : Pieces)
{
auto const piecelength{bezier{piece}.plan_length()};
auto const from{static_cast<float>(std::clamp(chainage / Original, 0.0, 1.0))};
auto const to{static_cast<float>(std::clamp((chainage + piecelength) / Original, 0.0, 1.0))};
piece.rolls = {glm::mix(Path.rolls[0], Path.rolls[1], from), glm::mix(Path.rolls[0], Path.rolls[1], to)};
piece.radius = Radius;
chainage += piecelength;
}
}
} // namespace
std::vector<segment_data> editor_track::stretched(segment_data const &Path, bool const Atend, double const Length)
{
auto const path{Atend ? Path : reversed(Path)};
bezier const curve{path};
auto const &start{path.points[segment_data::point::start]};
auto const &end{path.points[segment_data::point::end]};
auto const first{plan_of(curve.first(0.0))};
auto const last{plan_of(curve.first(1.0))};
if (glm::length(first) < 1e-9 || glm::length(last) < 1e-9)
return {};
glm::dvec2 const direction{glm::normalize(first)};
auto const grade{grade_of(curve.first(0.0))};
auto const original{curve.plan_length()};
auto const turn{signed_angle(direction, glm::normalize(last))};
auto const lateral{cross(direction, plan_of(end - start))};
std::vector<segment_data> pieces;
float radius{0.0f};
if (std::abs(turn) < 1e-4 && std::abs(lateral) < 0.01)
pieces = straight_piece(start, direction, grade, Length);
else
{
auto const arcradius{original / std::abs(turn)};
int const side{turn > 0.0 ? 1 : -1};
auto const arc{arc_pieces(start, direction, grade, arcradius, std::abs(turn), side)};
bool const circular{std::abs(turn) > 1e-4 && plan_distance(arc.back().points[segment_data::point::end], end) < 0.05 && plan_distance(arc.front().points[segment_data::point::start], start) < 1e-6};
if (false == circular)
return scaled(path, Atend, Length, original);
pieces = arc_pieces(start, direction, grade, arcradius, std::min(Length / arcradius, glm::radians(330.0)), side);
radius = path.radius;
}
spread_rolls(pieces, path, original, radius);
if (false == Atend)
{
std::reverse(pieces.begin(), pieces.end());
for (auto &piece : pieces)
piece = reversed(piece);
}
return pieces;
}
std::optional<editor_track::path_end> editor_track::stretch_end(TTrack const &Track, point_ref const &Point)
{
if (Track.eType != tt_Normal || false == is_supported(Track) || Track.m_paths.empty())
return std::nullopt;
auto const last{static_cast<int>(Track.m_paths.size()) - 1};
if (false == Point.valid())
{
bool const startfree{false == is_connected(Track, {0, point_kind::start})};
bool const endfree{false == is_connected(Track, {last, point_kind::end})};
bool const atend{endfree || false == startfree};
return path_end{atend ? last : 0, atend};
}
if (false == is_end(Point.kind) || Point.path > last)
return std::nullopt;
auto const position{point_position(Track, Point)};
for (int i = 0; i <= last; ++i)
if (i != Point.path && (plan_distance(Track.m_paths[i].points[segment_data::point::start], position) < geometry::same_point || plan_distance(Track.m_paths[i].points[segment_data::point::end], position) < geometry::same_point))
return std::nullopt;
return path_end{Point.path, Point.kind == point_kind::end};
}
std::pair<glm::dvec3, glm::dvec3> editor_track::end_axis(segment_data const &Path, bool const Atend)
{
bezier const curve{Path};
auto const tangent{Atend ? curve.first(1.0) : -curve.first(0.0)};
return {Path.points[Atend ? segment_data::point::end : segment_data::point::start], glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{}};
}
editor_track::point_ref editor_track::stretch_remap(point_ref Point, int const Path, std::size_t const Pieces)
{
if (false == Point.valid() || Point.path < Path)
return Point;
auto const extra{static_cast<int>(Pieces) - 1};
if (Point.path > Path || Point.kind == point_kind::end || Point.kind == point_kind::control2)
Point.path += extra;
return Point;
}
std::size_t editor_track::driven_paths(TTrack const &Track)
{
return Track.eType == tt_Switch ? std::min<std::size_t>(1, Track.m_paths.size()) : Track.m_paths.size();
}
editor_track::middle_point editor_track::middle(TTrack const &Track)
{
auto const count{driven_paths(Track)};
double total{0.0};
for (std::size_t i = 0; i < count; ++i)
total += bezier{Track.m_paths[i]}.plan_length();
middle_point result{Track.location(), {0.0, 0.0, 1.0}, total * 0.5};
double along{0.0};
for (std::size_t i = 0; i < count; ++i)
{
bezier const curve{Track.m_paths[i]};
auto const length{curve.plan_length()};
if (along + length >= result.chainage || i + 1 == count)
{
auto const t{curve.parameter(result.chainage - along)};
result.point = curve.point(t);
result.tangent = curve.first(t);
return result;
}
along += length;
}
return result;
}
double editor_track::joint_roll(TTrack const &Track, point_ref const &Point)
{
if (Point.path < 0 || Point.path >= static_cast<int>(Track.m_paths.size()))
return 0.0;
auto const &path{Track.m_paths[Point.path]};
return Point.kind == point_kind::start ? path.rolls[0] : -path.rolls[1];
}
void editor_track::roll_joined(TTrack &Other, point_ref const &Theirs, float const Roll)
{
auto &theirs{Other.m_paths[Theirs.path]};
if (Theirs.kind == point_kind::start)
theirs.rolls[0] = -Roll;
else
theirs.rolls[1] = Roll;
}
void editor_track::match_rolls(TTrack const &Track, std::vector<joint> const &Joints)
{
for (auto const &joint : Joints)
if (joint.own.path < static_cast<int>(Track.m_paths.size()) && joint.theirs.path < static_cast<int>(joint.other->m_paths.size()))
roll_joined(*joint.other, joint.theirs, static_cast<float>(joint_roll(Track, joint.own)));
}
glm::dvec3 editor_track::outward(TTrack const &Track, point_ref const &Point)
{
bezier const curve{Track.m_paths[Point.path]};
return Point.kind == point_kind::start ? -curve.first(0.0) : curve.first(1.0);
}
editor_track::point_ref editor_track::switch_main(TTrack const &Track, point_ref Point)
{
if (Track.eType == tt_Switch && Point.valid() && Point.path > 0)
Point.path = 0;
return Point;
}
glm::dvec3 editor_track::control_vector(TTrack const &Track, point_ref const &Control)
{
return point_position(Track, Control) - point_position(Track, end_of(Control));
}
std::optional<editor_track::drive_spot> editor_track::drive_spot_of(TTrack const &Switch)
{
if (Switch.eType != tt_Switch || Switch.m_paths.size() < 2)
return std::nullopt;
auto const &main{Switch.m_paths[0]};
auto const &diverging{Switch.m_paths[1]};
auto const start{main.points[segment_data::point::start]};
auto const &control{main.points[segment_data::point::control1]};
auto const along{control != glm::dvec3{} ? control : main.points[segment_data::point::end] - start};
auto heading{plan_of(along)};
if (glm::length(heading) < 1e-9)
return std::nullopt;
heading = glm::normalize(heading);
glm::dvec2 const right{-heading.y, heading.x};
drive_spot spot;
spot.rightswitch = glm::dot(plan_of(diverging.points[segment_data::point::end] - start), right) > 0.0;
spot.location = start;
auto const yaw{glm::degrees(std::atan2(heading.x, heading.y))};
spot.yaw = yaw < 0.0 ? yaw + 360.0 : yaw;
auto const run{glm::length(plan_of(along))};
auto const roll{static_cast<double>(main.rolls[0])};
spot.tilt = {-glm::degrees(std::atan(along.y / run)), roll};
if (Global.bRollFix)
spot.location.y += 0.75 * std::abs(std::sin(glm::radians(roll)));
return spot;
}
double editor_track::straight_axis::along(glm::dvec3 const &Point) const
{
return glm::dot(plan_of(Point - origin), direction);
}
double editor_track::straight_axis::lateral(glm::dvec3 const &Point) const
{
return std::abs(cross(direction, plan_of(Point - origin)));
}
std::optional<editor_track::straight_axis> editor_track::main_axis(TTrack const &Switch)
{
if (Switch.m_paths.empty())
return std::nullopt;
auto const &main{Switch.m_paths.front()};
auto const origin{main.points[segment_data::point::start]};
auto const axis{plan_of(main.points[segment_data::point::end] - origin)};
auto const length{glm::length(axis)};
if (length < 1e-3)
return std::nullopt;
return straight_axis{origin, axis / length, length};
}
std::vector<segment_data> editor_track::slid(TTrack const &Switch, double const Distance)
{
if (Switch.m_paths.empty())
return {};
auto const &main{Switch.m_paths.front()};
auto const run{main.points[segment_data::point::end] - main.points[segment_data::point::start]};
auto const planlength{glm::length(plan_of(run))};
if (planlength < 1e-3)
return {};
auto const shift{run * (Distance / planlength)};
auto paths{Switch.m_paths};
for (auto &path : paths)
{
path.points[segment_data::point::start] += shift;
path.points[segment_data::point::end] += shift;
}
return paths;
}
void editor_track::edit::capture(TTrack &Track)
{
if (std::find(changed.begin(), changed.end(), &Track) != changed.end())
return;
states.emplace_back(&Track, editor_track::capture(Track));
changed.push_back(&Track);
}