From 8778353b221be8ec2eb70df4bfa676cdcb176836 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:14:03 +0200 Subject: [PATCH] feat(editor): infrastructure bound to the paths follows their changes --- CMakeLists.txt | 2 + application/editormode.cpp | 7 + application/editormode.h | 54 +- application/editormodeinfra.cpp | 1390 ++++++++++++++++++++++ application/editormodetrack.cpp | 20 + editor/editorInfra.cpp | 304 +++++ editor/editorInfra.hpp | 128 ++ editor/editorTrack.cpp | 13 + editor/editorTrack.hpp | 7 + scene/scene.cpp | 24 + scene/scene.h | 8 + scene/scenelayers.h | 5 + scene/scenelayerssave.cpp | 112 ++ simulation/simulationstateserializer.cpp | 2 + world/Event.h | 8 + 15 files changed, 2083 insertions(+), 1 deletion(-) create mode 100644 application/editormodeinfra.cpp create mode 100644 editor/editorInfra.cpp create mode 100644 editor/editorInfra.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8900a0be..94d71aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,11 +173,13 @@ set(SOURCES "editor/editorTrack.cpp" "editor/editorAlignment.cpp" "editor/editorProfile.cpp" +"editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" "application/editormodeprofile.cpp" +"application/editormodeinfra.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 3b83664d..c4cedb68 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -344,6 +344,7 @@ editor_mode::editor_mode() { m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) { gather_ground_triangles(Min, Max, true, 50.0f, Out); }); + infra_hooks(); } editor_ui *editor_mode::ui() const @@ -869,6 +870,9 @@ bool editor_mode::update() simulation::State.update_clocks(); simulation::Environment.update(); + // bindings are matched with the objects by their locations, before anything gets moved + if (false == m_bindings_loaded && simulation::is_ready) + infra_load(); auto const deltarealtime = Timer::GetDeltaRenderTime(); @@ -1023,6 +1027,8 @@ bool editor_mode::update() render_track_window(); draw_profile_overlay(); render_profile_window(); + draw_infra_overlay(); + render_infra_window(); } // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- @@ -1637,6 +1643,7 @@ void editor_mode::save() if (m_profile.changed) profile_store(); + infra_store(); auto const result = scene::Layers.save(rootstatements); if (result.success && false == rootstatements.empty()) { diff --git a/application/editormode.h b/application/editormode.h index b517736c..082682e4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTrack.hpp" #include "editor/editorAlignment.hpp" #include "editor/editorProfile.hpp" +#include "editor/editorInfra.hpp" #include #include @@ -107,7 +108,7 @@ class editor_mode : public application_mode std::vector> tracks; std::vector created; std::vector removed; - + std::vector infra; // objects which followed the paths }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -545,6 +546,57 @@ class editor_mode : public application_mode void render_profile_point(); void draw_profile_overlay() const; + // infrastructure along the track: objects bound to the paths follow their changes. bindings are kept in the + // scenery files as "//$b" comment lines + struct infra_candidate + { + infra::binding binding; + double offset{0.0}; // sideways from the axis + std::string reason; + bool chosen{false}; + bool bound{false}; + }; + struct infra_window + { + bool open{false}; + int scope{1}; // 0: the selected path, 1: the line through it, 2: the route of the vertical profile + float reach{1500.0f}; + float corridor{8.0f}; + bool follow{true}; + bool show{true}; + std::vector rules{infra::default_rules()}; + std::vector tracks; // of the scope + std::vector candidates; + int hovered{-1}; + std::string status; + std::string error; + }; + infra_window m_infra; + std::vector m_bindings; + std::vector> m_bindings_unresolved; // lines kept as they are + bool m_bindings_loaded{false}; + bool m_bindings_changed{false}; + std::vector m_infra_buffer; // states of the objects before the change in progress + bool m_infra_suspended{false}; + void infra_hooks(); + void infra_load(); + void infra_store(); + std::vector infra_scope() const; + void infra_recognize(); + void infra_bind_chosen(); + void infra_unbind(std::vector Indices); + infra::binding *infra_find(infra::binding const &Binding); + infra::object_state infra_state_of(infra::binding const &Binding) const; + void infra_restore_state(infra::object_state const &State); + void infra_buffer(infra::binding const &Binding, bool const Refresh); + void infra_capture(TTrack const &Track); + void infra_follow(std::vector const &Tracks); + void infra_split(TTrack &Original, TTrack &Created, segment_data const &First); + void infra_move(infra::binding &Binding); + void infra_attach(EditorSnapshot &Snapshot); + void render_infra_window(); + void draw_infra_overlay() const; + bool m_route_gizmo_using{false}; glm::mat4 m_route_gizmo{1.0f}; float m_track_snap_radius{1.0f}; diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp new file mode 100644 index 00000000..0fb987e5 --- /dev/null +++ b/application/editormodeinfra.cpp @@ -0,0 +1,1390 @@ +/* +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/editorprojection.h" + +#include "editor/editorIncludeInfo.hpp" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "world/Event.h" +#include "world/EvLaunch.h" +#include "world/MemCell.h" +#include "world/Track.h" +#include "world/Traction.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +std::string const kBindingMark{"//$b"}; + +template +std::string format(char const *Format, Args... Arguments) +{ + char text[256]; + std::snprintf(text, sizeof(text), Format, Arguments...); + return text; +} + +double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +template +bool contains(std::vector const &Items, Type_ const &Item) +{ + return std::find(Items.begin(), Items.end(), Item) != Items.end(); +} + +ImU32 colour(infra::category const Category, int const Alpha = 255) +{ + switch (Category) + { + case infra::category::signal: return IM_COL32(255, 80, 80, Alpha); + case infra::category::sign: return IM_COL32(255, 220, 60, Alpha); + case infra::category::pole: return IM_COL32(160, 160, 255, Alpha); + case infra::category::catenary: return IM_COL32(80, 200, 255, Alpha); + case infra::category::logic: return IM_COL32(220, 120, 255, Alpha); + case infra::category::other: return IM_COL32(200, 200, 200, Alpha); + } + return IM_COL32(200, 200, 200, Alpha); +} + +// nodes still present in the scene; the editor deletes models outright +class live_nodes +{ + public: + void refresh() + { + std::array const sizes{simulation::Instances.sequence().size(), simulation::Traction.sequence().size(), simulation::Memory.sequence().size(), simulation::Events.launchers().size()}; + if (sizes == m_sizes && m_valid) + return; + m_sizes = sizes; + m_valid = true; + m_nodes.clear(); + for (auto *node : simulation::Instances.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Traction.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Memory.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Events.launchers()) + m_nodes.insert(node); + } + bool alive(scene::basic_node const *Node) const + { + return Node != nullptr && m_nodes.count(Node) > 0; + } + + private: + std::unordered_set m_nodes; + std::array m_sizes{}; + bool m_valid{false}; +}; +live_nodes Live; + +bool alive(infra::binding const &Binding) +{ + if (Binding.type == infra::kind::include) + { + if (false == scene::Layers.tracked(Binding.include)) + return false; + auto const &included{scene::Layers.instance(Binding.include)}; + return false == included.removed && false == included.dead; + } + return Live.alive(Binding.node); +} + +// placement parameters of an include +struct include_place +{ + std::string file; + std::vector values; + std::array position{-1, -1, -1}; // indices of the values + int yaw{-1}; + glm::dvec3 offset{0.0}; + std::string description; // name and category of the template +}; + +bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} + +bool include_place_of(scene::instance_handle const Instance, include_place &Place) +{ + if (false == scene::Layers.tracked(Instance)) + return false; + auto const &included{scene::Layers.instance(Instance)}; + if (included.removed || included.dead || included.file == nullptr) + return false; + // with a rotation or a scale in effect the parameters don't translate to a location in a single way + if (included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f}) + return false; + if (false == editor_includes::parse_directive(scene::Layers.directive(Instance), Place.file, Place.values)) + return false; + static std::map descriptions; + auto found{descriptions.find(*included.file)}; + if (found == descriptions.end()) + { + include_info info; + std::string error; + editor_includes::load(*included.file, info, error); + editor_includes::suggest(*included.file, info); + found = descriptions.emplace(*included.file, std::move(info)).first; + } + auto const &info{found->second}; + auto const index = [&](char const *Role) { + auto const result{editor_includes::parameter_with_role(info, Role) - 1}; + double value; + return (result >= 0 && result < static_cast(Place.values.size()) && numeric(Place.values[result], value)) ? result : -1; + }; + Place.position = {index("pos.x"), index("pos.y"), index("pos.z")}; + Place.yaw = index("rot.y"); + Place.offset = included.context.offset; + Place.description = info.name + ' ' + info.category; + return Place.position[0] >= 0 && Place.position[1] >= 0 && Place.position[2] >= 0; +} + +glm::dvec3 include_location(include_place const &Place) +{ + glm::dvec3 result{Place.offset}; + for (int axis = 0; axis < 3; ++axis) + { + double value{0.0}; + numeric(Place.values[Place.position[axis]], value); + result[axis] += value; + } + return result; +} + +// points which define the object, and its rotation around the vertical axis if it has one +bool object_points(infra::binding const &Binding, std::vector &Points, std::optional &Yaw) +{ + Points.clear(); + Yaw.reset(); + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + Points.push_back(model->location()); + Yaw = model->Angles().y; + return true; + } + case infra::kind::include: + { + include_place place; + if (false == include_place_of(Binding.include, place)) + return false; + Points.push_back(include_location(place)); + if (place.yaw >= 0) + { + double value{0.0}; + numeric(place.values[place.yaw], value); + Yaw = value; + } + return true; + } + case infra::kind::traction: + { + auto const *traction{static_cast(Binding.node)}; + Points = {traction->pPoint1, traction->pPoint2, traction->pPoint3, traction->pPoint4}; + return true; + } + case infra::kind::memcell: + case infra::kind::launcher: Points.push_back(Binding.node->location()); return true; + } + return false; +} + +std::string object_key(infra::binding const &Binding) +{ + std::string result; + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + result = model->Model() != nullptr ? model->Model()->NameGet() : std::string{}; + break; + } + case infra::kind::include: result = *scene::Layers.instance(Binding.include).file; break; + case infra::kind::traction: break; + case infra::kind::memcell: + case infra::kind::launcher: result = Binding.node->name(); break; + } + if (result.empty() || result.find_first_of(" \t\r\n") != std::string::npos) + result = "-"; + return result; +} + +std::string object_label(infra::binding const &Binding) +{ + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + auto name{model->Model() != nullptr ? model->Model()->NameGet() : std::string{}}; + if (false == model->name().empty() && model->name() != "none") + name = model->name() + " (" + name + ')'; + return name; + } + case infra::kind::include: return *scene::Layers.instance(Binding.include).file; + case infra::kind::traction: return Binding.node->name().empty() || Binding.node->name() == "none" ? std::string{"traction"} : Binding.node->name(); + case infra::kind::memcell: return "memcell " + Binding.node->name(); + case infra::kind::launcher: return "eventlauncher " + Binding.node->name(); + } + return {}; +} + +void move_model(TAnimModel *Model, glm::dvec3 const &Location, glm::vec3 const &Angles) +{ + if (glm::distance(Model->location(), Location) > 1e-6) + { + simulation::Region->erase(Model); + Model->location(Location); + simulation::Region->insert(Model); + } + Model->Angles(Angles); +} + +void move_traction(TTraction *Traction, std::array const &Points) +{ + if (Traction->pPoint1 == Points[0] && Traction->pPoint2 == Points[1] && Traction->pPoint3 == Points[2] && Traction->pPoint4 == Points[3]) + return; + // the height of the lowest dropper stays the same + auto const minheight{(Traction->pPoint3.y - Traction->pPoint1.y + Traction->pPoint4.y - Traction->pPoint2.y) * 0.5 - Traction->fHeightDifference}; + simulation::Region->erase_and_unregister(Traction); + Traction->pPoint1 = Points[0]; + Traction->pPoint2 = Points[1]; + Traction->pPoint3 = Points[2]; + Traction->pPoint4 = Points[3]; + Traction->fHeightDifference = (Points[2].y - Points[0].y + Points[3].y - Points[1].y) * 0.5 - minheight; + Traction->Init(); + Traction->location(glm::mix(Points[1], Points[0], 0.5)); + Traction->m_area.radius = -1.f; + simulation::Region->insert_and_register(Traction); + Traction->m_geometry = gfx::geometry_handle{}; + auto §ion{simulation::Region->section(Traction->location())}; + if (section.m_geometrycreated) + Traction->create_geometry(section.m_geometrybank); + Traction->mark_dirty(); +} + +void move_memcell(TMemCell *Memcell, glm::dvec3 const &Location) +{ + if (glm::distance(Memcell->location(), Location) <= 1e-6) + return; + simulation::Region->erase(Memcell); + Memcell->location(Location); + simulation::Region->insert(Memcell); +} + +void move_launcher(TEventLauncher *Launcher, glm::dvec3 const &Location) +{ + if (glm::distance(Launcher->location(), Location) <= 1e-6) + return; + // global and radio launchers aren't a part of the region + auto const placed{false == Launcher->IsGlobal() && false == Launcher->IsRadioActivated()}; + if (placed) + simulation::Region->erase(Launcher); + Launcher->location(Location); + if (placed) + simulation::Region->insert(Launcher); + Launcher->mark_dirty(); +} + +// bounding circle of the paths of a track, in the plan +struct track_area +{ + TTrack *track{nullptr}; + glm::dvec3 centre{0.0}; + double radius{0.0}; +}; + +track_area area_of(TTrack &Track) +{ + track_area result; + result.track = &Track; + std::vector points; + for (auto const &path : Track.m_paths) + { + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + points.insert(points.end(), {start, end, start + path.points[segment_data::point::control1], end + path.points[segment_data::point::control2]}); + } + if (points.empty()) + return result; + for (auto const &point : points) + result.centre += point / static_cast(points.size()); + for (auto const &point : points) + result.radius = std::max(result.radius, plan_distance(point, result.centre)); + return result; +} + +// the point lies sideways of the path, not beyond one of its ends +bool beside(infra::station const &Station, glm::dvec3 const &Point, bool const Interior) +{ + if (Interior) + return true; + auto const axis{infra::frame_at(Station)}; + return std::abs(glm::dot(glm::dvec2{Point.x - axis.point.x, Point.z - axis.point.z}, axis.forward)) <= 0.5; +} + +// nearest point of the tracks, sideways from the axis within the corridor +template +bool nearest_beside(Areas_ const &Areas, glm::dvec3 const &Point, double const Corridor, infra::station &Station) +{ + auto best{-1.0}; + for (auto const &entry : Areas) + { + track_area const &area{entry}; + if (plan_distance(Point, area.centre) > area.radius + Corridor) + continue; + infra::station station; + bool interior; + auto const distance{infra::project(*area.track, Point, station, interior)}; + if (distance <= Corridor && (best < 0.0 || distance < best) && beside(station, Point, interior)) + { + best = distance; + Station = station; + } + } + return best >= 0.0; +} + +bool station_near(std::vector const &Areas, glm::dvec3 const &Point, double const Corridor, infra::station &Station) +{ + return nearest_beside(Areas, Point, Corridor, Station); +} + +// paths of the whole scenery by cells of the plan, for the lookups reaching up to kReach from the axis +class track_index +{ + public: + static constexpr double kReach{55.0}; + void add(TTrack &Track) + { + auto const index{m_areas.size()}; + m_areas.push_back(area_of(Track)); + std::set cells; + for (auto const &path : Track.m_paths) + for (auto const &point : infra::outline(path, kCell * 0.5)) + for (auto x = key(point.x - kReach - kCell * 0.25); x <= key(point.x + kReach + kCell * 0.25); ++x) + for (auto z = key(point.z - kReach - kCell * 0.25); z <= key(point.z + kReach + kCell * 0.25); ++z) + cells.insert({x, z}); + for (auto const &cell : cells) + m_cells[cell].push_back(index); + } + bool nearest(glm::dvec3 const &Point, double const Corridor, infra::station &Station) const + { + auto const cell{m_cells.find({key(Point.x), key(Point.z)})}; + if (cell == m_cells.end()) + return false; + std::vector> areas; + for (auto const index : cell->second) + areas.emplace_back(m_areas[index]); + return nearest_beside(areas, Point, std::min(Corridor, kReach), Station); + } + bool empty() const + { + return m_areas.empty(); + } + + private: + static constexpr double kCell{64.0}; + using cell_key = std::pair; + static long long key(double const Value) + { + return static_cast(std::floor(Value / kCell)); + } + std::vector m_areas; + std::map> m_cells; +}; + +// lookup of points by a grid of 1 m cells in the plan +template +class point_grid +{ + public: + void add(glm::dvec3 const &Point, Type_ Item) + { + m_cells[key(Point)].emplace_back(Point, Item); + } + template + std::optional find(glm::dvec3 const &Point, double const Tolerance, Match_ const &Match) const + { + auto const centre{key(Point)}; + for (auto x = centre.first - 1; x <= centre.first + 1; ++x) + for (auto z = centre.second - 1; z <= centre.second + 1; ++z) + { + auto const cell{m_cells.find({x, z})}; + if (cell == m_cells.end()) + continue; + for (auto const &entry : cell->second) + if (glm::distance(entry.first, Point) <= Tolerance && Match(entry.second)) + return entry.second; + } + return std::nullopt; + } + + private: + using cell_key = std::pair; + static cell_key key(glm::dvec3 const &Point) + { + return {static_cast(std::floor(Point.x)), static_cast(std::floor(Point.z))}; + } + std::map>> m_cells; +}; + +double turn_of(infra::binding const &Binding, double const Yaw) +{ + auto const axis{infra::frame_at(Binding.anchors.front().at)}; + return clamp_circular(Yaw - infra::heading(axis.forward)); +} + +} // namespace + +void editor_mode::infra_hooks() +{ + editor_track::on_capture = [this](TTrack const &Track) { infra_capture(Track); }; + editor_track::on_commit = [this](std::vector const &Tracks) { infra_follow(Tracks); }; + editor_track::on_retire = [this](TTrack &) { infra_follow({}); }; + editor_track::on_split = [this](TTrack &Original, TTrack &Created, segment_data const &First) { infra_split(Original, Created, First); }; +} + +// format of the lines, coordinates in the space of the file: +// { } +// the object is the model, the include (position from its parameters), the traction piece (its first point), the memory +// cell or the event launcher found at the location; the anchors name the paths by their ends +void editor_mode::infra_load() +{ + if (m_bindings_loaded || scene::Layers.empty()) + return; + m_bindings_loaded = true; + auto const marked{scene::Layers.marked_layers(kBindingMark)}; + if (marked.empty()) + return; + + point_grid paths; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (auto const &path : track->m_paths) + paths.add(path.points[segment_data::point::start], track); + point_grid models; + for (auto *model : simulation::Instances.sequence()) + if (model != nullptr && false == model->from_template() && model->Model() != nullptr) + models.add(model->location(), model); + point_grid traction; + for (auto *piece : simulation::Traction.sequence()) + if (piece != nullptr && false == piece->from_template()) + traction.add(piece->pPoint1, piece); + point_grid logic; + for (auto *memcell : simulation::Memory.sequence()) + if (memcell != nullptr && false == memcell->from_template()) + logic.add(memcell->location(), memcell); + for (auto *launcher : simulation::Events.launchers()) + if (launcher != nullptr && false == launcher->from_template()) + logic.add(launcher->location(), launcher); + point_grid includes; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (include_place_of(handle, place)) + includes.add(include_location(place), handle); + } + + double const tolerance{0.05}; + auto resolved{0}; + for (auto const layer : marked) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + for (auto const &text : scene::Layers.marked(layer, kBindingMark)) + { + if (text.empty() || text.front() == '#') + continue; + std::istringstream line{text}; + std::string kindname, key; + int group{0}, turns{0}, count{0}; + glm::dvec3 location; + line >> kindname >> group >> location.x >> location.y >> location.z >> key >> turns >> count; + auto const keep = [&]() { m_bindings_unresolved.emplace_back(layer, text); }; + if (line.fail() || count < 1 || count > 4) + { + keep(); + continue; + } + location = context.to_world(location); + infra::binding binding; + binding.group = static_cast(std::clamp(group, 0, static_cast(infra::category::other))); + binding.turns = (turns != 0); + auto found{false}; + if (kindname == infra::name(infra::kind::model)) + { + binding.type = infra::kind::model; + auto const model{models.find(location, tolerance, [&](TAnimModel *Model) { return Model->Model()->NameGet() == key; })}; + if ((found = model.has_value())) + binding.node = *model; + } + else if (kindname == infra::name(infra::kind::include)) + { + binding.type = infra::kind::include; + auto const handle{includes.find(location, tolerance, [&](scene::instance_handle Handle) { return *scene::Layers.instance(Handle).file == key; })}; + if ((found = handle.has_value())) + binding.include = *handle; + } + else if (kindname == infra::name(infra::kind::traction)) + { + binding.type = infra::kind::traction; + auto const piece{traction.find(location, tolerance, [](TTraction *) { return true; })}; + if ((found = piece.has_value())) + binding.node = *piece; + } + else if (kindname == infra::name(infra::kind::memcell) || kindname == infra::name(infra::kind::launcher)) + { + binding.type = (kindname == infra::name(infra::kind::memcell) ? infra::kind::memcell : infra::kind::launcher); + auto const node{logic.find(location, tolerance, [&](scene::basic_node *Node) { + auto const ismemcell{dynamic_cast(Node) != nullptr}; + return ismemcell == (binding.type == infra::kind::memcell) && (key == "-" || Node->name() == key); + })}; + if ((found = node.has_value())) + binding.node = *node; + } + std::vector points; + std::optional yaw; + if (false == found || false == object_points(binding, points, yaw)) + { + keep(); + continue; + } + for (int i = 0; i < count && found; ++i) + { + int path{0}; + glm::dvec3 start, end; + line >> path >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z; + start = context.to_world(start); + end = context.to_world(end); + auto const track{paths.find(start, tolerance, [&](TTrack *Track) { + return path >= 0 && path < static_cast(Track->m_paths.size()) && + glm::distance(Track->m_paths[path].points[segment_data::point::start], start) <= tolerance && + glm::distance(Track->m_paths[path].points[segment_data::point::end], end) <= tolerance; + })}; + if (line.fail() || false == track.has_value()) + { + found = false; + break; + } + auto const &point{points[std::min(i, points.size() - 1)]}; + double s; + bool interior; + infra::project((*track)->m_paths[path], point, s, interior); + binding.anchors.push_back(infra::make_anchor({*track, path, s}, point)); + } + if (false == found || binding.anchors.size() != points.size()) + { + keep(); + continue; + } + binding.turns = binding.turns && yaw.has_value(); + if (binding.turns) + binding.yaw = turn_of(binding, *yaw); + binding.label = object_label(binding); + m_bindings.push_back(std::move(binding)); + ++resolved; + } + } + WriteLog("Editor: " + std::to_string(resolved) + " object(s) bound to the paths read from the scenery" + + (m_bindings_unresolved.empty() ? std::string{} : ", " + std::to_string(m_bindings_unresolved.size()) + " binding(s) whose objects or paths weren't found are kept as they are"), + logtype::generic); +} + +void editor_mode::infra_store() +{ + if (false == m_bindings_loaded || scene::Layers.empty()) + return; + Live.refresh(); + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), [](infra::binding const &Binding) { return false == alive(Binding); }), m_bindings.end()); + + std::map> lines; + for (auto const layer : scene::Layers.marked_layers(kBindingMark)) + lines[layer]; + for (auto const &entry : m_bindings_unresolved) + lines[entry.first].push_back(entry.second); + for (auto const &binding : m_bindings) + { + std::vector points; + std::optional yaw; + if (binding.anchors.empty() || false == object_points(binding, points, yaw)) + continue; + auto layer{scene::Layers.resolve(binding.anchors.front().at.track->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + auto text{std::string{infra::name(binding.type)} + ' ' + std::to_string(static_cast(binding.group)) + ' ' + local(points.front()) + ' ' + object_key(binding) + ' ' + + (binding.turns ? "1 " : "0 ") + std::to_string(binding.anchors.size())}; + for (auto const &anchor : binding.anchors) + { + auto const &path{anchor.at.track->m_paths[anchor.at.path]}; + text += ' ' + std::to_string(anchor.at.path) + ' ' + local(path.points[segment_data::point::start]) + ' ' + local(path.points[segment_data::point::end]); + } + lines[layer].push_back(std::move(text)); + } + for (auto &entry : lines) + { + if (false == entry.second.empty()) + entry.second.insert(entry.second.begin(), "# objects bound to the paths in the track editor, they follow changes of the track; the simulation skips these lines"); + scene::Layers.mark(entry.first, kBindingMark, std::move(entry.second)); + } + m_bindings_changed = false; +} + +std::vector editor_mode::infra_scope() const +{ + std::vector result; + auto const add = [&](TTrack *Track) { + if (Track != nullptr && false == Track->m_editorremoved && editor_track::is_supported(*Track) && false == contains(result, Track)) + result.push_back(Track); + }; + auto *selected{selected_track()}; + switch (m_infra.scope) + { + case 0: add(selected); break; + case 1: + if (selected != nullptr) + for (auto const &span : editor_track::run_route(*selected, m_infra.reach).spans) + add(span.track); + break; + case 2: + for (auto const &span : m_profile.route.spans) + add(span.track); + break; + case 3: + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && editor_track::is_supported(*track)) + result.push_back(track); + break; + default: break; + } + return result; +} + +infra::binding *editor_mode::infra_find(infra::binding const &Binding) +{ + for (auto &binding : m_bindings) + if (binding.same(Binding)) + return &binding; + return nullptr; +} + +void editor_mode::infra_recognize() +{ + auto &state{m_infra}; + infra_load(); + state.candidates.clear(); + state.hovered = -1; + state.error.clear(); + state.status.clear(); + state.tracks = infra_scope(); + if (state.tracks.empty()) + { + state.error = (state.scope == 2 ? "Open the vertical profile of a route first" : state.scope == 3 ? "The scenery has no paths the editor can handle" : "Select a path first"); + return; + } + track_index index; + std::unordered_set names; + for (auto *track : state.tracks) + { + index.add(*track); + if (false == track->name().empty() && track->name() != "none") + names.insert(track->name()); + } + Live.refresh(); + + auto const corridor{static_cast(state.corridor)}; + auto const add = [&](infra::binding Binding, std::string const &Reason, double const Reach) { + std::vector points; + std::optional yaw; + if (false == object_points(Binding, points, yaw)) + return; + for (auto const &point : points) + { + infra::station station; + if (false == index.nearest(point, Reach, station)) + return; + Binding.anchors.push_back(infra::make_anchor(station, point)); + } + if (std::abs(Binding.anchors.front().height) > 40.0) + return; + Binding.turns = yaw.has_value() && Binding.type != infra::kind::traction; + if (Binding.turns) + Binding.yaw = turn_of(Binding, *yaw); + Binding.label = object_label(Binding); + infra_candidate candidate; + candidate.offset = Binding.anchors.front().offset; + candidate.reason = Reason; + candidate.bound = (infra_find(Binding) != nullptr); + candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other); + candidate.binding = std::move(Binding); + state.candidates.push_back(std::move(candidate)); + }; + auto const rule_reason = [](infra::category const Group, char const *What) { return Group == infra::category::other ? std::string{"nearby, no rule matches the "} + What : std::string{"the "} + What + " matches a rule"; }; + + for (auto *model : simulation::Instances.sequence()) + { + // models of the includes follow along with their include + if (model == nullptr || model->from_template() || model->Model() == nullptr) + continue; + infra::binding binding; + binding.type = infra::kind::model; + binding.node = model; + binding.group = infra::classify(model->Model()->NameGet() + ' ' + model->name(), state.rules); + add(binding, rule_reason(binding.group, "file name"), corridor); + } + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (false == include_place_of(handle, place)) + continue; + infra::binding binding; + binding.type = infra::kind::include; + binding.include = handle; + binding.group = infra::classify(place.file + ' ' + place.description, state.rules); + add(binding, rule_reason(binding.group, "template or its description"), corridor); + } + for (auto *piece : simulation::Traction.sequence()) + { + if (piece == nullptr || piece->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::traction; + binding.node = piece; + binding.group = infra::category::catenary; + add(binding, "wire over the track", corridor); + } + for (auto *memcell : simulation::Memory.sequence()) + { + if (memcell == nullptr || memcell->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::memcell; + binding.node = memcell; + binding.group = infra::category::logic; + auto const named{false == memcell->asTrackName.empty() && names.count(memcell->asTrackName) > 0}; + add(binding, named ? "names the path " + memcell->asTrackName : std::string{"nearby"}, named ? std::max(corridor, 50.0) : corridor); + } + for (auto *launcher : simulation::Events.launchers()) + { + if (launcher == nullptr || launcher->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::launcher; + binding.node = launcher; + binding.group = infra::category::logic; + add(binding, "nearby", corridor); + } + std::stable_sort(state.candidates.begin(), state.candidates.end(), [](infra_candidate const &A, infra_candidate const &B) { return A.binding.group < B.binding.group; }); + state.status = format("%d object(s) found along %d path(s)", static_cast(state.candidates.size()), static_cast(state.tracks.size())); +} + +void editor_mode::infra_bind_chosen() +{ + auto &state{m_infra}; + auto count{0}; + Live.refresh(); + for (auto &candidate : state.candidates) + { + if (false == candidate.chosen || candidate.bound || false == alive(candidate.binding)) + continue; + if (auto *existing{infra_find(candidate.binding)}) + *existing = candidate.binding; + else + m_bindings.push_back(candidate.binding); + candidate.bound = true; + candidate.chosen = false; + ++count; + } + if (count > 0) + m_bindings_changed = true; + state.status = format("%d object(s) bound, they follow the changes of the track. The bindings are kept in the scenery files on save", count); +} + +void editor_mode::infra_unbind(std::vector Indices) +{ + std::sort(Indices.begin(), Indices.end()); + Indices.erase(std::unique(Indices.begin(), Indices.end()), Indices.end()); + for (auto index{Indices.rbegin()}; index != Indices.rend(); ++index) + { + if (*index >= m_bindings.size()) + continue; + for (auto &candidate : m_infra.candidates) + if (candidate.binding.same(m_bindings[*index])) + candidate.bound = false; + m_bindings.erase(m_bindings.begin() + *index); + } + if (false == Indices.empty()) + m_bindings_changed = true; +} + +infra::object_state editor_mode::infra_state_of(infra::binding const &Binding) const +{ + infra::object_state result; + result.saved = Binding; + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + result.location = model->location(); + result.angles = model->Angles(); + break; + } + case infra::kind::include: result.directive = scene::Layers.directive(Binding.include); break; + case infra::kind::traction: + { + auto const *traction{static_cast(Binding.node)}; + result.points = {traction->pPoint1, traction->pPoint2, traction->pPoint3, traction->pPoint4}; + break; + } + case infra::kind::memcell: + case infra::kind::launcher: result.location = Binding.node->location(); break; + } + return result; +} + +void editor_mode::infra_restore_state(infra::object_state const &State) +{ + Live.refresh(); + if (false == alive(State.saved)) + return; + if (auto *binding{infra_find(State.saved)}) + *binding = State.saved; + switch (State.saved.type) + { + case infra::kind::model: move_model(static_cast(State.saved.node), State.location, State.angles); break; + case infra::kind::include: + if (false == State.directive.empty() && State.directive != scene::Layers.directive(State.saved.include)) + set_include_directive(State.saved.include, State.directive); + break; + case infra::kind::traction: move_traction(static_cast(State.saved.node), State.points); break; + case infra::kind::memcell: move_memcell(static_cast(State.saved.node), State.location); break; + case infra::kind::launcher: move_launcher(static_cast(State.saved.node), State.location); break; + } +} + +void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh) +{ + for (auto &entry : m_infra_buffer) + { + if (false == entry.saved.same(Binding)) + continue; + if (Refresh && false == entry.moved) + entry = infra_state_of(Binding); + return; + } + if (false == Refresh && false == m_history.empty() && m_history.back().action == EditorSnapshot::Action::TrackEdit) + { + // a drag keeps a single undo step, which holds the state from before the drag already + for (auto const &entry : m_history.back().infra) + if (entry.saved.same(Binding)) + return; + } + m_infra_buffer.push_back(infra_state_of(Binding)); +} + +void editor_mode::infra_capture(TTrack const &Track) +{ + if (m_infra_suspended || false == m_infra.follow) + return; + infra_load(); + if (m_bindings.empty()) + return; + Live.refresh(); + for (auto &binding : m_bindings) + { + if (std::none_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { return Anchor.at.track == &Track; }) || false == alive(binding)) + continue; + // an object moved by hand since it was bound is measured again, as long as the path is the way it was measured + std::vector points; + std::optional yaw; + if (object_points(binding, points, yaw) && points.size() == binding.anchors.size()) + { + for (std::size_t i = 0; i < points.size(); ++i) + { + auto &anchor{binding.anchors[i]}; + if (anchor.at.track != &Track || anchor.at.path >= static_cast(Track.m_paths.size())) + continue; + auto const &path{Track.m_paths[anchor.at.path]}; + if (glm::distance(path.points[segment_data::point::start], anchor.start) > 1e-4 || glm::distance(path.points[segment_data::point::end], anchor.end) > 1e-4) + continue; + auto placed{anchor}; + if (glm::distance(infra::place(placed), points[i]) <= 0.01) + continue; + double s; + bool interior; + infra::project(path, points[i], s, interior); + anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, points[i]); + } + if (binding.turns && yaw.has_value() && binding.anchors.front().at.track == &Track) + binding.yaw = turn_of(binding, *yaw); + } + infra_buffer(binding, true); + } +} + +void editor_mode::infra_follow(std::vector const &Tracks) +{ + if (m_infra_suspended || false == m_infra.follow) + return; + infra_load(); + if (m_bindings.empty()) + return; + Live.refresh(); + std::vector areas; + for (auto *track : Tracks) + if (false == track->m_editorremoved) + areas.push_back(area_of(*track)); + // paths which took over may have been committed before the old ones were retired + std::optional> everywhere; + auto const take_over = [&](glm::dvec3 const &Foot, infra::station &Station) { + if (station_near(areas, Foot, 30.0, Station)) + return true; + if (false == everywhere.has_value()) + { + everywhere.emplace(); + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && editor_track::is_supported(*track)) + everywhere->push_back(area_of(*track)); + } + // only a path running where the old one did, not the one alongside + return station_near(*everywhere, Foot, 1.0, Station); + }; + for (auto &binding : m_bindings) + { + auto const affected{std::any_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { + return Anchor.at.track != nullptr && (Anchor.at.track->m_editorremoved || contains(Tracks, Anchor.at.track)); + })}; + if (false == affected || false == alive(binding)) + continue; + auto moved{binding}; + auto lost{false}; + for (auto &anchor : moved.anchors) + { + auto *track{anchor.at.track}; + if (false == track->m_editorremoved && false == contains(Tracks, track)) + continue; + // the object goes square to the new axis from the point of the old one, so a shift of the track carries it + // sideways rather than along; its own path goes first, then the ones which took over + infra::station station; + auto found{false}; + if (false == track->m_editorremoved && anchor.at.path < static_cast(track->m_paths.size())) + { + double s; + bool interior; + auto const distance{infra::project(track->m_paths[anchor.at.path], anchor.foot, s, interior)}; + station = {track, anchor.at.path, s}; + found = (distance <= 30.0 && beside(station, anchor.foot, interior)); + } + if (false == found && false == take_over(anchor.foot, station)) + { + lost = true; + break; + } + auto const offset{anchor.offset}; + auto const height{anchor.height}; + anchor = infra::make_anchor(station, anchor.foot); + anchor.offset = offset; + anchor.height = height; + } + if (lost) + { + binding.lost = true; + continue; + } + infra_buffer(binding, false); + moved.lost = false; + binding = std::move(moved); + infra_move(binding); + for (auto &entry : m_infra_buffer) + if (entry.saved.same(binding)) + entry.moved = true; + } +} + +void editor_mode::infra_split(TTrack &Original, TTrack &Created, segment_data const &First) +{ + if (m_infra_suspended || Created.m_paths.empty()) + return; + auto const length{infra::plan_length(First)}; + auto const &second{Created.m_paths.front()}; + for (auto &binding : m_bindings) + for (auto &anchor : binding.anchors) + { + if (anchor.at.track != &Original || anchor.at.path != 0) + continue; + if (anchor.at.s > length) + { + anchor.at.track = &Created; + anchor.at.s -= length; + anchor.start = second.points[segment_data::point::start]; + anchor.end = second.points[segment_data::point::end]; + anchor.length = infra::plan_length(second); + } + else + { + anchor.end = First.points[segment_data::point::end]; + anchor.length = length; + } + } +} + +void editor_mode::infra_move(infra::binding &Binding) +{ + if (Binding.anchors.empty()) + return; + auto const location{infra::place(Binding.anchors.front())}; + auto const facing = [&]() { return clamp_circular(infra::heading(infra::frame_at(Binding.anchors.front().at).forward) + Binding.yaw); }; + switch (Binding.type) + { + case infra::kind::model: + { + auto *model{static_cast(Binding.node)}; + auto angles{model->Angles()}; + if (Binding.turns) + angles.y = static_cast(facing()); + move_model(model, location, angles); + break; + } + case infra::kind::include: + { + include_place place; + if (false == include_place_of(Binding.include, place)) + return; + for (int axis = 0; axis < 3; ++axis) + place.values[place.position[axis]] = editor_includes::number(location[axis] - place.offset[axis]); + if (Binding.turns && place.yaw >= 0) + place.values[place.yaw] = editor_includes::number(facing()); + auto const directive{editor_includes::compose_directive(place.file, place.values)}; + if (directive != scene::Layers.directive(Binding.include)) + set_include_directive(Binding.include, directive); + break; + } + case infra::kind::traction: + { + if (Binding.anchors.size() != 4) + return; + std::array points; + for (std::size_t i = 0; i < 4; ++i) + points[i] = infra::place(Binding.anchors[i]); + move_traction(static_cast(Binding.node), points); + break; + } + case infra::kind::memcell: move_memcell(static_cast(Binding.node), location); break; + case infra::kind::launcher: move_launcher(static_cast(Binding.node), location); break; + } +} + +void editor_mode::infra_attach(EditorSnapshot &Snapshot) +{ + auto const involved = [&](infra::object_state const &State) { + if (State.moved) + return true; + for (auto const &anchor : State.saved.anchors) + { + if (contains(Snapshot.created, anchor.at.track)) + return true; + for (auto const &entry : Snapshot.tracks) + if (entry.first == anchor.at.track) + return true; + } + return false; + }; + for (auto &entry : m_infra_buffer) + if (involved(entry)) + Snapshot.infra.push_back(std::move(entry)); + m_infra_buffer.clear(); +} + +void editor_mode::render_infra_window() +{ + auto &state{m_infra}; + if (false == state.open) + return; + ImGui::SetNextWindowSize(ImVec2(560, 620), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin("Infrastructure along the track###trackinfra", &state.open)) + { + ImGui::End(); + return; + } + infra_load(); + Live.refresh(); + + auto follow{state.follow}; + if (ImGui::Checkbox("Bound objects follow the changes of the track", &follow)) + { + state.follow = follow; + if (follow) + { + // what changed in the meantime becomes the new starting point + for (auto &binding : m_bindings) + { + std::vector points; + std::optional yaw; + if (false == alive(binding) || false == object_points(binding, points, yaw) || points.size() != binding.anchors.size()) + continue; + for (std::size_t i = 0; i < points.size(); ++i) + { + auto &anchor{binding.anchors[i]}; + if (anchor.at.track == nullptr || anchor.at.track->m_editorremoved || anchor.at.path >= static_cast(anchor.at.track->m_paths.size())) + { + binding.lost = true; + continue; + } + double s; + bool interior; + infra::project(anchor.at.track->m_paths[anchor.at.path], points[i], s, interior); + anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, points[i]); + } + if (binding.turns && yaw.has_value()) + binding.yaw = turn_of(binding, *yaw); + } + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Bound objects keep their chainage along the path, the offset from its axis and the height above the rail top;\n" + "models and includes turn with the path and stay upright. The move is a part of the undo step of the change."); + ImGui::SameLine(); + ImGui::Checkbox("Show in 3D", &state.show); + ImGui::Text("Objects bound in the scenery: %d", static_cast(m_bindings.size())); + if (false == m_bindings_unresolved.empty()) + { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.f, 0.8f, 0.3f, 1.f), "(%d stored binding(s) not found, kept as they are)", static_cast(m_bindings_unresolved.size())); + } + + ImGui::Separator(); + static char const *const scopes[] = {"Selected path", "Line through the selected path", "Route of the vertical profile", "Whole scenery"}; + ImGui::SetNextItemWidth(260.f); + ImGui::Combo("Look along", &state.scope, scopes, IM_ARRAYSIZE(scopes)); + if (state.scope == 1) + { + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat("Reach each way, m", &state.reach, 100.f, 500.f, "%.0f"); + state.reach = std::clamp(state.reach, 10.f, 20000.f); + } + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat("Corridor each side of the axis, m", &state.corridor, 0.5f, 2.f, "%.1f"); + state.corridor = std::clamp(state.corridor, 0.5f, 50.f); + if (ImGui::TreeNode("Rules for names of the files")) + { + ImGui::TextDisabled("Parts of the names, separated by spaces; the first rule which matches decides"); + for (std::size_t i = 0; i < state.rules.size(); ++i) + { + auto &rule{state.rules[i]}; + char buffer[512]; + std::snprintf(buffer, sizeof(buffer), "%s", rule.keywords.c_str()); + ImGui::SetNextItemWidth(360.f); + if (ImGui::InputText((std::string{infra::name(rule.group)} + "##rule" + std::to_string(i)).c_str(), buffer, sizeof(buffer))) + rule.keywords = buffer; + } + if (ImGui::SmallButton("Default rules")) + state.rules = infra::default_rules(); + ImGui::TextDisabled("Includes are matched by the template name and its //$e description, models by the file and node name;\n" + "traction is always the catenary, memory cells and event launchers the logic"); + ImGui::TreePop(); + } + if (ImGui::Button("Find objects")) + infra_recognize(); + + auto &candidates{state.candidates}; + state.hovered = -1; + if (false == candidates.empty()) + { + int chosen{0}; + for (auto const &candidate : candidates) + chosen += candidate.chosen ? 1 : 0; + ImGui::SameLine(); + if (ImGui::Button(format("Bind the chosen (%d)", chosen).c_str())) + infra_bind_chosen(); + ImGui::SameLine(); + if (ImGui::SmallButton("All")) + for (auto &candidate : candidates) + candidate.chosen = (false == candidate.bound); + ImGui::SameLine(); + if (ImGui::SmallButton("None")) + for (auto &candidate : candidates) + candidate.chosen = false; + + ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); + for (auto const group : infra::categories()) + { + auto const count{std::count_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })}; + if (count == 0) + continue; + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(colour(group))); + auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast(count)).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{"all##g"} + infra::name(group)).c_str())) + for (auto &candidate : candidates) + if (candidate.binding.group == group) + candidate.chosen = (false == candidate.bound); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{"none##g"} + infra::name(group)).c_str())) + for (auto &candidate : candidates) + if (candidate.binding.group == group) + candidate.chosen = false; + if (false == open) + continue; + // the candidates are sorted by the category + auto const first{static_cast(std::distance(candidates.begin(), std::find_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })))}; + ImGuiListClipper clipper(static_cast(count)); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const i{static_cast(first + row)}; + auto &candidate{candidates[i]}; + ImGui::PushID(static_cast(i)); + if (candidate.bound) + { + ImGui::TextDisabled("[bound]"); + } + else + { + ImGui::Checkbox("##chosen", &candidate.chosen); + } + ImGui::SameLine(); + ImGui::Text("%s", candidate.binding.label.c_str()); + if (ImGui::IsItemHovered()) + { + state.hovered = static_cast(i); + ImGui::SetTooltip("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s", infra::name(candidate.binding.type), candidate.binding.anchors.front().at.track->name().c_str(), + std::abs(candidate.offset), candidate.offset >= 0.0 ? "right" : "left", candidate.binding.anchors.front().height, candidate.reason.c_str()); + } + ImGui::SameLine(); + ImGui::TextDisabled("%s %+.1f m", infra::name(candidate.binding.type), candidate.offset); + ImGui::PopID(); + } + ImGui::TreePop(); + } + ImGui::EndChild(); + } + + if (ImGui::TreeNode("Bound objects along the scope")) + { + auto const scope{infra_scope()}; + std::unordered_set const tracks(scope.begin(), scope.end()); + std::vector along; + for (std::size_t i = 0; i < m_bindings.size(); ++i) + if (std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) + along.push_back(i); + ImGui::Text("%d of %d", static_cast(along.size()), static_cast(m_bindings.size())); + if (false == along.empty()) + { + ImGui::SameLine(); + if (ImGui::SmallButton("Unbind all of these")) + { + infra_unbind(along); + along.clear(); + } + } + std::optional unbind; + ImGui::BeginChild("##infrabound", ImVec2(0.f, 160.f), true); + ImGuiListClipper clipper(static_cast(along.size())); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const index{along[row]}; + auto const &binding{m_bindings[index]}; + ImGui::PushID(static_cast(index)); + if (ImGui::SmallButton("unbind")) + unbind = index; + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(binding.lost ? IM_COL32(255, 80, 70, 255) : colour(binding.group))); + ImGui::Text("%s%s", binding.label.c_str(), binding.lost ? " (no path to follow)" : ""); + ImGui::PopStyleColor(); + ImGui::PopID(); + } + ImGui::EndChild(); + if (unbind) + infra_unbind({*unbind}); + ImGui::TreePop(); + } + + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.f, 0.4f, 0.35f, 1.f), "%s", state.error.c_str()); + else if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + ImGui::End(); +} + +void editor_mode::draw_infra_overlay() const +{ + auto const &state{m_infra}; + if (false == state.open || false == state.show) + return; + Live.refresh(); + screen_projection const projection; + ImDrawList *drawlist{ImGui::GetBackgroundDrawList()}; + auto const camera{GfxRenderer->Camera_Position()}; + auto const draw = [&](infra::binding const &Binding, ImU32 const Colour, float const Size) { + if (Binding.anchors.empty() || glm::distance(Binding.anchors.front().foot, camera) > 2000.0) + return; + std::vector points; + std::optional yaw; + if (false == alive(Binding) || false == object_points(Binding, points, yaw)) + return; + for (std::size_t i = 0; i < points.size() && i < Binding.anchors.size(); ++i) + { + auto anchor{Binding.anchors[i]}; + auto const foot{infra::frame_at(anchor.at).point}; + projection.line(drawlist, foot, points[i], Colour, 1.5f); + } + ImVec2 screen; + if (projection.project(points.front(), screen)) + drawlist->AddCircleFilled(screen, Size, Colour); + }; + for (auto const &binding : m_bindings) + draw(binding, binding.lost ? IM_COL32(255, 60, 60, 255) : IM_COL32(90, 230, 110, 220), 3.5f); + for (int i = 0; i < static_cast(state.candidates.size()); ++i) + { + auto const &candidate{state.candidates[i]}; + if (candidate.bound) + continue; + draw(candidate.binding, colour(candidate.binding.group, candidate.chosen ? 255 : 110), i == state.hovered ? 9.f : (candidate.chosen ? 5.f : 3.f)); + } +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index ed5ecd30..29a2ab9b 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -342,6 +342,7 @@ void editor_mode::push_track_snapshot(std::vectoruuid; snap.tracks = std::move(States); snap.created = std::move(Created); + infra_attach(snap); m_history.push_back(std::move(snap)); g_redo.clear(); } @@ -354,6 +355,7 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve std::vector tracks; for (auto const &entry : Snapshot.tracks) tracks.emplace_back(entry.first); + m_infra_suspended = true; EditorSnapshot current{Snapshot}; current.tracks.clear(); auto involved{tracks}; @@ -361,6 +363,10 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve involved.insert(involved.end(), Snapshot.created.begin(), Snapshot.created.end()); for (auto *track : involved) current.tracks.emplace_back(track, editor_track::capture(*track)); + current.infra.clear(); + for (auto const &entry : Snapshot.infra) + if (auto const *binding{infra_find(entry.saved)}) + current.infra.push_back(infra_state_of(*binding)); Opposite.push_back(std::move(current)); if (Undo) @@ -383,6 +389,10 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve for (auto const &entry : Snapshot.tracks) editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); + for (auto const &entry : Snapshot.infra) + infra_restore_state(entry); + m_infra_buffer.clear(); + m_infra_suspended = false; if (false == m_route.chain.tracks.empty()) { std::string error; @@ -420,6 +430,9 @@ void editor_mode::render_track_ui() profile_open_run(*track); m_profile.open = true; } + ImGui::SameLine(); + if (ImGui::Button("Infrastructure")) + m_infra.open = true; if (ImGui::CollapsingHeader("Straights in the scenery")) render_straights_ui(); render_switch_ui(); @@ -470,6 +483,13 @@ void editor_mode::render_track_window() } if (ImGui::SmallButton("Vertical profile along this line") && selected_track() != nullptr) profile_open_run(*selected_track()); + ImGui::SameLine(); + if (ImGui::SmallButton("Infrastructure along this line")) + { + m_infra.open = true; + m_infra.scope = 1; + infra_recognize(); + } ImGui::End(); } diff --git a/editor/editorInfra.cpp b/editor/editorInfra.cpp new file mode 100644 index 00000000..974b5689 --- /dev/null +++ b/editor/editorInfra.cpp @@ -0,0 +1,304 @@ +/* +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/editorInfra.hpp" + +#include "world/Track.h" + +#include +#include +#include +#include + +namespace infra +{ + +namespace +{ + +int const kSteps{128}; + +struct curve +{ + glm::dvec3 p0, p1, p2, p3; +}; + +curve curve_of(segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + return {start, start + (end - start) / 3.0, end + (start - end) / 3.0, end}; + return {start, start + control1, end + control2, end}; +} + +glm::dvec3 point(curve const &Curve, double const T) +{ + auto const u{1.0 - T}; + return Curve.p0 * (u * u * u) + Curve.p1 * (3.0 * u * u * T) + Curve.p2 * (3.0 * u * T * T) + Curve.p3 * (T * T * T); +} + +glm::dvec3 derivative(curve const &Curve, double const T) +{ + auto const u{1.0 - T}; + return (Curve.p1 - Curve.p0) * (3.0 * u * u) + (Curve.p2 - Curve.p1) * (6.0 * u * T) + (Curve.p3 - Curve.p2) * (3.0 * T * T); +} + +double plan(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +// arc length in the plan at the evenly spread parameters +std::array lengths(curve const &Curve) +{ + std::array result{}; + auto previous{Curve.p0}; + for (int i = 1; i <= kSteps; ++i) + { + auto const next{point(Curve, static_cast(i) / kSteps)}; + result[i] = result[i - 1] + plan(previous, next); + previous = next; + } + return result; +} + +double parameter(std::array const &Lengths, double const S) +{ + if (S <= 0.0) + return 0.0; + if (S >= Lengths.back()) + return 1.0; + auto const next{std::upper_bound(Lengths.begin(), Lengths.end(), S)}; + auto const index{static_cast(std::distance(Lengths.begin(), next)) - 1}; + auto const run{Lengths[index + 1] - Lengths[index]}; + auto const fraction{run > 1e-12 ? (S - Lengths[index]) / run : 0.0}; + return (index + fraction) / kSteps; +} + +double length_at(std::array const &Lengths, double const T) +{ + auto const position{std::clamp(T, 0.0, 1.0) * kSteps}; + auto const index{std::min(static_cast(position), kSteps - 1)}; + return Lengths[index] + (Lengths[index + 1] - Lengths[index]) * (position - index); +} + +glm::dvec2 plan_direction(curve const &Curve, double const T) +{ + auto tangent{derivative(Curve, T)}; + glm::dvec2 result{tangent.x, tangent.z}; + if (glm::length(result) < 1e-9) + result = {Curve.p3.x - Curve.p0.x, Curve.p3.z - Curve.p0.z}; + return glm::length(result) > 1e-9 ? glm::normalize(result) : glm::dvec2{0.0, 1.0}; +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return Text; +} + +} // namespace + +char const *name(kind const Kind) +{ + switch (Kind) + { + case kind::model: return "model"; + case kind::include: return "include"; + case kind::traction: return "traction"; + case kind::memcell: return "memcell"; + case kind::launcher: return "eventlauncher"; + } + return ""; +} + +char const *name(category const Category) +{ + switch (Category) + { + case category::signal: return "Signals"; + case category::sign: return "Signs"; + case category::pole: return "Poles and gantries"; + case category::catenary: return "Catenary"; + case category::logic: return "Logic"; + case category::other: return "Other"; + } + return ""; +} + +std::vector const &categories() +{ + static std::vector const result{category::signal, category::sign, category::pole, category::catenary, category::logic, category::other}; + return result; +} + +double plan_length(segment_data const &Path) +{ + return lengths(curve_of(Path)).back(); +} + +std::vector outline(segment_data const &Path, double const Step) +{ + auto const shape{curve_of(Path)}; + // the speed along the curve doesn't exceed three times the longest leg of the control polygon + auto const leg{std::max({plan(shape.p0, shape.p1), plan(shape.p1, shape.p2), plan(shape.p2, shape.p3)})}; + auto const count{std::clamp(static_cast(std::ceil(3.0 * leg / std::max(Step, 0.1))), 1, 4096)}; + std::vector result; + result.reserve(count + 1); + for (int i = 0; i <= count; ++i) + result.push_back(point(shape, static_cast(i) / count)); + return result; +} + +frame frame_at(segment_data const &Path, double const S) +{ + auto const shape{curve_of(Path)}; + auto const t{parameter(lengths(shape), S)}; + return {point(shape, t), plan_direction(shape, t)}; +} + +frame frame_at(station const &Station) +{ + if (Station.track == nullptr || Station.path < 0 || Station.path >= static_cast(Station.track->m_paths.size())) + return {}; + return frame_at(Station.track->m_paths[Station.path], Station.s); +} + +double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior) +{ + auto const shape{curve_of(Path)}; + auto const table{lengths(shape)}; + auto const distance = [&](double const T) { return plan(point(shape, T), Point); }; + auto best{0}; + auto bestdistance{distance(0.0)}; + for (int i = 1; i <= kSteps; ++i) + { + auto const d{distance(static_cast(i) / kSteps)}; + if (d < bestdistance) + { + bestdistance = d; + best = i; + } + } + // golden section search around the nearest sample + auto low{std::max(0, best - 1) / static_cast(kSteps)}; + auto high{std::min(kSteps, best + 1) / static_cast(kSteps)}; + double const ratio{0.6180339887498949}; + auto a{high - ratio * (high - low)}; + auto b{low + ratio * (high - low)}; + auto da{distance(a)}, db{distance(b)}; + for (int i = 0; i < 40; ++i) + { + if (da < db) + { + high = b; + b = a; + db = da; + a = high - ratio * (high - low); + da = distance(a); + } + else + { + low = a; + a = b; + da = db; + b = low + ratio * (high - low); + db = distance(b); + } + } + auto t{(low + high) * 0.5}; + if (distance(0.0) <= distance(t)) + t = 0.0; + if (distance(1.0) <= distance(t)) + t = 1.0; + S = length_at(table, t); + Interior = (t > 1e-6 && t < 1.0 - 1e-6); + return distance(t); +} + +double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior) +{ + auto best{-1.0}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + double s; + bool interior; + auto const d{project(Track.m_paths[i], Point, s, interior)}; + // an interior point takes precedence over an end shared with the other path + if (best < 0.0 || d < best - 1e-6 || (std::abs(d - best) <= 1e-6 && interior && false == Interior)) + { + best = d; + Station = {&Track, i, s}; + Interior = interior; + } + } + return best; +} + +double heading(glm::dvec2 const &Forward) +{ + return glm::degrees(std::atan2(Forward.x, Forward.y)); +} + +anchor make_anchor(station const &At, glm::dvec3 const &Point) +{ + anchor result; + result.at = At; + auto const axis{frame_at(At)}; + glm::dvec2 const side{axis.forward.y, -axis.forward.x}; + result.offset = glm::dot(glm::dvec2{Point.x - axis.point.x, Point.z - axis.point.z}, side); + result.height = Point.y - axis.point.y; + result.foot = axis.point; + if (At.track != nullptr && At.path >= 0 && At.path < static_cast(At.track->m_paths.size())) + { + auto const &path{At.track->m_paths[At.path]}; + result.start = path.points[segment_data::point::start]; + result.end = path.points[segment_data::point::end]; + result.length = plan_length(path); + } + return result; +} + +glm::dvec3 place(anchor &Anchor) +{ + auto const axis{frame_at(Anchor.at)}; + Anchor.foot = axis.point; + glm::dvec2 const side{axis.forward.y, -axis.forward.x}; + return {axis.point.x + side.x * Anchor.offset, axis.point.y + Anchor.height, axis.point.z + side.y * Anchor.offset}; +} + +std::vector default_rules() +{ + return { + {category::signal, "semafor sem_ sem- tarcza tarcz_ tz_ tm_ tzw powtarz sygnaliz sygnal signal"}, + {category::sign, "wskaz wsk_ wsk- w4 w8 w11 w24 w27 kilometr hektometr slupek_km znak tablic sign"}, + {category::catenary, "trakcj siec_ siec- catenary przewod lina_nosna"}, + {category::pole, "slup maszt wysieg bramk konstrukcj kotw odciag pole gantry"}, + }; +} + +category classify(std::string const &Name, std::vector const &Rules) +{ + auto const text{lower(Name)}; + for (auto const &rule : Rules) + { + std::istringstream words{lower(rule.keywords)}; + std::string word; + while (words >> word) + if (text.find(word) != std::string::npos) + return rule.group; + } + return category::other; +} + +} // namespace infra diff --git a/editor/editorInfra.hpp b/editor/editorInfra.hpp new file mode 100644 index 00000000..afb76d8e --- /dev/null +++ b/editor/editorInfra.hpp @@ -0,0 +1,128 @@ +/* +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 + +#include "world/Segment.h" +#include "scene/scenenode.h" + +class TTrack; + +// objects which belong to the railway infrastructure (signals, signs, catenary, poles, the logic placed along the +// line) bound to the paths they stand by, so they follow the changes of the track geometry +namespace infra +{ + +enum class kind +{ + model, + include, + traction, + memcell, + launcher +}; +enum class category +{ + signal, + sign, + pole, + catenary, + logic, + other +}; +char const *name(kind Kind); +char const *name(category Category); +std::vector const &categories(); + +// point of a path, measured in the plan from the start of the path +struct station +{ + TTrack *track{nullptr}; + int path{0}; + double s{0.0}; +}; +struct frame +{ + glm::dvec3 point{0.0}; + glm::dvec2 forward{0.0, 1.0}; // in the plan, x and z +}; + +double plan_length(segment_data const &Path); +// points of the path, no farther apart in the plan than about specified step; the ends included +std::vector outline(segment_data const &Path, double Step); +frame frame_at(segment_data const &Path, double S); +frame frame_at(station const &Station); +// nearest point of specified path in the plan. returns: distance in the plan; Interior tells the nearest point +// isn't one of the ends of the path +double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior); +// nearest point among the paths of the track +double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior); +// angle around the vertical axis which turns a model to face specified direction, in degrees +double heading(glm::dvec2 const &Forward); + +// location of a point relative to the rail axis: the station, a sideways offset and a height above the rail top +struct anchor +{ + station at; + double offset{0.0}; // positive to the right of the direction of the path + double height{0.0}; + glm::dvec3 foot{0.0}; // point of the axis the location was measured from + // ends and length of the path when the station was measured, to tell whether the path changed since + glm::dvec3 start{0.0}, end{0.0}; + double length{0.0}; +}; +anchor make_anchor(station const &At, glm::dvec3 const &Point); +// location for the current geometry of the path +glm::dvec3 place(anchor &Anchor); + +// rules telling infrastructure apart by the names of the files and nodes +struct rule +{ + category group{category::other}; + std::string keywords; // separated by spaces, matched as parts of the lower case name +}; +std::vector default_rules(); +// first rule in the list which matches. returns: category::other if none does +category classify(std::string const &Name, std::vector const &Rules); + +struct binding +{ + kind type{kind::model}; + category group{category::other}; + scene::basic_node *node{nullptr}; // the object, unless it's an include + scene::instance_handle include{0}; + std::vector anchors; // one per point which defines the object: four for a traction piece + double yaw{0.0}; // rotation of the object relative to the direction of the path + bool turns{false}; // the object has a rotation which follows the path + std::string label; + bool lost{false}; // none of the paths around could take the object over + + bool same(binding const &Other) const + { + return type == Other.type && node == Other.node && include == Other.include; + } +}; + +// placement of a bound object, to take a change back +struct object_state +{ + binding saved; + glm::dvec3 location{0.0}; + glm::vec3 angles{0.f}; + std::array points{}; + std::string directive; + bool moved{false}; // the object followed a change since the state was taken +}; + +} // namespace infra diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index a579defe..f86f012f 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -124,8 +124,15 @@ bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool } +std::function editor_track::on_capture; +std::function const &)> editor_track::on_commit; +std::function editor_track::on_retire; +std::function editor_track::on_split; + editor_track::state editor_track::capture(TTrack const &Track) { + if (on_capture) + on_capture(Track); state result; result.paths = Track.m_paths; result.velocity = Track.fVelocity; @@ -235,6 +242,8 @@ void editor_track::commit(std::vector const &Tracks) for (auto *track : affected) for (auto *vehicle : track->Dynamics) vehicle->Move(0.000001); + if (on_commit) + on_commit(Tracks); } void editor_track::commit_parameters(TTrack &Track) @@ -2300,6 +2309,8 @@ TTrack *editor_track::split_path(TTrack &Track, double const T) second.rolls[0] = roll; auto *created{create_path(Track, second)}; Track.m_paths.front() = first; + if (on_split) + on_split(Track, *created, first); commit({&Track, created}); return created; } @@ -2336,6 +2347,8 @@ void editor_track::retire(TTrack &Track) update_transition(*neighbour); rebuild_geometry(*neighbour); } + if (on_retire) + on_retire(Track); } void editor_track::revive(TTrack &Track) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 396a66c5..34b0a7d8 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -227,6 +227,13 @@ class editor_track static void velocity(TTrack &Track, double const Velocity); static void damage(TTrack &Track, int const Damage); + // let the objects which belong to the paths follow their changes + static std::function on_capture; + static std::function const &)> on_commit; + static std::function on_retire; + // called before the commit; the first argument keeps the beginning of the path, given in the last one + static std::function on_split; + private: static void disconnect(TTrack &Track); static void join(TTrack &Track); diff --git a/scene/scene.cpp b/scene/scene.cpp index e72ab930..d876a7ba 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -503,6 +503,30 @@ basic_cell::erase( TMemCell *Memorycell ) { std::end( m_memorycells ) ); } +void +basic_cell::erase( TTraction *Traction ) { + + m_traction.erase( + std::remove( std::begin( m_traction ), std::end( m_traction ), Traction ), + std::end( m_traction ) ); +} + +void +basic_cell::unregister_end( TTraction *Traction ) { + + m_directories.traction.erase( + std::remove( std::begin( m_directories.traction ), std::end( m_directories.traction ), Traction ), + std::end( m_directories.traction ) ); +} + +void +basic_cell::erase( TEventLauncher *Launcher ) { + + m_eventlaunchers.erase( + std::remove( std::begin( m_eventlaunchers ), std::end( m_eventlaunchers ), Launcher ), + std::end( m_eventlaunchers ) ); +} + // registers provided path in the lookup directory of the cell void basic_cell::register_end( TTrack *Path ) { diff --git a/scene/scene.h b/scene/scene.h index 5cad1fcc..23c912d7 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -155,6 +155,14 @@ public: // removes provided memory cell from the cell void erase( TMemCell *Memorycell ); + // removes provided traction piece from the cell + void + erase( TTraction *Traction ); + void + unregister_end( TTraction *Traction ); + // removes provided event launcher from the cell + void + erase( TEventLauncher *Launcher ); // find a vehicle located nearest to specified point, within specified radius. reurns: located vehicle and distance std::tuple find( glm::dvec3 const &Point, float const Radius, bool const Onlycontrolled, bool const Findbycoupler ) const; diff --git a/scene/scenelayers.h b/scene/scenelayers.h index 523e2da3..28517c16 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -292,6 +292,11 @@ class node_layers { return Instance != 0 && Instance <= m_instances.size(); } + // handles of the includes run from 1 up to this number + std::size_t instance_count() const + { + return m_instances.size(); + } // grants access to specified include. NOTE: the include has to be tracked include_instance const &instance(instance_handle const Instance) const { diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 9d5f9297..02029a3a 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -14,6 +14,9 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "model/AnimModel.h" #include "world/MemCell.h" +#include "world/Traction.h" +#include "world/EvLaunch.h" +#include "world/Event.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/utilities.h" @@ -233,6 +236,54 @@ bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &E return true; } +// definition layout: node traction +// [parallel ] endtraction +bool patch_traction(std::string &Text, TTraction const &Traction, glm::dvec3 const &Offset, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 30 || tokens[0].text != "node" || tokens[4].text != "traction" || tokens.back().text != "endtraction") + { + Error = "unexpected layout of traction definition"; + return false; + } + std::vector changes; + std::size_t index{12}; + for (auto const &point : {Traction.pPoint1, Traction.pPoint2, Traction.pPoint3, Traction.pPoint4}) + { + auto const local{point - Offset}; + for (auto const value : {local.x, local.y, local.z}) + { + changes.push_back({tokens[index].begin, tokens[index].end, number(value)}); + ++index; + } + } + auto const &p1{Traction.pPoint1}; + auto const &p2{Traction.pPoint2}; + auto const &p3{Traction.pPoint3}; + auto const &p4{Traction.pPoint4}; + changes.push_back({tokens[24].begin, tokens[24].end, number((p3.y - p1.y + p4.y - p2.y) * 0.5 - Traction.fHeightDifference)}); + if (Traction.iNumSections > 0) + { + // the load truncates length / segment length, the margin keeps the rounding of the number from losing a section + changes.push_back({tokens[25].begin, tokens[25].end, number(glm::length(p1 - p2) / (Traction.iNumSections + 0.001))}); + } + apply(Text, changes); + return true; +} + +// definition layout: node eventlauncher ... end +bool patch_launcher(std::string &Text, glm::dvec3 const &Location, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 9 || tokens[0].text != "node" || tokens[4].text != "eventlauncher") + { + Error = "unexpected layout of event launcher definition"; + return false; + } + apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}}); + return true; +} + std::string detect_eol(std::string const &Text, std::string const &Fallback) { auto const linebreak{Text.find('\n')}; @@ -730,6 +781,63 @@ save_result node_layers::save(std::vector const &Rootstatements) return fail(state.error); } } + // traction and event launchers are rewritten when the editor moved them along with the track they belong to + std::vector patchednodes; + auto const patch_node = [&](basic_node *Node, auto const &Patch) { + auto const lookup{m_sources.find(Node)}; + if (lookup == m_sources.end() || false == Node->dirty()) + { + return true; + } + auto const &source{lookup->second}; + if (false == is_output(resolve(source.layer)) || false == writable(source.layer)) + { + return true; + } + if (false == load(source.layer)) + { + return false; + } + auto const &content{state.content[source.layer]}; + if (source.span.end > static_cast(content.size())) + { + state.error = "definition of \"" + Node->name() + "\" is out of bounds of file \"" + layer(source.layer).name + "\""; + return false; + } + auto text{content.substr(static_cast(source.span.begin), static_cast(source.span.end - source.span.begin))}; + std::string error; + if (false == Patch(text, source, error)) + { + state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; + return false; + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + edit.length = text.size(); + edit.text = std::move(text); + edit.node = Node; + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(Node); + patchednodes.push_back(Node); + return true; + }; + for (auto *traction : simulation::Traction.sequence()) + { + if (traction != nullptr && + false == patch_node(traction, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_traction(Text, *traction, Source.context.offset, Error); })) + { + return fail(state.error); + } + } + for (auto *launcher : simulation::Events.launchers()) + { + if (launcher != nullptr && + false == patch_node(launcher, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_launcher(Text, Source.context.to_local(launcher->location()), Error); })) + { + return fail(state.error); + } + } std::vector savedpaths; std::vector droppedpaths; auto const path_text = [](TTrack &Path, glm::dvec3 const &Offset) { @@ -1281,6 +1389,10 @@ save_result node_layers::save(std::vector const &Rootstatements) { path->m_dirty = false; } + for (auto *node : patchednodes) + { + node->m_dirty = false; + } for (auto const *path : droppedpaths) { m_sources.erase(path); diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 18cd07af..da0813a8 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -510,6 +510,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } scene::Groups.insert( scene::Groups.handle(), traction ); simulation::Region->insert_and_register( traction ); + scene::Layers.track( traction, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "tractionpowersource" ) { @@ -656,6 +657,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch simulation::Region->insert( eventlauncher ); } } + scene::Layers.track( eventlauncher, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "sound" ) { diff --git a/world/Event.h b/world/Event.h index 52ee3468..9b8f32d8 100644 --- a/world/Event.h +++ b/world/Event.h @@ -699,6 +699,14 @@ public: // returns all eventlaunchers in radius ignoring height std::vector find_eventlaunchers(glm::vec2 center, float radius) const; + // all event launchers, global ones included + std::vector + launchers() const { + auto const &input { m_inputdrivenlaunchers.sequence() }; + auto const &radio { m_radiodrivenlaunchers.sequence() }; + std::vector result( input.begin(), input.end() ); + result.insert( result.end(), radio.begin(), radio.end() ); + return result; } private: // types