diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c023ec6..8524fd8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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" diff --git a/application/editormode.h b/application/editormode.h index 07480eeb..793f8e14 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -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 const &Connection, std::function 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 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 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 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_templates(std::function 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 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 const &Tracks); // places them in the active layer, as one step of the undo. returns: how many were placed diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp new file mode 100644 index 00000000..a5b05e4c --- /dev/null +++ b/application/editormoderecords.cpp @@ -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 +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_of; +using geometry::signed_angle; +using geometry::turned; +using path_ends = std::pair; + +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 const &Moving, std::vector const &Before, TTrack const *Track) +{ + return Before[static_cast(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 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 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 const &Tracks, std::vector const &Moving, std::vector 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 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(path.rolls[end]))))}; + Drift.cant = std::max(Drift.cant, std::abs(actual - alignment_store::cant_at(Record, found->chainage))); + } + } +} + +std::vector drift_notes(record_drift const &Drift) +{ + std::vector 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 +bool edit_dashed(char const *Label, std::string &Value) +{ + std::array 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 ¬e : notes) + text += (text.empty() ? "" : "\n") + note; + ui()->set_status(text, drift.departed > 0 || drift.cant > 5.0 || notes.size() > static_cast(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 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(); + } +} diff --git a/application/editormodeshared.h b/application/editormodeshared.h new file mode 100644 index 00000000..8d482e74 --- /dev/null +++ b/application/editormodeshared.h @@ -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)); +} diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp index ee57378a..5cc26e21 100644 --- a/application/editormodesweep.cpp +++ b/application/editormodesweep.cpp @@ -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) diff --git a/application/editormodeswitchdrive.cpp b/application/editormodeswitchdrive.cpp new file mode 100644 index 00000000..fc92a14e --- /dev/null +++ b/application/editormodeswitchdrive.cpp @@ -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 +#include +#include +#include +#include + +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(std::tolower(Character)); }); + return Text; +} + +int drive_side(std::string const &Side) +{ + if (Side == "left") + return 0; + return Side == "right" ? 1 : -1; +} + +std::optional 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 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(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 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 ¶meter : info.parameters) + { + if (parameter.id < 1 || parameter.id > static_cast(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(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::find_switch_drive(TTrack const &Switch) +{ + auto const &tool{m_switch}; + scan_switch_drives(); + std::map, std::less<>> variants; + for (int family = 0; family < static_cast(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(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 + and - 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; +} diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp new file mode 100644 index 00000000..15ea0945 --- /dev/null +++ b/application/editormodeswitchslide.cpp @@ -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 +#include +#include + +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> switch_end_moves(TTrack const &Track, segment_data const &Main, bool const Online, std::pair const &Room, double const Offset, std::string &Status) +{ + auto const axis{editor_track::main_axis(Track)}; + std::vector 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 const &Joints, std::vector const &Handled, editor_track::edit &Edit) +{ + std::vector 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> 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 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 positions; + for (auto const &joint : joints) + positions.push_back(editor_track::point_position(Track, joint.own)); + std::vector 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()); +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index d05c9d41..22177a45 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -12,6 +12,7 @@ http: #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" @@ -50,6 +51,11 @@ using geometry::plan_of; using geometry::signed_angle; using geometry::turned; using geometry::arc_pieces; +using geometry::arc_through; +using geometry::has_controls; +using geometry::level; +using geometry::reversed; +using geometry::straight_piece; double constexpr kParallelFindAngle{15.0}; // deg, neighbours counted as beside the straight double constexpr kParallelSnapAngle{5.0}; // deg, close enough to snap parallel @@ -92,7 +98,6 @@ editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, float const kHandleRadius{10.0f}; double const kLaySnapRadius{5.0}; float const kSnapPixels{16.0f}; -float const kDragPixels{5.0f}; double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch double const kLineReach{2000.0}; // how far each way the Curve tab follows the line double const kDrawnReach{100000.0}; @@ -100,12 +105,6 @@ double const kJointRange{400.0}; // the ends of the paths are drawn for the trac ImU32 const kJointColor{IM_COL32(255, 255, 255, 200)}; int const kRailCategory{1}; -void item_tooltip(char const *Text) -{ - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C(Text)); -} - // the curve a track ends with at Joint, as it goes on beyond the joint, and a transition curve leading out of it to the straight struct easing { @@ -124,7 +123,7 @@ easing ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double cons { if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > 0.05) continue; - if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) + if (false == has_controls(path)) return result; bezier const curve{path}; auto const first{curve.first(atend ? 1.0 : 0.0)}; @@ -176,71 +175,6 @@ easing ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double cons return result; } -segment_data turned_around(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; -} - -double joint_roll(TTrack const &Track, editor_track::point_ref const &Point) -{ - if (Point.path < 0 || Point.path >= static_cast(Track.m_paths.size())) - return 0.0; - auto const &path{Track.m_paths[Point.path]}; - return Point.kind == editor_track::point_kind::start ? path.rolls[0] : -path.rolls[1]; -} - -std::vector 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}; -} - -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; -} - -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 (Piece.points[segment_data::point::control1] != glm::dvec3{} || Piece.points[segment_data::point::control2] != glm::dvec3{}) - { - 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(From)), glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast(To))}; - return result; -} - -std::vector 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); -} - // curvature of the shape at Chainage, positive turning from x towards z double curvature_at(alignment::result const &Shape, double const Chainage) { @@ -266,203 +200,6 @@ void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex) Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); } -std::vector stretched(segment_data const &Path, bool const Atend, double const Length) -{ - auto const path{Atend ? Path : turned_around(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 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)}; - if (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) - { - pieces = arc_pieces(start, direction, grade, arcradius, std::min(Length / arcradius, glm::radians(330.0)), side); - radius = path.radius; - } - else - { - 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(scale); - return {Atend ? piece : turned_around(piece)}; - } - } - double chainage{0.0}; - for (auto &piece : pieces) - { - auto const piecelength{bezier{piece}.plan_length()}; - auto const from{static_cast(std::clamp(chainage / original, 0.0, 1.0))}; - auto const to{static_cast(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; - } - if (false == Atend) - { - std::reverse(pieces.begin(), pieces.end()); - for (auto &piece : pieces) - piece = turned_around(piece); - } - return pieces; -} - -bool stretch_end(TTrack const &Track, editor_track::point_ref const &Point, int &Path, bool &Atend) -{ - if (Track.eType != tt_Normal || false == editor_track::is_supported(Track) || Track.m_paths.empty()) - return false; - auto const last{static_cast(Track.m_paths.size()) - 1}; - if (Point.valid()) - { - if (false == editor_track::is_end(Point.kind) || Point.path > last) - return false; - Path = Point.path; - Atend = Point.kind == editor_track::point_kind::end; - auto const position{editor_track::point_position(Track, Point)}; - for (int i = 0; i <= last; ++i) - if (i != 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 false; - return true; - } - bool const startfree{false == editor_track::is_connected(Track, {0, editor_track::point_kind::start})}; - bool const endfree{false == editor_track::is_connected(Track, {last, editor_track::point_kind::end})}; - Atend = endfree || false == startfree; - Path = Atend ? last : 0; - return true; -} - -std::pair stretch_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{}}; -} - -bool mouse_along(glm::dvec3 const &Origin, glm::dvec3 const &Axis, double &Along) -{ - screen_projection const projection; - glm::dvec3 origin, 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 track_middle(TTrack const &Track, glm::dvec3 &Point, glm::dvec3 &Tangent, double &Chainage) -{ - auto const count{Track.eType == tt_Switch ? std::min(1, Track.m_paths.size()) : Track.m_paths.size()}; - double total{0.0}; - for (std::size_t i = 0; i < count; ++i) - total += bezier{Track.m_paths[i]}.plan_length(); - Chainage = total * 0.5; - double along{0.0}; - for (std::size_t i = 0; i < count; ++i) - { - auto const &path{Track.m_paths[i]}; - bezier const curve{path}; - auto const length{curve.plan_length()}; - if (along + length >= Chainage || i + 1 == count) - { - auto const t{curve.parameter(Chainage - along)}; - Point = curve.point(t); - Tangent = curve.first(t); - return; - } - along += length; - } - Point = Track.location(); - Tangent = {0.0, 0.0, 1.0}; -} - -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}; -} - -double roll_into(TTrack const &Track, editor_track::point_ref const &Point) -{ - auto const &path{Track.m_paths[Point.path]}; - return Point.kind == editor_track::point_kind::start ? path.rolls[0] : -path.rolls[1]; -} - -void match_rolls(TTrack const &Track, std::vector const &Joints) -{ - for (auto const &joint : Joints) - { - if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(joint.other->m_paths.size())) - continue; - auto const roll{static_cast(roll_into(Track, joint.own))}; - auto &theirs{joint.other->m_paths[joint.theirs.path]}; - if (joint.theirs.kind == editor_track::point_kind::start) - theirs.rolls[0] = -roll; - else - theirs.rolls[1] = roll; - } -} - -glm::dvec3 track_outward(TTrack const &Track, editor_track::point_ref const &Point) -{ - bezier const curve{Track.m_paths[Point.path]}; - return Point.kind == editor_track::point_kind::start ? -curve.first(0.0) : curve.first(1.0); -} - -editor_track::point_ref switch_main(TTrack const &Track, editor_track::point_ref Point) -{ - if (Track.eType == tt_Switch && Point.valid() && Point.path > 0) - Point.path = 0; - return Point; -} - -editor_track::point_ref stretch_remap(editor_track::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(Pieces) - 1}; - if (Point.path > Path) - Point.path += extra; - else if (Point.kind == editor_track::point_kind::end || Point.kind == editor_track::point_kind::control2) - Point.path += extra; - return Point; -} - } TTrack *editor_mode::selected_track() const @@ -596,612 +333,6 @@ void editor_mode::draw_track_overlay() const } } -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; - int path{0}; - bool atend{true}; - return stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}, path, atend) ? 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)}; - glm::dvec3 axis{0.0}; - if (Operation == gizmo_operation::scale) - { - int path{0}; - bool atend{true}; - if (Pointmode && false == editor_track::is_end(m_track_point.kind)) - axis = anchor - editor_track::point_position(Track, editor_track::end_of(m_track_point)); - else if (stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}, path, atend)) - std::tie(anchor, axis) = stretch_axis(Track.m_paths[path], atend); - } - else if (Operation == gizmo_operation::rotate) - { - if (Pointmode && false == editor_track::is_end(m_track_point.kind)) - axis = level(anchor - editor_track::point_position(Track, editor_track::end_of(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 - { - double chainage{0.0}; - track_middle(Track, anchor, axis, chainage); - axis = level(axis); - } - } - 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 &entry : m_track_gizmo_drag.held) - editor_track::settle_controls(*entry.first, entry.second); - m_track_gizmo_drag.held.clear(); -} - -void editor_mode::track_gizmo_stretch(TTrack &Track, double const Length) -{ - auto &drag{m_track_gizmo_drag}; - auto const pieces{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 = stretch_remap(drag.point, drag.path, drag.pieces); - auto joints{m_track_joints}; - for (auto &joint : joints) - joint.own = stretch_remap(joint.own, drag.path, drag.pieces); - editor_track::follow(Track, joints); - std::vector rest; - if (Track.eType == tt_Normal && pieces.size() > 1) - { - auto const first{Track.m_paths.begin() + drag.path}; - auto const keep{drag.atend ? first : first + (pieces.size() - 1)}; - for (auto piece = first; piece != first + pieces.size(); ++piece) - if (piece != keep) - rest.push_back(*piece); - auto const kept{*keep}; - Track.m_paths.erase(first, first + pieces.size()); - 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(); - m_track_drag.erase(std::remove(m_track_drag.begin(), m_track_drag.end(), surplus), m_track_drag.end()); - 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}); - } - m_track_dirty = true; -} - -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); - if (Track.eType == tt_Switch) - { - 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_point(TTrack &Track, glm::dvec3 const &Position) -{ - editor_track::move_point(Track, m_track_point, Position); - if (Track.eType == tt_Switch) - { - editor_track::conform_switch(Track); - editor_track::follow(Track, m_track_joints); - editor_track::follow_grade(Track, m_track_joints); - } - auto const moved{editor_track::point_position(Track, m_track_point)}; - auto const &links{m_track_gizmo_drag.switches}; - for (auto const &connection : m_track_drag_points) - if (std::none_of(links.begin(), links.end(), [&](auto const &Link) { return std::any_of(Link.cluster.begin(), Link.cluster.end(), [&](auto const &Member) { return Member.first == connection.first; }); })) - editor_track::move_point(*connection.first, connection.second, moved); -} - -void editor_mode::track_gizmo_switches(TTrack &Track) -{ - for (auto &link : m_track_gizmo_drag.switches) - { - if (link.own.path >= static_cast(Track.m_paths.size())) - continue; - auto const point{editor_track::point_position(Track, link.own)}; - auto const tangent{track_outward(Track, link.own)}; - auto const roll{roll_into(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; - 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 : link.cluster) - { - member.first->m_paths = member.second; - for (auto &path : member.first->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); - } - } - } - if (std::abs(roll - link.roll) > 1e-6) - { - 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(value), static_cast(value)}; - editor_track::conform_switch(*link.track); - } - for (auto const &entry : link.joints) - { - editor_track::follow(*entry.first, entry.second); - editor_track::follow_grade(*entry.first, entry.second); - match_rolls(*entry.first, entry.second); - } - } -} - -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) - { - m_track_joints = editor_track::joints(Track); - for (auto const &joint : m_track_joints) - if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) - m_track_drag.emplace_back(joint.other); - } - if (Pointmode && false == controlmode && Operation == gizmo_operation::translate && m_track_drag_connected) - { - for (auto const &connection : editor_track::connected_points(Track, editor_track::point_position(Track, m_track_point))) - { - std::string reason; - if (false == editor_track::can_edit_geometry(*connection.first, reason)) - continue; - m_track_drag_points.emplace_back(connection); - if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) - m_track_drag.emplace_back(connection.first); - } - } - if (controlmode) - { - for (auto const &partner : editor_track::partners(Track, editor_track::end_of(m_track_point))) - { - std::string reason; - auto const &path{partner.first->m_paths[partner.second.path]}; - bool const own{partner.first == &Track}; - if (own ? m_track_point.path != 0 : partner.first->eType != tt_Normal || false == editor_track::can_edit_geometry(*partner.first, reason)) - { - bool const start{partner.second.kind == editor_track::point_kind::start}; - auto const theirs{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{theirs != glm::dvec3{} ? theirs : chord}; - if (glm::length(along) > 1e-9) - drag.fixed = (own && partner.second.kind == editor_track::end_of(m_track_point).kind ? 1.0 : -1.0) * glm::normalize(along); - continue; - } - if (false == m_track_drag_connected && partner.first != &Track) - continue; - drag.partners.push_back(partner); - if (std::find(m_track_drag.begin(), m_track_drag.end(), partner.first) == m_track_drag.end()) - m_track_drag.emplace_back(partner.first); - } - } - if (false == whole && Track.eType == tt_Switch && m_track_drag_connected) - { - 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); - if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) - m_track_drag.emplace_back(joint.other); - } - } - if (m_track_drag_connected) - { - auto const taken = [&](TTrack const *Other) { return std::any_of(drag.switches.begin(), drag.switches.end(), [&](auto const &Link) { return std::any_of(Link.cluster.begin(), Link.cluster.end(), [&](auto const &Member) { return Member.first == Other; }); }); }; - auto const movable = [&](TTrack const *Other) { - std::string reason; - return Other != &Track && Other->eType == tt_Switch && Other->m_paths.size() == 2 && editor_track::can_edit_geometry(*Other, reason) && false == taken(Other); - }; - for (int i = 0; i < static_cast(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 (false == movable(connection.first)) - continue; - track_gizmo_drag::switch_joint link{connection.first, own, connection.second, editor_track::point_position(Track, own), track_outward(Track, own), roll_into(Track, own)}; - std::vector 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{}); - auto joints{editor_track::joints(*member)}; - joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return Joint.other == &Track; }), joints.end()); - if (std::find(m_track_drag.begin(), m_track_drag.end(), member) == m_track_drag.end()) - m_track_drag.emplace_back(member); - for (auto const &joint : joints) - if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) - m_track_drag.emplace_back(joint.other); - link.joints.emplace_back(member, std::move(joints)); - } - drag.switches.push_back(std::move(link)); - } - } - } - std::vector> 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 : link.cluster) - member.second = member.first->m_paths; - if (controlmode) - { - drag.origin = editor_track::point_position(Track, m_track_point); - auto const control{drag.origin - editor_track::point_position(Track, editor_track::end_of(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 (Operation == gizmo_operation::scale) - { - stretch_end(Track, drag.point, drag.path, drag.atend); - drag.paths = Track.m_paths; - std::tie(drag.origin, drag.axis) = stretch_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) - { - auto const frame{track_gizmo_frame(Track, Pointmode, Operation)}; - 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{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(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_turn(TTrack &Track, int const Axis, double const Angle) -{ - auto &drag{m_track_gizmo_drag}; - if (drag.control && drag.fixed != glm::dvec3{}) - return; - auto const snap{Global.ctrlState}; - if (Axis == 1) - { - auto target{Angle}; - if (snap) - target = glm::radians(std::round(glm::degrees(target) / 5.0) * 5.0); - auto const step{std::remainder(target - drag.angle, 2.0 * glm::pi())}; - if (std::abs(step) < 1e-9) - return; - drag.angle += step; - if (drag.control) - { - auto const control{editor_track::point_position(Track, drag.point) - editor_track::point_position(Track, editor_track::end_of(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; - return; - } - if (Axis == 0) - { - auto target{-std::tan(std::clamp(Angle, -1.4, 1.4))}; - if (snap) - target = std::round(target * 1000.0) / 1000.0; - auto const step{target - drag.grade}; - if (std::abs(step) < 1e-9) - return; - drag.grade = target; - if (drag.control) - { - auto const control{editor_track::point_position(Track, drag.point) - editor_track::point_position(Track, editor_track::end_of(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{Track.eType == tt_Switch ? std::min(1, Track.m_paths.size()) : Track.m_paths.size()}; - 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; - return; - } - auto const end{editor_track::end_of(drag.point)}; - auto const canted{switch_main(Track, end)}; - auto target{glm::degrees(drag.point.kind == editor_track::point_kind::control2 ? -Angle : Angle)}; - if (snap) - target = glm::degrees(std::asin(std::clamp(std::round(1500.0 * std::sin(glm::radians(drag.roll0 + target)) / 5.0) * 5.0 / 1500.0, -1.0, 1.0))) - drag.roll0; - auto const step{static_cast(target - drag.roll)}; - if (std::abs(step) < 1e-6f) - return; - drag.roll = target; - for (int i = 0; i < static_cast(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; - } - if (Track.eType == tt_Switch) - { - editor_track::conform_switch(Track); - editor_track::follow(Track, m_track_joints); - editor_track::follow_grade(Track, m_track_joints); - } - match_rolls(Track, m_track_joints); - for (auto const &partner : drag.partners) - { - if (partner.first == &Track) - continue; - auto const roll{static_cast(roll_into(Track, end))}; - auto &theirs{partner.first->m_paths[partner.second.path]}; - if (partner.second.kind == editor_track::point_kind::start) - theirs.rolls[0] = -roll; - else - theirs.rolls[1] = roll; - } - m_track_dirty = true; -} - -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::render_track_gizmo() -{ - auto *track = selected_track(); - std::string reason; - if (track == nullptr || false == editor_track::can_edit_geometry(*track, reason)) - { - m_track_gizmo_using = false; - return; - } - bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast(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 (ImGuizmo::IsUsing()) - { - 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) - { - auto const turn{glm::transpose(drag.basis) * glm::dmat3(glm::mat3(m_track_gizmo))}; - std::array 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(largest - angles.begin()); - } - if (drag.ring >= 0) - track_gizmo_turn(*track, drag.ring, angles[drag.ring]); - } - else if (operation == gizmo_operation::scale) - { - double along{0.0}; - if (mouse_along(drag.origin, drag.axis, along)) - { - 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); - } - } - else if (controlmode) - { - glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]); - track_gizmo_control(*track, position - editor_track::point_position(*track, editor_track::end_of(m_track_point))); - } - else if (pointmode) - { - track_gizmo_point(*track, frame.camera + glm::dvec3(m_track_gizmo[3])); - auto const position{editor_track::point_position(*track, m_track_point)}; - m_track_dirty = true; - if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) - { - std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); - screen_projection const projection; - ImVec2 screen; - m_track_snap = projection.project(position, screen) ? snap_free_end(position, {screen.x, screen.y}, track, track->iCategoryFlag & 15, exclude) : editor_track::snap_target{}; - } - } - else - { - glm::dvec3 const offset{delta[3]}; - if (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); - } - else if (m_track_gizmo_using) - track_gizmo_end(*track); -} - void editor_mode::commit_track_drag(bool const Force) { if (false == m_track_dirty || m_track_drag.empty()) @@ -1245,159 +376,6 @@ void editor_mode::push_track_snapshot(std::vectorm_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 &entry : watch.on) - { - bool whole{true}; - for (auto *track : simulation::Paths.sequence()) - if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Normal && false == track->m_paths.empty() && - std::find(watch.moving.begin(), watch.moving.end(), track) == watch.moving.end() && lies_on(entry.first, *track)) - { - whole = false; - break; - } - if (whole) - watch.whole.insert(entry.first); - } -} - -// 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; - auto const index = [&](TTrack const *Track) { return static_cast(std::find(watch.moving.begin(), watch.moving.end(), Track) - watch.moving.begin()); }; - auto const chord = [](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}; - }; - std::vector notes; - int followed{0}; - double departure{0.0}, cant{0.0}; - int departed{0}; - std::string layer; - for (auto const &[record, tracks] : watch.on) - { - if (watch.whole.count(record) > 0) - { - auto const &first{watch.before[index(tracks.front())]}; - auto const &path{tracks.front()->m_paths.front()}; - auto const from{chord(first.first, first.second)}; - auto const to{chord(path.points[segment_data::point::start], path.points[segment_data::point::end])}; - auto const angle{signed_angle(from, to)}; - bool rigid{true}; - for (auto *track : tracks) - { - auto const &was{watch.before[index(track)]}; - auto const &now{track->m_paths.front()}; - for (auto const &[old, current] : {std::pair{was.first, now.points[segment_data::point::start]}, std::pair{was.second, now.points[segment_data::point::end]}}) - rigid = rigid && glm::distance(plan_of(path.points[segment_data::point::start]) + turned(plan_of(old) - plan_of(first.first), angle), plan_of(current)) <= 0.01; - } - if (rigid) - { - bool const moved{glm::distance(first.first, path.points[segment_data::point::start]) > 1e-4 || std::abs(angle) > 1e-7}; - if (moved) - { - alignment_store::transform(record, first.first, from, path.points[segment_data::point::start], to); - ++followed; - } - continue; - } - } - auto const &records{alignment_store::records()}; - auto const gauge{records[record].design.norms.gauge}; - for (auto *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) - { - ++departed; - departure = std::max(departure, largest); - 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(path.rolls[end]))))}; - cant = std::max(cant, std::abs(actual - alignment_store::cant_at(record, found->chainage))); - } - } - } - if (followed > 0) - notes.push_back(STR("The kept design of the line went along with its paths")); - if (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"), departed, layer.c_str(), departure)); - if (cant > 5.0) - notes.push_back(format(STR_C("The cant differs from the kept design by up to %.0f mm"), cant)); - switch_store::follow(); - for (auto *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 ¬e : notes) - text += (text.empty() ? "" : "\n") + note; - ui()->set_status(text, departed > 0 || cant > 5.0 || notes.size() > static_cast(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); -} - bool editor_mode::tracks_editable(std::vector const &Tracks, std::string &Reason) { for (auto const *track : Tracks) @@ -1929,11 +907,10 @@ void editor_mode::render_turnout_ui() ImGui::Text(STR_C("Length %.3f m, diverging %s, end offset %.3f m"), length, offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset)); render_track_name(*track); render_switch_record(*track); - standing_template drive; - int family{-1}; - int variant{0}; - if (find_switch_drive(*track, drive, family, variant)) + if (auto const drive{find_switch_drive(*track)}) { + auto const family{drive->family}; + auto const variant{drive->index}; ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), track->name().c_str(), track->name().c_str()); ImGui::PushItemWidth(200.0f); if (ImGui::BeginCombo(STR_C("Drive##standing"), m_switch.drives[family].name.c_str())) @@ -2640,23 +1617,6 @@ alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) return vertex; } -namespace -{ - -// 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 record_layer(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 bool editor_mode::route_load(TTrack &Track) { @@ -2766,7 +1726,7 @@ void editor_mode::route_store() auto &route{m_route}; if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) return; - alignment_store::store(route.design, route.result, route.origin, record_layer(*route.chain.tracks.front())); + alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front())); route.departed = -1; route.departure = 0.0; } @@ -2841,7 +1801,7 @@ bool editor_mode::digitize_chain(editor_track::chain const &Chain) if (index == *covered.begin() && *end > *start) origin = old.origin - *start; } - auto const layer{covered.empty() ? record_layer(*Chain.tracks.front()) : records[*covered.begin()].layer}; + auto const layer{covered.empty() ? alignment_store::layer_of(*Chain.tracks.front()) : records[*covered.begin()].layer}; for (auto index{covered.rbegin()}; index != covered.rend(); ++index) alignment_store::forget(*index); alignment_store::store(design, result, origin, layer, "drawn"); @@ -5152,238 +4112,6 @@ glm::dvec3 editor_mode::cursor_ground() const return {ground.x, 0.0, ground.z}; } -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 found; - auto ground{ground_heights({glm::dvec3{point.x, std::max(point.y, static_cast(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(Global.pCamera.Pos.y))}; - for (double const radius : {10.0, 25.0, 50.0, 100.0, 200.0, 400.0}) - { - std::vector probes; - for (int i = 0; i < 12; ++i) - { - auto const angle{glm::two_pi() * i / 12.0}; - probes.push_back({Point.x + radius * std::cos(angle), ceiling, Point.z + radius * std::sin(angle)}); - } - std::vector 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::quiet_NaN(); -} - -std::pair editor_mode::trackbed(TTrack const *Style) -{ - return Style != nullptr ? profile_ballast(*Style) : std::pair{0.2, 0.5 * 1.435 + 0.5 + 1.1}; -} - -std::vector editor_mode::bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const -{ - auto const [depth, half]{trackbed(Style)}; - std::vector 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 found; - auto const heights{ground_heights(probes, true, &found)}; - std::vector result(Points.size(), std::numeric_limits::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::quiet_NaN()}; - auto toe{std::numeric_limits::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 &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const -{ - double constexpr step{2.0}; - double constexpr tolerance{0.1}; - double constexpr spacing{10.0}; - std::vector lengths; - double total{0.0}; - for (auto const &piece : Pieces) - total += lengths.emplace_back(bezier{piece}.plan_length(256)); - if (total < 1e-3) - return; - std::vector chainages; - std::vector points; - std::vector directions; - 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(std::ceil(lengths[i] / step)))}; - 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 = Height0 + (Height1 - Height0) * chainage / total + 50.0; - chainages.push_back(chainage); - points.push_back(point); - directions.push_back(plan_of(curve.first(t))); - } - reached += lengths[i]; - } - auto const targets{bed_heights(points, directions, Style)}; - std::vector knots{0.0, total}; - std::vector heights{Height0, Height1}; - auto const slopes = [&]() { - auto const last{knots.size() - 1}; - std::vector result(knots.size()); - 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}; - result[i] = (heights[b] - heights[a]) / (knots[b] - knots[a]); - } - if (false == std::isnan(Grade0)) - result.front() = Grade0; - if (false == std::isnan(Grade1)) - result.back() = Grade1; - return result; - }; - auto slope{slopes()}; - auto const at = [&](double const Chainage, double &Grade) { - auto const next{static_cast(std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin())}; - auto const i{std::clamp(next, 1, knots.size() - 1) - 1}; - auto const length{knots[i + 1] - knots[i]}; - return hermite(length, heights[i], slope[i], heights[i + 1], slope[i + 1], std::clamp((Chainage - knots[i]) / length, 0.0, 1.0), Grade); - }; - for (std::size_t pass = 0; pass < chainages.size(); ++pass) - { - auto worst{tolerance}; - auto pick{chainages.size()}; - for (std::size_t j = 0; j < chainages.size(); ++j) - { - if (std::isnan(targets[j])) - continue; - double grade; - auto const miss{std::abs(at(chainages[j], grade) - targets[j])}; - if (miss <= worst) - continue; - auto const next{std::upper_bound(knots.begin(), knots.end(), chainages[j])}; - if (next == knots.end() || *next - chainages[j] < spacing || chainages[j] - *std::prev(next) < spacing) - continue; - worst = miss; - pick = j; - } - if (pick == chainages.size()) - break; - auto const where{std::upper_bound(knots.begin(), knots.end(), chainages[pick]) - knots.begin()}; - knots.insert(knots.begin() + where, chainages[pick]); - heights.insert(heights.begin() + where, targets[pick]); - slope = slopes(); - } - std::vector result; - std::vector> spans; - 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; - } - for (std::size_t i = 0; i < result.size(); ++i) - { - auto &points{result[i].points}; - auto const [a, b]{spans[i]}; - auto const length{b - a}; - double begin, end; - 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 = at(a, begin); - finish.y = at(b, end); - if (control1 != glm::dvec3{} || control2 != glm::dvec3{}) - { - control1.y = begin * glm::length(plan_of(control1)); - control2.y = -end * glm::length(plan_of(control2)); - continue; - } - 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)) - continue; - 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}; - } - Pieces = std::move(result); -} - -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}; -} - glm::dvec3 editor_mode::cursor_level(double const Height) const { if (m_cursor_override.has_value()) @@ -5718,10 +4446,10 @@ std::vector editor_mode::join_pieces(TTrack const *From, editor_tr alignment::join_request request; request.start.position = Start; request.start.direction = Direction; - request.start.roll = From != nullptr ? -joint_roll(*From, Frompoint) : 0.0; + request.start.roll = From != nullptr ? -editor_track::joint_roll(*From, Frompoint) : 0.0; request.end.position = End.position; request.end.direction = plan_of(End.direction); - request.end.roll = End.track != nullptr ? joint_roll(*End.track, End.point) : 0.0; + request.end.roll = End.track != nullptr ? editor_track::joint_roll(*End.track, End.point) : 0.0; request.radius = m_join_radius; request.variant = m_join_variant; request.transitions = m_lay.transitions; @@ -5759,7 +4487,7 @@ std::vector editor_mode::join_pieces(TTrack const *From, editor_tr std::vector pieces{in.pieces}; pieces.insert(pieces.end(), eased.pieces.begin(), eased.pieces.end()); for (auto piece{out.pieces.rbegin()}; piece != out.pieces.rend(); ++piece) - pieces.push_back(turned_around(*piece)); + pieces.push_back(reversed(*piece)); eased.pieces = std::move(pieces); eased.length += (in.pieces.empty() ? 0.0 : in.length) + (out.pieces.empty() ? 0.0 : out.length); result = std::move(eased); @@ -5985,7 +4713,7 @@ std::vector editor_mode::lay_pieces(std::vector const { auto const back{layout_pieces(End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) - pieces.push_back(turned_around(*piece)); + pieces.push_back(reversed(*piece)); follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); } for (auto const &piece : pieces) @@ -6054,7 +4782,7 @@ std::vector editor_mode::lay_pieces(std::vector const auto const middle{alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign))}; result.insert(result.end(), middle.begin(), middle.end()); for (auto piece{out.pieces.rbegin()}; piece != out.pieces.rend(); ++piece) - result.push_back(turned_around(*piece)); + result.push_back(reversed(*piece)); Length = easedshape.length + (in.pieces.empty() ? 0.0 : in.length) + (out.pieces.empty() ? 0.0 : out.length); Error = easedshape.warnings.empty() ? std::string{} : easedshape.warnings.front(); return result; @@ -6437,140 +5165,6 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const switch_placed(placed); } -namespace -{ - -void place_record(switch_store::record &Entry, TTrack const &First) -{ - auto const &main{First.m_paths.front()}; - bezier const curve{main}; - Entry.start = main.points[segment_data::point::start]; - auto const direction{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); -} - -} // namespace - -// 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; - switch_store::record entry; - if (Shape.source != nullptr) - { - 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"; - } - } - else - { - 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"; - } - entry.kind = "switch"; - entry.name = Track.name(); - entry.parts = {entry.name}; - place_record(entry, Track); - bezier const main{Track.m_paths[0]}; - auto const &diverging{Track.m_paths[1]}; - entry.hand = cross(entry.direction, plan_of(diverging.points[segment_data::point::end] - entry.start)) > 0.0 ? 'R' : 'L'; - auto const first{plan_of(main.first(0.0))}; - auto const last{plan_of(main.first(1.0))}; - auto const turn{glm::length(first) > 1e-12 && glm::length(last) > 1e-12 ? 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"; - switch_store::put(entry, record_layer(Track)); -} - -void editor_mode::record_slip(std::vector 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 *first{simulation::Paths.find(entry.parts.front())}; - if (first == nullptr || first->m_paths.empty()) - return; - place_record(entry, *first); - switch_store::put(entry, record_layer(*first)); -} - -void editor_mode::render_switch_record(TTrack &Track) -{ - 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")); - bool changed{false}; - char number[32]; - std::snprintf(number, sizeof(number), "%s", entry.number == "-" ? "" : entry.number.c_str()); - ImGui::PushItemWidth(100.0f); - if (ImGui::InputText(STR_C("Number##switchnumber"), number, sizeof(number), ImGuiInputTextFlags_EnterReturnsTrue) || (ImGui::IsItemDeactivatedAfterEdit())) - { - entry.number = number[0] != '\0' ? number : "-"; - changed = true; - } - item_tooltip("Number of the switch in the station, as the plans of the station give it"); - ImGui::SameLine(); - char rail[16]; - std::snprintf(rail, sizeof(rail), "%s", entry.rail == "-" ? "" : entry.rail.c_str()); - if (ImGui::InputText(STR_C("Rail##switchrail"), rail, sizeof(rail), ImGuiInputTextFlags_EnterReturnsTrue) || (ImGui::IsItemDeactivatedAfterEdit())) - { - entry.rail = rail[0] != '\0' ? rail : "-"; - changed = true; - } - 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(); - } -} void editor_mode::switch_placed(std::vector const &Created) { @@ -6603,44 +5197,6 @@ void editor_mode::advance_switch_name() std::snprintf(m_switch.name, sizeof(m_switch.name), "%s", name.c_str()); } -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 side = [](std::string const &Side) { return Side == "left" ? 0 : Side == "right" ? 1 : -1; }; - auto const hand{side(info.switch_hand)}; - auto const 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; })}; - tool.drive = tool.drives.empty() ? -1 : preferred != tool.drives.end() ? static_cast(preferred - tool.drives.begin()) : 0; -} - std::string editor_mode::switch_name_for_new() const { if (m_switch.name[0] != '\0') @@ -6653,243 +5209,6 @@ std::string editor_mode::switch_name_for_new() const } } -bool editor_mode::switch_drive_spot(TTrack const &Switch, glm::dvec3 &Location, double &Yaw, glm::dvec2 &Tilt, bool &Rightswitch) const -{ - if (Switch.eType != tt_Switch || Switch.m_paths.size() < 2) - return false; - 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 false; - heading = glm::normalize(heading); - glm::dvec2 const right{-heading.y, heading.x}; - Rightswitch = glm::dot(plan_of(diverging.points[segment_data::point::end] - start), right) > 0.0; - Location = start; - auto const yaw{glm::degrees(std::atan2(heading.x, heading.y))}; - Yaw = yaw < 0.0 ? yaw + 360.0 : yaw; - auto const run{glm::length(plan_of(along))}; - auto const roll{static_cast(main.rolls[0])}; - Tilt = {-glm::degrees(std::atan(along.y / run)), roll}; - if (Global.bRollFix) - Location.y += 0.75 * std::abs(std::sin(glm::radians(roll))); - return true; -} - -void editor_mode::switch_drive_captured(TTrack const &Track) -{ - if (Track.eType != tt_Switch || m_infra_suspended) - return; - standing_template drive; - int family{-1}; - int variant{0}; - if (false == find_switch_drive(Track, drive, family, variant)) - return; - if (std::any_of(m_drive_buffer.begin(), m_drive_buffer.end(), [&](auto const &Entry) { return Entry.first == drive.instance; })) - return; - m_drive_buffer.emplace_back(drive.instance, scene::Layers.directive(drive.instance)); -} - -void editor_mode::switch_drives_committed(std::vector const &Tracks) -{ - if (m_infra_suspended) - return; - for (auto *track : Tracks) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch) - continue; - standing_template drive; - int family{-1}; - int variant{0}; - glm::dvec3 location; - double yaw; - glm::dvec2 tilt; - bool rightswitch; - if (false == find_switch_drive(*track, drive, family, variant) || false == switch_drive_spot(*track, location, yaw, tilt, rightswitch)) - continue; - 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; - location.y += m_switch.drives[family].heights[variant]; - auto const local{location - included.context.offset}; - auto values{drive.values}; - for (auto const ¶meter : info.parameters) - { - if (parameter.id < 1 || parameter.id > static_cast(values.size())) - continue; - auto &value{values[parameter.id - 1]}; - if (parameter.role == "pos.x") - value = editor_includes::number(local.x); - else if (parameter.role == "pos.y") - value = editor_includes::number(local.y); - else if (parameter.role == "pos.z") - value = editor_includes::number(local.z); - else if (parameter.role == "rot.y") - value = editor_includes::number(yaw); - else if (parameter.role == "rot.x") - value = editor_includes::number(tilt.x); - else if (parameter.role == "rot.z") - value = editor_includes::number(tilt.y); - } - auto const directive{editor_includes::compose_directive(*included.file, values)}; - if (directive != scene::Layers.directive(drive.instance)) - set_include_directive(drive.instance, directive); - } -} - -bool editor_mode::place_switch_drive(TTrack &Switch) -{ - auto &tool{m_switch}; - scan_switch_drives(); - if (tool.drive < 0 || tool.drive >= static_cast(tool.drives.size()) || Switch.eType != tt_Switch || Switch.m_paths.size() < 2) - return false; - glm::dvec3 location; - double yaw; - glm::dvec2 tilt; - bool rightswitch; - if (false == switch_drive_spot(Switch, location, yaw, tilt, rightswitch)) - return false; - bool const driveright{(tool.drive_side == 0) == rightswitch}; - auto const &family{tool.drives[tool.drive]}; - auto const index{(rightswitch ? 2 : 0) + (driveright ? 1 : 0)}; - template_item item; - item.file = family.files[index]; - if (item.file.empty()) - { - tool.status = format(STR_C("The drive %s has no variant for a %s switch with the drive on the %s"), family.name.c_str(), rightswitch ? STR_C("right") : STR_C("left"), driveright ? STR_C("right") : STR_C("left")); - return false; - } - item.location = location + glm::dvec3{0.0, family.heights[index], 0.0}; - item.yaw = yaw; - item.described = true; - item.track = Switch.name(); - item.tilt = tilt; - standing_template standing; - int present{-1}; - int variant{0}; - if (find_switch_drive(Switch, standing, present, variant)) - return replace_switch_drive(Switch, tool.drive, (variant & 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; -} - -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(std::tolower(Character)); }); - return Text; -} -} // namespace - -bool editor_mode::find_switch_drive(TTrack const &Switch, standing_template &Drive, int &Family, int &Index) -{ - auto &tool{m_switch}; - scan_switch_drives(); - auto const &plain{drive_file}; - std::map> variants; - for (int family = 0; family < static_cast(tool.drives.size()); ++family) - for (int index = 0; index < 4; ++index) - if (false == tool.drives[family].files[index].empty()) - variants[plain(tool.drives[family].files[index])] = {family, index}; - auto const name{plain(Switch.name())}; - auto const standing{standing_templates([&](std::string const &File) { return variants.count(File) > 0; })}; - auto const found{std::find_if(standing.begin(), standing.end(), [&](standing_template const &Standing) { return plain(Standing.values[0]) == name; })}; - if (found == standing.end()) - return false; - Drive = *found; - std::tie(Family, Index) = variants[found->file]; - return true; -} - -void editor_mode::apply_switch_drive_choice(TTrack &Switch) -{ - if (m_switch.drive >= 0) - { - place_switch_drive(Switch); - return; - } - standing_template found; - int family{-1}; - int variant{0}; - if (find_switch_drive(Switch, found, family, variant) && remove_templates({found.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}; - standing_template found; - int family{-1}; - int index{0}; - if (false == find_switch_drive(Switch, found, family, index)) - { - tool.status = format(STR_C("No drive of the switch %s among the described drives"), Switch.name().c_str()); - return false; - } - if (Family >= 0) - family = Family; - if (Flip) - index ^= 1; - 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()) : format(STR_C("The drive %s has no variant for a %s switch with the drive on the %s"), tool.drives[family].name.c_str(), index & 2 ? STR_C("right") : STR_C("left"), index & 1 ? STR_C("right") : STR_C("left")); - return false; - } - if (drive_file(item.file) == found.file) - return false; - glm::dvec3 location; - double yaw; - glm::dvec2 tilt; - bool rightswitch; - item.location = switch_drive_spot(Switch, location, yaw, tilt, rightswitch) ? 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(); - auto const angle = [&](std::size_t const Index) { - double value{0.0}; - if (Index < found.values.size()) - std::sscanf(found.values[Index].c_str(), "%lf", &value); - return value; - }; - item.tilt = {angle(5), angle(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; -} - void editor_mode::join_last_steps() { if (m_history.size() < 2) @@ -6900,40 +5219,6 @@ void editor_mode::join_last_steps() previous.instances.insert(previous.instances.end(), last.instances.begin(), last.instances.end()); } -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(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 + and - 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; -} - bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style) { auto const from{From}; @@ -7022,334 +5307,6 @@ bool editor_mode::cut_straight(editor_track::straight const &Line, double const return true; } -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) - return false; - auto const &main{Track.m_paths.front()}; - if (main.points[segment_data::point::control1] != glm::dvec3{} || main.points[segment_data::point::control2] != glm::dvec3{}) - return false; - 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 false; - auto const direction{axis / length}; - auto const line{editor_track::find_straight(Track, m_straights.tolerance)}; - if (false == tracks_editable(line.tracks, Reason)) - return false; - std::vector> 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{glm::dot(plan_of(path.points[segment_data::point::start] - origin), direction)}; - auto const b{glm::dot(plan_of(path.points[segment_data::point::end] - origin), direction)}; - 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}, high{length}; - for (auto const &[from, to, plain] : spans) - if (from > 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::move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status) -{ - if (Paths.size() < 2 || Track.m_paths.size() < 2) - return false; - auto const old{Track.m_paths}; - auto const &main{old.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 false; - auto const direction{axis / length}; - auto const along = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - origin), direction); }; - auto const lateral = [&](glm::dvec3 const &Point) { return std::abs(cross(direction, plan_of(Point - origin))); }; - double low{0.0}, 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; - } - struct end_move - { - glm::dvec3 from; - glm::dvec3 to; - double shift; - }; - std::vector ends; - for (auto const index : {segment_data::point::start, segment_data::point::end}) - { - auto const from{main.points[index]}; - auto const to{Paths.front().points[index]}; - if (false == online || glm::distance(from, to) < 0.005 || lateral(to) > m_straights.tolerance.offset) - continue; - auto const target{along(to)}; - if (target < low - 0.01 || target > high + 0.01) - { - Status = STR_C("The switch would run into another switch or past the end of the straight"); - return false; - } - ends.push_back({from, to, index == segment_data::point::start ? along(from) - target : target - along(from)}); - } - std::stable_sort(ends.begin(), ends.end(), [](end_move const &A, end_move const &B) { return A.shift > B.shift; }); - auto const joints{editor_track::joints(Track)}; - std::vector positions; - for (auto const &joint : joints) - positions.push_back(editor_track::point_position(Track, joint.own)); - std::vector handled(joints.size(), false); - std::vector> states{{&Track, editor_track::capture(Track)}}; - std::vector created; - std::vector removed; - std::vector changed{&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 (end.shift > 0.0) - { - auto const a{line.along(end.from)}; - auto const b{line.along(end.to)}; - if (false == cut_straight(line, std::min(a, b), std::max(a, b), states, created, removed, nullptr)) - { - Status = STR_C("The switch would overlap another switch, move it onto plain track"); - return false; - } - continue; - } - TTrack const *style{nullptr}; - bool filler{false}; - for (auto const &connection : editor_track::connected_points(Track, end.from)) - { - auto *other{connection.first}; - if (other->m_editorremoved) - continue; - if (other->eType == tt_Normal && std::find(line.tracks.begin(), line.tracks.end(), other) != line.tracks.end()) - { - if (std::find(changed.begin(), changed.end(), other) == changed.end()) - { - states.emplace_back(other, editor_track::capture(*other)); - changed.push_back(other); - } - editor_track::move_point(*other, connection.second, end.to); - continue; - } - filler = true; - if (style == nullptr && other->eType == tt_Normal) - style = other; - } - if (filler) - { - segment_data path; - path.points[segment_data::point::start] = end.to; - path.points[segment_data::point::end] = end.from; - auto *piece{editor_track::create_path(style != nullptr ? *style : Track, path)}; - created.push_back(piece); - changed.push_back(piece); - } - } - Track.m_paths.assign(Paths.begin(), Paths.begin() + 2); - std::vector 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() ? "" : ", ") + (joint.other->name().empty() ? std::string{"(noname)"} : joint.other->name()); - continue; - } - following.push_back(joint); - if (std::find(changed.begin(), changed.end(), joint.other) == changed.end()) - { - changed.push_back(joint.other); - states.emplace_back(joint.other, editor_track::capture(*joint.other)); - } - } - editor_track::follow(Track, following); - editor_track::commit(changed); - push_track_snapshot(std::move(states), std::move(created), std::move(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 &main{Track.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 false; - auto const shift{run * (Distance / planlength)}; - auto paths{Track.m_paths}; - for (auto &path : paths) - { - path.points[segment_data::point::start] += shift; - path.points[segment_data::point::end] += shift; - } - 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 && Track.m_paths.front().points[segment_data::point::control1] == glm::dvec3{} && Track.m_paths.front().points[segment_data::point::control2] == glm::dvec3{}; -} - -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 &main{Track.m_paths.front()}; - auto const origin{main.points[segment_data::point::start]}; - auto const direction{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))}; - auto const &mouse{ImGui::GetIO().MousePos}; - slide.active = true; - slide.track = &Track; - slide.pressed = {mouse.x, mouse.y}; - slide.grip = glm::dot(plan_of(cursor_level(origin.y) - origin), direction); - 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(glm::dot(plan_of(path.points[index] - origin), direction)); - } -} - -void editor_mode::switch_slide_update() -{ - auto &slide{m_switch.slide}; - if (false == slide.active) - return; - if (slide.track == nullptr || slide.track->m_editorremoved || slide.track->m_paths.size() < 2) - { - slide = {}; - return; - } - auto const &main{slide.track->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)}; - auto const direction{axis / length}; - auto distance{std::clamp(glm::dot(plan_of(cursor_level(origin.y) - origin), direction) - 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 &main{slide.track->m_paths.front()}; - auto const run{main.points[segment_data::point::end] - main.points[segment_data::point::start]}; - auto const shift{run * (slide.distance / std::max(1e-6, glm::length(plan_of(run))))}; - screen_projection const projection; - auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; - for (auto path : slide.track->m_paths) - { - path.points[segment_data::point::start] += shift; - path.points[segment_data::point::end] += shift; - 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{main.points[segment_data::point::start] + shift}; - 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()); -} - bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector> &States, std::vector &Created, std::vector &Removed, std::string const &Name) { auto const &shape{Shape}; @@ -7946,15 +5903,6 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g path.radius = static_cast(Radius); return path; }; - auto const 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; - }; auto const middle{Angle * 0.5}; auto const split{gap / Radius}; auto const centreA{at(Crossing - First * gap, Height)}; diff --git a/application/editormodetrackbed.cpp b/application/editormodetrackbed.cpp new file mode 100644 index 00000000..322bda3c --- /dev/null +++ b/application/editormodetrackbed.cpp @@ -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 +#include +#include + +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 found; + auto ground{ground_heights({glm::dvec3{point.x, std::max(point.y, static_cast(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(Global.pCamera.Pos.y))}; + for (double const radius : {10.0, 25.0, 50.0, 100.0, 200.0, 400.0}) + { + std::vector probes; + for (int i = 0; i < 12; ++i) + { + auto const angle{glm::two_pi() * i / 12.0}; + probes.push_back({Point.x + radius * std::cos(angle), ceiling, Point.z + radius * std::sin(angle)}); + } + std::vector 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::quiet_NaN(); +} + +std::pair editor_mode::trackbed(TTrack const *Style) +{ + return Style != nullptr ? profile_ballast(*Style) : std::pair{0.2, 0.5 * 1.435 + 0.5 + 1.1}; +} + +std::vector editor_mode::bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const +{ + auto const [depth, half]{trackbed(Style)}; + std::vector 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 found; + auto const heights{ground_heights(probes, true, &found)}; + std::vector result(Points.size(), std::numeric_limits::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::quiet_NaN()}; + auto toe{std::numeric_limits::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 &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 const &Points, std::vector 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}; +} diff --git a/application/editormodetrackgizmo.cpp b/application/editormodetrackgizmo.cpp new file mode 100644 index 00000000..9fb40a3b --- /dev/null +++ b/application/editormodetrackgizmo.cpp @@ -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 +#include +#include +#include + +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 &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 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(value), static_cast(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(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 const &Connection, std::function 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 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{}); + 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(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(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> 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())}; + 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(target - drag.roll)}; + if (std::abs(step) < 1e-6f) + return; + drag.roll = target; + for (int i = 0; i < static_cast(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(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 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(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 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(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); +} diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp index 43bff47c..2991a599 100644 --- a/application/editormodeturntable.cpp +++ b/application/editormodeturntable.cpp @@ -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) { diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp index 0542ca98..e9d87c61 100644 --- a/editor/editorAlignmentStore.cpp +++ b/editor/editorAlignmentStore.cpp @@ -745,4 +745,15 @@ void restore(std::vector 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 diff --git a/editor/editorAlignmentStore.hpp b/editor/editorAlignmentStore.hpp index 84e5368c..3ace09ff 100644 --- a/editor/editorAlignmentStore.hpp +++ b/editor/editorAlignmentStore.hpp @@ -101,5 +101,8 @@ std::optional> take_changes(); void settle(); // puts the records back as they were, for undo and redo void restore(std::vector 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 diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp index 2253f43f..83db47e5 100644 --- a/editor/editorGeometry.hpp +++ b/editor/editorGeometry.hpp @@ -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 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(From)), glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast(To))}; + return result; +} + +inline std::vector 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 diff --git a/editor/editorGroundFollow.cpp b/editor/editorGroundFollow.cpp new file mode 100644 index 00000000..cdd24a30 --- /dev/null +++ b/editor/editorGroundFollow.cpp @@ -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 +#include +#include + +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 chainages; + std::vector points; + std::vector directions; +}; + +samples sample(std::vector const &Pieces, std::vector 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(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 const &Chainages, std::vector 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 cut(std::vector const &Pieces, std::vector const &Lengths, std::vector const &Knots, std::vector> &Spans) +{ + std::vector 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 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::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin())}; + auto const i{std::clamp(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 &Pieces, ends const &Ends, bed_query const &Bed) +{ + std::vector 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> 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 diff --git a/editor/editorGroundFollow.hpp b/editor/editorGroundFollow.hpp new file mode 100644 index 00000000..9b532b35 --- /dev/null +++ b/editor/editorGroundFollow.hpp @@ -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 +#include +#include + +#include "world/Segment.h" + +namespace ground_follow +{ + +using bed_query = std::function(std::vector const &, std::vector const &)>; + +struct ends +{ + double height0{0.0}; + double grade0{0.0}; + double height1{0.0}; + double grade1{0.0}; +}; + +struct knot_line +{ + std::vector knots; + std::vector heights; + std::vector 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 &Pieces, ends const &Ends, bed_query const &Bed); + +} // namespace ground_follow diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index b780e785..131cbf07 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -363,4 +363,29 @@ void restore(std::vector 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 diff --git a/editor/editorSwitchStore.hpp b/editor/editorSwitchStore.hpp index 3cca9f3a..4edf98e8 100644 --- a/editor/editorSwitchStore.hpp +++ b/editor/editorSwitchStore.hpp @@ -60,6 +60,8 @@ std::vector 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(); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index b2069eaf..95d3eee9 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -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) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index f4d87b41..ff44f142 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -126,6 +126,60 @@ class editor_track static std::vector> partners(TTrack &Track, point_ref const &End); static void align_controls(TTrack const &Track, point_ref const &Control, std::vector> 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 const &Joints); + + struct path_end + { + int path{0}; + bool atend{true}; + }; + static std::optional stretch_end(TTrack const &Track, point_ref const &Point); + static std::vector stretched(segment_data const &Path, bool const Atend, double const Length); + static std::pair 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> states; + std::vector created; + std::vector removed; + std::vector 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_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 main_axis(TTrack const &Switch); + static std::vector slid(TTrack const &Switch, double const Distance); struct snap_target { diff --git a/editor/editorTrackShape.cpp b/editor/editorTrackShape.cpp new file mode 100644 index 00000000..2d1dbb15 --- /dev/null +++ b/editor/editorTrackShape.cpp @@ -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 +#include + +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 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(scale); + return {Atend ? piece : reversed(piece)}; +} + +void spread_rolls(std::vector &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(std::clamp(chainage / Original, 0.0, 1.0))}; + auto const to{static_cast(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 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 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::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(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 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(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(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(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 const &Joints) +{ + for (auto const &joint : Joints) + if (joint.own.path < static_cast(Track.m_paths.size()) && joint.theirs.path < static_cast(joint.other->m_paths.size())) + roll_joined(*joint.other, joint.theirs, static_cast(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_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(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::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 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); +}