From f30fefd91cd8fe4023e6aa6e183c0ec3b0537197 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:15:00 +0200 Subject: [PATCH] =?UTF-8?q?Sie=C4=87=20trakcyjna:=20prz=C4=99s=C5=82a=20wg?= =?UTF-8?q?=20typ=C3=B3w=20sieci,=20wysi=C4=99gniki=20z=20katalogu,=20bram?= =?UTF-8?q?ki,=20kotwienia=20i=20edytor=20sieci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sam kod exe z gałęzi siec-trakcyjna (bez testów, notatek i danych z tests/catenary). Dane (data/traction_*.txt) leżą poza repozytorium. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019Tqn7T8D1Gx3ptKtZgNtX5 --- CMakeLists.txt | 13 +- application/driveruipanels.cpp | 5 + application/editormode.cpp | 53 +- application/editormode.h | 34 +- application/editormodecatenary.cpp | 3286 ++++++++++++++++++++++ application/editormodecatenary.h | 208 ++ application/editormodetrack.cpp | 29 +- application/editormodetrackux.cpp | 12 +- application/editormodeviewport.cpp | 9 + application/editoruilayer.cpp | 2 +- application/editoruilayer.h | 3 + editor/editorIcons.hpp | 4 + editor/editorTrack.cpp | 10 +- editor/icons/editor_icons.png | Bin 52589 -> 51260 bytes editor/icons/editor_icons.py | 52 +- lang/editor_pl.po | 681 +++++ model/Model3d.cpp | 75 + rendering/opengl33renderer.cpp | 67 + rendering/openglrenderer.cpp | 16 + shaders/mat_wire.frag | 57 + simulation/simulation.cpp | 2 + simulation/simulationstateserializer.cpp | 9 + utilities/Globals.cpp | 17 + utilities/Globals.h | 1 + world/Traction.cpp | 32 +- world/Traction.h | 4 + world/catenary_cantilever.cpp | 2276 +++++++++++++++ world/catenary_cantilever.h | 321 +++ world/catenary_catalog.cpp | 255 ++ world/catenary_catalog.h | 103 + world/catenary_layout.cpp | 121 + world/catenary_layout.h | 53 + world/catenary_network.cpp | 561 ++++ world/catenary_network.h | 164 ++ world/catenary_scene.cpp | 1602 +++++++++++ world/catenary_scene.h | 121 + world/catenary_scene_objects.cpp | 106 + world/catenary_solver.cpp | 321 +++ world/catenary_solver.h | 88 + world/catenary_span.cpp | 385 +++ world/catenary_span.h | 57 + world/catenary_suspension.cpp | 453 +++ world/catenary_suspension.h | 152 + world/catenary_tensioner.cpp | 619 ++++ world/catenary_tensioner.h | 180 ++ 45 files changed, 12595 insertions(+), 24 deletions(-) create mode 100644 application/editormodecatenary.cpp create mode 100644 application/editormodecatenary.h create mode 100644 shaders/mat_wire.frag create mode 100644 world/catenary_cantilever.cpp create mode 100644 world/catenary_cantilever.h create mode 100644 world/catenary_catalog.cpp create mode 100644 world/catenary_catalog.h create mode 100644 world/catenary_layout.cpp create mode 100644 world/catenary_layout.h create mode 100644 world/catenary_network.cpp create mode 100644 world/catenary_network.h create mode 100644 world/catenary_scene.cpp create mode 100644 world/catenary_scene.h create mode 100644 world/catenary_scene_objects.cpp create mode 100644 world/catenary_solver.cpp create mode 100644 world/catenary_solver.h create mode 100644 world/catenary_span.cpp create mode 100644 world/catenary_span.h create mode 100644 world/catenary_suspension.cpp create mode 100644 world/catenary_suspension.h create mode 100644 world/catenary_tensioner.cpp create mode 100644 world/catenary_tensioner.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 72d3f05c..d3be814d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,16 @@ set(SOURCES "world/Sweep.cpp" "world/RoadPoint.cpp" "world/Traction.cpp" +"world/catenary_solver.cpp" +"world/catenary_network.cpp" +"world/catenary_span.cpp" +"world/catenary_tensioner.cpp" +"world/catenary_suspension.cpp" +"world/catenary_scene.cpp" +"world/catenary_cantilever.cpp" +"world/catenary_layout.cpp" +"world/catenary_catalog.cpp" +"world/catenary_scene_objects.cpp" "world/TractionPower.cpp" "vehicle/Train.cpp" "world/TrkFoll.cpp" @@ -199,6 +209,7 @@ set(SOURCES "application/editormodeselection.cpp" "application/editormodeviewport.cpp" "application/editormodesignal.cpp" +"application/editormodecatenary.cpp" "application/editormodesweep.cpp" "application/editormodeturntable.cpp" "application/editormodeselftest.cpp" @@ -737,4 +748,4 @@ if(WITH_TESTS) # Visual Studio builds rerun them with the next change of the game) set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY LINK_DEPENDS $ ${UNIT_TEST_DATA} "${CMAKE_CURRENT_SOURCE_DIR}/tests/unit/run_tests.cmake") -endif() \ No newline at end of file +endif() diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index e318d356..b27935a7 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Timer.h" #include "world/Event.h" #include "world/TractionPower.h" +#include "world/catenary_span.h" #include "vehicle/Camera.h" #include "world/mtable.h" #include "vehicle/Train.h" @@ -1540,6 +1541,10 @@ debug_panel::render_section_settings() const { if( ImGui::SliderFloat( ( to_string( std::abs( Global.gfx_shadow_angle_min ), 2 ) + "###shadowanglecutoff" ).c_str(), &Global.gfx_shadow_angle_min, -1.0, -0.2f, "Shadow angle cutoff" ) ) { Global.gfx_shadow_angle_min = quantize( Global.gfx_shadow_angle_min, 0.05f ); } + // sieć trakcyjna: zasięg rysowania przewodów, lin i uchwytów jako brył; 0 wyłącza bryły + if( ImGui::SliderFloat( ( to_string( CatenarySettings.solid_range, 0 ) + " m###catenarysolidrange" ).c_str(), &CatenarySettings.solid_range, 0.0f, 500.0f, "3D overhead wires range" ) ) { + CatenarySettings.solid_range = quantize( CatenarySettings.solid_range, 10.0f ); + } if( DebugModeFlag ) { // sky sliders { diff --git a/application/editormode.cpp b/application/editormode.cpp index 32f991c5..641f04c7 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -28,6 +28,7 @@ extern char **environ; #include "editor/editorIncludeInfo.hpp" #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" +#include "world/catenary_scene.h" #include "editor/editorGeometry.hpp" #include "editor/editorIcons.hpp" #include "utilities/translation.h" @@ -1264,6 +1265,7 @@ void editor_mode::redo_one() bool editor_mode::update() { + // the catenary doesn't need the far tracks drawn (50 km of them cost a lot in a big scenery) Global.editor_tracks = (current_work_area() == work_area::tracks); selftest_step(); if (m_vehicle.leave) @@ -1420,6 +1422,11 @@ bool editor_mode::update() signal_move_update(); draw_signal_overlay(); } + if (m_track_tab == track_tab::catenary) + { + catenary_drag_update(); + draw_catenary_overlay(); + } draw_parallel_preview(); if (m_track_tab == track_tab::lineside) { @@ -2901,6 +2908,23 @@ bool editor_mode::save() if (m_profile.changed) profile_store(); infra_store(); + // the catenary (cantilevers on the poles) lives in files of its own; a file made in the editor needs the directive + std::vector catenarynew; + if (catenary_scene_changed()) + { + std::string message; + if (false == catenary_scene_save(message)) + { + ui()->set_status(format(STR_C("The catenary couldn't be saved: %s"), message.c_str()), true); + return false; + } + WriteLog("Editor: catenary written: " + message, logtype::generic); + } + for (auto const &file : catenary_scene_new_files()) + { + rootstatements.emplace_back("catenary " + file + " endcatenary"); + catenarynew.push_back(file); + } if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) { m_saved_camera = std::make_pair(glm::dvec3{Camera.Pos}, glm::vec3{Camera.Angle}); @@ -2926,6 +2950,9 @@ bool editor_mode::save() { scene::Layers.terrain_directive(true); } + if (result.success) + for (auto const &file : catenarynew) + catenary_scene_file_included(file); ui()->set_status(result.message, false == result.success); WriteLog("Editor: " + result.message, logtype::generic); return result.success; @@ -3555,6 +3582,7 @@ void editor_mode::render_environment_window() { Global.AirTemperature = std::clamp(temperature, -35.0f, 40.0f); simulation::Environment.compute_weather(); + catenary_scene_temperature(); // ciężary kotwień idą za temperaturą } ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); ImGui::TextWrapped("%s", STR_C("Seen in the editor only; the scenery keeps its own time and weather")); @@ -4395,7 +4423,7 @@ editor_mode::work_area editor_mode::current_work_area() const if (m_terrain_open) return work_area::terrain; if (m_track_window_open) - return work_area::tracks; + return m_track_tab == track_tab::catenary ? work_area::catenary : work_area::tracks; if (m_roadtool.window) return work_area::roads; return work_area::surroundings; @@ -4411,7 +4439,11 @@ void editor_mode::choose_work_area(work_area const Area) break; case work_area::tracks: terrain_workspace(false); - show_track_tab(m_track_tab); + show_track_tab(m_track_tab == track_tab::catenary ? track_tab::path : m_track_tab); + break; + case work_area::catenary: + terrain_workspace(false); + show_track_tab(track_tab::catenary); break; case work_area::roads: m_track_window_open = false; @@ -4440,8 +4472,9 @@ void editor_mode::render_workspaces() {STR_C("Tracks"), work_area::tracks, editor_icons::icon::work_tracks, STR_C("Tracks and switches, signals, objects along the track, checks of the line")}, {STR_C("Roads"), work_area::roads, editor_icons::icon::work_roads, STR_C("Roads and junctions, level crossings, the points where the traffic comes and goes")}, {STR_C("Terrain"), work_area::terrain, editor_icons::icon::work_terrain, STR_C("Heightmap terrain: chunks, sculpting, painting, water, orthophoto")}, + {STR_C("Catenary"), work_area::catenary, editor_icons::icon::work_catenary, STR_C("The overhead catenary: poles with cantilevers, gantries, the wires of the chains")}, }; - char const *const keys[] = {"F2", "F3", "F4", "F5"}; + char const *const keys[] = {"F2", "F3", "F4", "F5", "F6"}; auto const current{current_work_area()}; auto const &style{ImGui::GetStyle()}; auto *list{ImGui::GetWindowDrawList()}; @@ -4581,7 +4614,7 @@ void editor_mode::render_shortcuts_window() {"Ctrl+Shift+F11", "Export scenery dump"}, {"F11", "Leave the editor"}, {"F10", "Quit"}}}, - {"Fields of work", {{"F2", "Surroundings"}, {"F3", "Tracks"}, {"F4", "Roads"}, {"F5", "Terrain"}}}, + {"Fields of work", {{"F2", "Surroundings"}, {"F3", "Tracks"}, {"F4", "Roads"}, {"F5", "Terrain"}, {"F6", "Catenary"}}}, {"Camera", {{"RMB held, mouse", "Look around"}, {"RMB held, W S A D", "Fly forwards, back, left, right (arrows in the legacy scheme)"}, @@ -5198,6 +5231,8 @@ void editor_mode::delete_selected() // what's selected: a road piece or point, a signal, an include of a scenery template, a model, tracks if (road_delete()) return; + if (m_track_tab == track_tab::catenary && ui()->mode() == nodebank_panel::TRACK && catenary_delete_selected()) + return; if (m_instance != 0 && signal_delete(m_instance)) return; if (m_instance != 0) @@ -5265,6 +5300,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in if (ui()->mode() == nodebank_panel::TRACK && is_press(Action) && (Mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)) == 0 && typed_key(Key)) return; + // the catenary tool may have keys of its own + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::catenary && catenary_on_key(Key, Action, Mods)) + return; + // gizmo transform shortcuts (Q/W/E/R) — only when the camera isn't being flown (RMB up). // handled before the camera keyboard step because Q/W/E are also the fly-mode movement keys, // which would otherwise consume them. @@ -5288,6 +5327,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_F3: choose_work_area(work_area::tracks); break; case GLFW_KEY_F4: choose_work_area(work_area::roads); break; case GLFW_KEY_F5: choose_work_area(work_area::terrain); break; + case GLFW_KEY_F6: choose_work_area(work_area::catenary); break; // the edit modes in the order of the toolbar; in the track mode the digits typed as a value were taken already case GLFW_KEY_1: choose_edit_mode(nodebank_panel::MODIFY); break; case GLFW_KEY_2: choose_edit_mode(nodebank_panel::ADD); break; @@ -5355,6 +5395,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in return; if (Global.ctrlState && Key == GLFW_KEY_Z && is_press(Action)) { + // in the catenary tool its own changes are taken back first + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::catenary && catenary_undo()) + return; undo_last(); return; } @@ -5690,6 +5733,8 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const turntable_finish_placement(); if (m_signal.placing || m_signal.moving) signal_release(); + if (catenary_dragging()) + catenary_release(); } m_dragging = false; diff --git a/application/editormode.h b/application/editormode.h index c7dbd391..28dde2c8 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -48,6 +48,8 @@ struct vehicle_desc; struct skin_set; } +struct catenary_editor; // application/editormodecatenary.h + class editor_mode : public application_mode, private editor_track::observer { @@ -606,7 +608,8 @@ class editor_mode : public application_mode, private editor_track::observer surroundings, tracks, roads, - terrain + terrain, + catenary // the catenary tool of the tracks, as a field of work of its own }; work_area current_work_area() const; void choose_work_area(work_area const Area); @@ -1013,7 +1016,7 @@ class editor_mode : public application_mode, private editor_track::observer void update_build_tools(); void draw_build_overlay() const; // modes of the track editor, one at a time: path is the plain selection - enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals }; + enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, catenary }; struct track_mode { char const *label; @@ -1021,7 +1024,7 @@ class editor_mode : public application_mode, private editor_track::observer track_tab tab; char const *tooltip; }; - static std::array const &track_modes(); + static std::array const &track_modes(); // the objects along the track: a tab for each tool int m_lineside_tab{0}; void render_lineside_ui(); @@ -1078,7 +1081,7 @@ class editor_mode : public application_mode, private editor_track::observer void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, catenary }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -1087,6 +1090,7 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 position{0.0}; editor_track::snap_target snap; std::string action; + int detail{0}; // what the tool of the intent means by it (catenary: what was hit) }; track_intent m_intent; track_intent track_intent_at(int const Mods); @@ -1489,6 +1493,28 @@ class editor_mode : public application_mode, private editor_track::observer std::string signal_intent_label(signal_template const &Template, signal_spot const &Spot) const; std::string signal_variant(signal_template const &Template, signal_spot const &Spot) const; + // catenary (sieć trakcyjna): the whole tool, its state and its workings are in application/editormodecatenary.h/.cpp; + // here only the fixed hooks the rest of the editor calls, so changes of the tool rebuild only its own file + friend struct catenary_editor; + std::shared_ptr m_catenary_editor; + catenary_editor &catenary_tool(); + void catenary_scan(); + bool catenary_intent(track_intent &Intent); // in the catenary tab: what a click would do; false: nothing + void catenary_press(track_intent const &Intent); + void catenary_release(); + bool catenary_dragging() const; + void catenary_drag_update(); + void draw_catenary_overlay() const; + void render_catenary_ui(); + bool catenary_delete_selected(); + bool catenary_undo(); // false: nothing of its own to take back + bool catenary_on_key(int const Key, int const Action, int const Mods); // true: the key was used by the tool + std::string catenary_mode_name() const; + void catenary_key_hints(std::vector &Hints) const; + // the page of the catenary field of work: 0 poles, 1 gantries, 2 wires + int catenary_page(); + void catenary_choose_page(int const Page); + // a model laid along the track: bent to follow it, or repeated along it struct sweep_tool { diff --git a/application/editormodecatenary.cpp b/application/editormodecatenary.cpp new file mode 100644 index 00000000..4286ec23 --- /dev/null +++ b/application/editormodecatenary.cpp @@ -0,0 +1,3286 @@ +/* +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/. +*/ + +// Catenary in the editor: poles with cantilevers put by the track where the user sees them (on the orthophoto). The card of +// the cantilever is chosen from the catalogue by the distance from the wires to the face of the pole (band A, last digit of +// the card), the cantilever is assembled by the same solver the simulation uses and shown at once. Nothing is laid out +// automatically: the spans between the poles the user put are only checked against the tables of the catalogue. + +#include "stdafx.h" +#include "application/editormodecatenary.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "world/Track.h" +#include "world/catenary_scene.h" +#include "world/catenary_layout.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_of; + +double const kRailTop{0.18}; // m from the points of a track to the heads of its rails +double const kCatenaryRange{1500.0}; // m from the camera, farther poles aren't drawn nor picked +double const kSpanSearch{150.0}; // m, the farthest a neighbouring pole of the same line is looked for +double const kSameLine{1.5}; // m across the span, farther the two poles belong to different tracks +double const kCurveRadius{4000.0}; // m, a track bent less is a straight for the cards (table 0007) +float const kPolePick{12.0f}; +double const kSwitchRange{8.0}; // m from a pole to a switch path: the pole stands by the switch (also on the neighbouring track) + +double nearest_parameter(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + bezier const curve{Track.m_paths[Path]}; + auto parameter{0.0}; + auto best{std::numeric_limits::max()}; + for (int k = 0; k <= 256; ++k) + { + auto const distance{geometry::plan_distance(curve.point(k / 256.0), Point)}; + if (distance < best) + { + best = distance; + parameter = k / 256.0; + } + } + return parameter; +} + +glm::dvec2 forward_at(TTrack const &Track, int const Path, double const Parameter) +{ + auto const tangent{bezier{Track.m_paths[Path]}.first(Parameter)}; + glm::dvec2 const planar{tangent.x, tangent.z}; + return glm::length(planar) > 1e-9 ? glm::normalize(planar) : glm::dvec2{0.0, 1.0}; +} + +glm::dvec2 right_of(glm::dvec2 const &Direction) +{ + return {-Direction.y, Direction.x}; +} + +// the pole the suspension stands on, or the point it's attached at +glm::dvec3 pole_of(catenary::suspension const &Data) +{ + return Data.has_pole ? glm::dvec3{Data.pole[0], Data.pole[1], Data.pole[2]} : glm::dvec3{Data.attach[0], Data.attach[1], Data.attach[2]}; +} + +glm::dvec3 wire_of(catenary::suspension const &Data) +{ + return {Data.at[0], Data.at[1], Data.at[2]}; +} + +// from the pole towards the wires, in the plan; across the track +glm::dvec2 across_of(catenary::suspension const &Data) +{ + auto const delta{plan_of(wire_of(Data)) - plan_of(pole_of(Data))}; + return glm::length(delta) > 1e-6 ? glm::normalize(delta) : glm::dvec2{1.0, 0.0}; +} + +// the top of the pole over its point; a drop hangs down from its point, so its "top" is negative +double pole_top(catenary::suspension const &Data) +{ + auto const *catalog{catenary_scene_catalog()}; + auto const *pole{catalog != nullptr && Data.has_pole ? catalog->pole(Data.pole_type) : nullptr}; + if (pole != nullptr && false == pole->drop.empty()) + return -pole->length; + return pole != nullptr ? pole->top : 7.0; +} + +// spans to check against the catalogue: the spans of the chains, and between the poles which aren't in any chain the pairs of +// the same line next to each other. Two suspensions are of the same line when the chord between their wires runs along the +// track: across the bisector of their directions it's off no more than the staggers of the wires make (the wires of the +// neighbouring track are 4 m and more away) +struct span_check +{ + std::size_t from{0}, to{0}; + double length{0.0}; + double radius{0.0}; + double limit{0.0}; + int chain{-1}; +}; + +// radius of the track over the span from the change of the direction of the cantilevers: R = length / angle +double span_radius(catenary::suspension const &A, catenary::suspension const &B, double const Length) +{ + auto const across{across_of(A)}; + auto otheracross{across_of(B)}; + if (glm::dot(otheracross, across) < 0.0) + otheracross = -otheracross; + auto const angle{std::acos(std::clamp(glm::dot(across, otheracross), -1.0, 1.0))}; + return angle > 1e-4 ? Length / angle : 0.0; +} + +std::vector spans_around(glm::dvec3 const &Camera, std::string const &Network, int const Zone) +{ + auto const &tables{catenary_scene_layout()}; + std::vector result; + auto const add = [&](std::size_t const From, std::size_t const To, std::string const &Symbol, int const Chain) { + auto const &a{*catenary_scene_get(From)}; + auto const &b{*catenary_scene_get(To)}; + span_check span; + span.from = From; + span.to = To; + span.chain = Chain; + span.length = glm::length(plan_of(wire_of(b)) - plan_of(wire_of(a))); + span.radius = span_radius(a, b, span.length); + span.limit = catenary::span_limit(tables, Symbol.empty() ? Network : Symbol, Zone, span.radius > kCurveRadius ? 0.0 : span.radius, tables.curve_stagger()); + result.push_back(span); + }; + std::vector chained(catenary_scene_size(), false); + for (std::size_t chain = 0; chain < catenary_chain_size(); ++chain) + { + auto const *data{catenary_chain_get(chain)}; + if (data == nullptr) + continue; + auto const members{catenary_chain_members(chain)}; + for (std::size_t k = 0; k < members.size(); ++k) + { + if (members[k] < chained.size()) + chained[members[k]] = true; + if (k + 1 >= members.size() || catenary_scene_get(members[k]) == nullptr || catenary_scene_get(members[k + 1]) == nullptr) + continue; + if (glm::distance(wire_of(*catenary_scene_get(members[k])), Camera) > kCatenaryRange) + continue; + add(members[k], members[k + 1], data->type, static_cast(chain)); + } + } + std::vector nearby; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data != nullptr && data->has_at && false == chained[index] && glm::distance(wire_of(*data), Camera) < kCatenaryRange) + nearby.push_back(index); + } + std::vector> pairs; + for (auto const i : nearby) + { + auto const &a{*catenary_scene_get(i)}; + auto const across{across_of(a)}; + glm::dvec2 const along{-across.y, across.x}; + for (int direction : {1, -1}) + { + auto best{kSpanSearch}; + std::size_t found{i}; + for (auto const j : nearby) + { + if (j == i) + continue; + auto const &b{*catenary_scene_get(j)}; + auto const chord{plan_of(wire_of(b)) - plan_of(wire_of(a))}; + auto const length{glm::length(chord)}; + if (length < 1.0 || length >= best || glm::dot(chord, along) * direction <= 0.0) + continue; + // the directions of both suspensions turned to one sense; their bisector is the direction of the chord on a curve + auto otheracross{across_of(b)}; + if (glm::dot(otheracross, across) < 0.0) + otheracross = -otheracross; + auto const bisector{glm::normalize(across + otheracross)}; + if (std::abs(glm::dot(chord, bisector)) > kSameLine) + continue; + best = length; + found = j; + } + if (found != i) + pairs.emplace_back(std::min(i, found), std::max(i, found)); + } + } + // a span found from both ends counts once + std::sort(pairs.begin(), pairs.end()); + pairs.erase(std::unique(pairs.begin(), pairs.end()), pairs.end()); + for (auto const &pair : pairs) + add(pair.first, pair.second, {}, -1); + return result; +} + +// distance on the screen from the mouse to the segment between two points, the largest float if it's behind the camera +float mouse_to_segment(screen_projection const &Projection, glm::dvec3 const &A, glm::dvec3 const &B) +{ + ImVec2 a, b; + if (false == Projection.project(A, a) || false == Projection.project(B, b)) + return std::numeric_limits::max(); + auto const &mouse{ImGui::GetIO().MousePos}; + ImVec2 const span{b.x - a.x, b.y - a.y}; + auto const length2{span.x * span.x + span.y * span.y}; + auto const t{length2 > 1e-6f ? std::clamp(((mouse.x - a.x) * span.x + (mouse.y - a.y) * span.y) / length2, 0.0f, 1.0f) : 0.0f}; + auto const dx{a.x + span.x * t - mouse.x}; + auto const dy{a.y + span.y * t - mouse.y}; + return dx * dx + dy * dy; +} + +char const *const kExecutions[] = {"1 - two contact wires, light arms, hk 1.70", "2 - two contact wires, heavy arms, hk 1.70", "3 - one contact wire, heavy arms, hk 1.70", + "4 - one contact wire, heavy arms, hk 1.30", "5 - one contact wire, light arms, hk 1.70"}; +char const *const kMaterials[] = {"cu", "al"}; +// replaceable textures of the gantries (the masts, beams and drops have none of their own) +char const *const kGantrySkins[] = {"siec_trakcyjna/bramki_new1", "siec_trakcyjna/bramki_new2", "siec_trakcyjna/bramki_old1", "siec_trakcyjna/bramki_old2", "siec_trakcyjna/bramki_old3"}; + +// a pole worked on by itself: for that while no gantry counts as selected (otherwise the spot would hang on a drop) +struct without_gantry +{ + int &selected; + int saved; + explicit without_gantry(int &Selected) : selected{Selected}, saved{Selected} { selected = -1; } + ~without_gantry() { selected = saved; } +}; +// two mast points of gantries are one mast (a gantry of many spans) when they're this close in the plan +bool same_mast(double const *A, double const *B) +{ + auto const dx{A[0] - B[0]}, dz{A[2] - B[2]}; + return dx * dx + dz * dz < 0.0025; +} + +// the spans of the gantry: Gantry and all joined with it on shared masts, Gantry first +std::vector gantry_group(std::size_t const Gantry, std::size_t const Without = std::numeric_limits::max()) +{ + std::vector group; + if (catenary_gantry_get(Gantry) == nullptr) + return group; + group.push_back(Gantry); + for (std::size_t next = 0; next < group.size(); ++next) + { + auto const *span{catenary_gantry_get(group[next])}; + for (std::size_t other = 0; span != nullptr && other < catenary_gantry_size(); ++other) + { + auto const *candidate{catenary_gantry_get(other)}; + if (candidate == nullptr || other == Without || std::find(group.begin(), group.end(), other) != group.end()) + continue; + for (int a = 0; a < 2; ++a) + for (int b = 0; b < 2; ++b) + if (false == span->masts[a].empty() && false == candidate->masts[b].empty() && same_mast(span->points[a], candidate->points[b]) && std::find(group.begin(), group.end(), other) == group.end()) + group.push_back(other); + } + } + return group; +} + +// how many other spans stand on this mast point +int spans_at(double const *Point, std::size_t const Except) +{ + int count{0}; + for (std::size_t other = 0; other < catenary_gantry_size(); ++other) + { + auto const *candidate{catenary_gantry_get(other)}; + if (candidate != nullptr && other != Except && ((false == candidate->masts[0].empty() && same_mast(candidate->points[0], Point)) || (false == candidate->masts[1].empty() && same_mast(candidate->points[1], Point)))) + ++count; + } + return count; +} + +// the items of a combo of poles or masts, by the groups of the described catalogue (foundation, where they stand), the +// rest at the end; Tag gives a word added to a name (e.g. through / end mast). Returns true when another one was chosen +bool grouped_items(std::vector const &Names, int &Chosen, std::function const &Tag = {}) +{ + auto const &structures{catenary_scene_structures()}; + auto changed{false}; + auto const item = [&](int const Index) { + auto label{" " + Names[Index]}; + if (Tag) + label += Tag(Names[Index]); + if (ImGui::Selectable(label.c_str(), Index == Chosen)) + { + changed = Index != Chosen; + Chosen = Index; + } + }; + std::vector shown(Names.size(), false); + for (auto const &group : structures.polegroups()) + { + std::vector members; + for (auto const &name : group.poles) + if (auto const found{std::find(Names.begin(), Names.end(), name)}; found != Names.end()) + members.push_back(static_cast(found - Names.begin())); + if (members.empty()) + continue; + std::string tags; + if (group.foundation == "piles") + tags += STR(", on piles"); + else if (group.foundation == "cast") + tags += STR(", on a cast foundation"); + if (group.site == "line") + tags += STR(", on the line"); + else if (group.site == "station") + tags += STR(", in a station"); + else if (group.site == "platform") + tags += STR(", on a platform"); + ImGui::TextDisabled("%s%s", group.title.c_str(), tags.c_str()); + if (ImGui::IsItemHovered() && false == group.text.empty()) + ImGui::SetTooltip("%s", group.text.c_str()); + for (auto const index : members) + { + shown[index] = true; + item(index); + } + } + if (std::find(shown.begin(), shown.end(), false) != shown.end()) + { + if (false == structures.polegroups().empty()) + ImGui::TextDisabled("%s", STR_C("Other (not described in traction_catalog.txt)")); + for (int i = 0; i < static_cast(Names.size()); ++i) + if (false == shown[i]) + item(i); + } + return changed; +} + +// the states of the wires in the panel, and as they're written in the entries +char const *const kStates[] = {"new", "worn", "tarnished"}; +int const kStateValues[] = {0, 2, 1}; + +} // namespace + +void catenary_editor::catenary_scan() +{ + auto &tool{m_catenary}; + if (tool.scanned) + return; + tool.scanned = true; + tool.poles.clear(); + if (auto const *catalog{catenary_scene_catalog()}; catalog != nullptr) + { + for (auto const &pole : catalog->poles()) + if (pole.second.drop.empty() && false == pole.second.gantry) + tool.poles.push_back(pole.first); + tool.masts.clear(); + for (auto const &mast : catalog->masts()) + tool.masts.push_back(mast.first); + } + // the first group of the described catalogue to start with: the tubular swinging ones, if they're there + if (auto const &structures{catenary_scene_structures()}; false == structures.empty() && structures.group(tool.catalog_group) == nullptr) + { + auto const *start{structures.group("t_tube_swing")}; + if (start == nullptr) + start = &structures.groups().front(); + tool.catalog_group = start->id; + tool.catalog_kind = start->category; + } + tool.networks = catenary_scene_layout().symbols(); + auto const preferred{std::find(tool.networks.begin(), tool.networks.end(), std::string{"YwsC120-2C"})}; + tool.network = preferred != tool.networks.end() ? static_cast(preferred - tool.networks.begin()) : 0; + auto const pole{std::find(tool.poles.begin(), tool.poles.end(), std::string{"1611-11"})}; + tool.pole = pole != tool.poles.end() ? static_cast(pole - tool.poles.begin()) : 0; + if (tool.file[0] == '\0') + { + auto const files{catenary_scene_files()}; + auto name{files.empty() ? std::filesystem::path{Global.SceneryFile}.stem().string() + "_siec.scm" : files.front()}; + std::snprintf(tool.file, sizeof(tool.file), "%s", name.c_str()); + } +} + +bool catenary_editor::catenary_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Side, catenary_spot &Spot) const +{ + if (Track.m_paths.empty() || Track.m_editorremoved) + return false; + auto const path{std::clamp(Path, 0, static_cast(Track.m_paths.size()) - 1)}; + bezier const curve{Track.m_paths[path]}; + auto const parameter{nearest_parameter(Track, path, Point)}; + Spot.track = &Track; + Spot.path = path; + Spot.parameter = parameter; + Spot.axis = curve.point(parameter); + Spot.forward = forward_at(Track, path, parameter); + auto const right{right_of(Spot.forward)}; + Spot.side = Side != 0 ? Side : (glm::dot(plan_of(Cursor) - plan_of(Spot.axis), right) >= 0.0 ? 1 : -1); + // the radius over about 10 m each way; on a curve the wires go to the outside of it (table 0007) + auto const length{std::max(curve.plan_length(), 1.0)}; + auto const step{std::min(10.0 / length, 0.5)}; + auto const before{curve.point(std::max(0.0, parameter - step))}; + auto const after{curve.point(std::min(1.0, parameter + step))}; + double a[2]{before.x, before.z}, b[2]{Spot.axis.x, Spot.axis.z}, c[2]{after.x, after.z}; + Spot.radius = catenary::plan_radius(a, b, c); + switch (m_catenary.bend) + { + case 1: Spot.curve = false; break; + case 2: Spot.curve = true; break; + default: Spot.curve = Spot.radius > 0.0 && Spot.radius < kCurveRadius; break; + } + Spot.toward = m_catenary.toward; + if (Spot.curve && Spot.radius > 0.0) + { + // the middle of the arc is outwards of the chord: is the pole on the outside of the curve + Spot.toward = glm::dot(plan_of(Spot.axis) - 0.5 * (plan_of(before) + plan_of(after)), right) * Spot.side > 0.0; + } + auto const stagger{Spot.curve ? std::max(static_cast(m_catenary.stagger), catenary_scene_layout().curve_stagger()) : static_cast(m_catenary.stagger)}; + catenary_compose(Spot, m_catenary.clearance, stagger, Spot.toward); + // a gantry is selected: the cantilever hangs on a drop on its beam + glm::dvec3 from, to; + double bottom{0.0}; + auto const *catalog{catenary_scene_catalog()}; + if (m_catenary.gantry_placing || m_catenary.gantry_selected < 0 || catalog == nullptr || false == catenary_gantry_beam(static_cast(m_catenary.gantry_selected), from, to, bottom)) + return true; + auto const *gantry{catenary_gantry_get(static_cast(m_catenary.gantry_selected))}; + if (gantry != nullptr && m_catenary.gantry_hang == 1) + return catenary_on_mast(*gantry, stagger, Spot); + auto const *beam{gantry != nullptr ? catalog->beam(gantry->beam) : nullptr}; + std::vector> drops; + for (auto const &pole : catalog->poles()) + if (beam != nullptr && pole.second.drop == beam->drop) + drops.emplace_back(pole.second.length, pole.first); + std::sort(drops.begin(), drops.end()); + if (drops.empty()) + { + Spot.card = 0; + Spot.problem = STR("No drops in the catalogue for this beam"); + return true; + } + // along the track onto the line of the beam + catenary_compose(Spot, m_catenary.clearance, stagger, Spot.toward, drops.front().second); + auto const p{plan_of(glm::dvec3{Spot.data.pole[0], 0.0, Spot.data.pole[2]})}; + auto const f{Spot.forward}; + auto const origin{plan_of(from)}; + auto const u{plan_of(to) - origin}; + auto const denominator{f.x * u.y - f.y * u.x}; + if (std::abs(denominator) < 1e-6) + { + Spot.card = 0; + Spot.problem = STR("The track runs along the beam of the gantry"); + return true; + } + auto const s{((origin.x - p.x) * u.y - (origin.y - p.y) * u.x) / denominator}; + auto const t{((origin.x - p.x) * f.y - (origin.y - p.y) * f.x) / denominator}; + if (t < 0.0 || t > 1.0) + { + Spot.card = 0; + Spot.problem = STR("The drop would be off the beam of the gantry"); + return true; + } + Spot.axis.x += f.x * s; + Spot.axis.z += f.y * s; + bottom = from.y + (to.y - from.y) * t; // the beam may lie aslant between masts of different clearance + // the shortest drop which reaches down to the lowest mount of the cantilever + auto const take = [&](std::string const &Name) { + catenary_compose(Spot, m_catenary.clearance, stagger, Spot.toward, Name); + Spot.data.pole[1] = bottom; + }; + // the drop chosen in the panel, if it's one for this beam + if (false == m_catenary.drop_choice.empty() && std::any_of(drops.begin(), drops.end(), [&](auto const &Drop) { return Drop.second == m_catenary.drop_choice; })) + { + take(m_catenary.drop_choice); + return true; + } + for (auto const &drop : drops) + { + take(drop.second); + if (Spot.card == 0) + return true; + catenary::cantilever_result probe; + catenary_scene_probe(Spot.data, probe); + if (std::none_of(probe.warnings.begin(), probe.warnings.end(), [](std::string const &Warning) { return Warning.find("below the lowest mount") != std::string::npos; })) + return true; + } + take(drops.back().second); + return true; +} + +// the line of the beam of the gantry at its bottom (where the drops hang from) +bool catenary_editor::catenary_gantry_beam(std::size_t const Index, glm::dvec3 &From, glm::dvec3 &To, double &Bottom) const +{ + auto const *gantry{catenary_gantry_get(Index)}; + auto const *catalog{catenary_scene_catalog()}; + auto const *first{gantry != nullptr && catalog != nullptr ? catalog->mast(gantry->masts[0]) : nullptr}; + auto const single{gantry != nullptr && gantry->masts[1].empty()}; + auto const *second{gantry != nullptr && catalog != nullptr && false == single ? catalog->mast(gantry->masts[1]) : nullptr}; + auto const *beam{gantry != nullptr && catalog != nullptr ? catalog->beam(gantry->beam) : nullptr}; + if (first == nullptr || (second == nullptr && false == single) || beam == nullptr) + return false; + // each end on the mount of its own mast; a single element from the face of its mast for its length + double from[3], to[3]; + gantry_beam_ends(*gantry, *first, second, beam->span, from, to); + From = {from[0], from[1], from[2]}; + To = {to[0], to[1], to[2]}; + Bottom = From.y; + return true; +} + +glm::dvec3 catenary_editor::catenary_mast_point() const +{ + // on the ground, at the height of the rail heads of the track under the cursor if there's one + auto point{e.cursor_ground()}; + if (e.m_hover.track != nullptr) + { + point.y = e.m_hover.point.y + kRailTop; + // the first mast: its face at the set distance from the axis of that track, on the side of the cursor + auto const &tool{m_catenary}; + auto const *catalog{catenary_scene_catalog()}; + if (tool.gantry_snap && false == tool.gantry_first.has_value() && false == e.m_hover.track->m_paths.empty() && catalog != nullptr && false == tool.masts.empty()) + { + auto const path{std::clamp(e.m_hover.path, 0, static_cast(e.m_hover.track->m_paths.size()) - 1)}; + bezier const curve{e.m_hover.track->m_paths[path]}; + auto const parameter{nearest_parameter(*e.m_hover.track, path, point)}; + auto const axis{curve.point(parameter)}; + auto const right{right_of(forward_at(*e.m_hover.track, path, parameter))}; + auto const side{glm::dot(plan_of(point) - plan_of(axis), right) >= 0.0 ? 1.0 : -1.0}; + auto const *mast{catalog->mast(tool.masts[std::clamp(tool.mast_first, 0, static_cast(tool.masts.size()) - 1)])}; + auto const half{mast != nullptr ? mast->offset : 0.0}; + auto const at{plan_of(axis) + right * side * (static_cast(tool.gantry_face) + half)}; + point = {at.x, axis.y + kRailTop, at.y}; + } + } + return point; +} + +bool catenary_editor::catenary_mast_face(std::string const &Mast, glm::dvec3 const &Point, double &Face, glm::dvec2 &Away) const +{ + auto const *catalog{catenary_scene_catalog()}; + auto const *mast{catalog != nullptr ? catalog->mast(Mast) : nullptr}; + auto best{20.0}; + auto found{false}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + { + bezier const curve{track->m_paths[path]}; + if (geometry::plan_distance(curve.point(0.5), Point) > curve.plan_length() * 0.5 + best) + continue; + auto const parameter{nearest_parameter(*track, path, Point)}; + auto const axis{curve.point(parameter)}; + auto const distance{geometry::plan_distance(axis, Point)}; + if (distance < best && distance > 1e-3) + { + best = distance; + Away = glm::normalize(plan_of(Point) - plan_of(axis)); + found = true; + } + } + } + // the face towards the track: the half of the mast across it as the drops and the beam take it (its offset) + Face = best - (mast != nullptr ? mast->offset : 0.0); + return found; +} + +bool catenary_editor::catenary_along_track(glm::dvec3 const &Point, double const Axis, glm::dvec3 &Place) const +{ + // of the tracks near, the one the point is nearest to the wanted distance from: the same track as before, also over its joints + auto best{std::numeric_limits::max()}; + auto found{false}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + { + bezier const curve{track->m_paths[path]}; + if (geometry::plan_distance(curve.point(0.5), Point) > curve.plan_length() * 0.5 + Axis + 10.0) + continue; + auto const parameter{nearest_parameter(*track, path, Point)}; + auto const axis{curve.point(parameter)}; + auto const distance{geometry::plan_distance(axis, Point)}; + // off the end of the path: the point isn't square to it + auto const forward{forward_at(*track, path, parameter)}; + if ((parameter <= 1e-6 || parameter >= 1.0 - 1e-6) && std::abs(glm::dot(plan_of(Point) - plan_of(axis), forward)) > 0.05) + continue; + if (std::abs(distance - Axis) >= best) + continue; + best = std::abs(distance - Axis); + auto const right{right_of(forward)}; + auto const side{glm::dot(plan_of(Point) - plan_of(axis), right) >= 0.0 ? 1.0 : -1.0}; + auto const at{plan_of(axis) + right * side * Axis}; + Place = {at.x, Point.y, at.y}; + found = true; + } + } + return found && best < 10.0; +} + +void catenary_editor::catenary_gantry_shift(glm::dvec2 const &Shift) +{ + auto &tool{m_catenary}; + if (tool.gantry_selected < 0 || glm::length(Shift) < 1e-6) + return; + std::vector> spans; + std::vector> hangers; + for (auto const span : gantry_group(static_cast(tool.gantry_selected))) + { + spans.emplace_back(span, *catenary_gantry_get(span)); + catenary_gantry_hangers(span, hangers); + } + // dragging the distance: the state before the drag is taken back at once + auto const record{tool.shift_group == 0 || tool.undo.empty() || tool.undo.back().group != tool.shift_group}; + if (record) + tool.shift_group = ++tool.group; + for (auto const &[span, before] : spans) + { + if (false == record) + break; + catenary_change change; + change.group = tool.group; + change.gantry = true; + change.index = span; + change.gantry_before = before; + tool.undo.push_back(change); + } + if (record) + for (auto const &[index, before] : hangers) + tool.undo.push_back({tool.group, false, index, before, std::nullopt, false}); + for (auto const &[span, before] : spans) + { + auto moved{before}; + for (auto &point : moved.points) + { + point[0] += Shift.x; + point[2] += Shift.y; + } + catenary_gantry_update(span, moved); + } + for (auto const &[index, before] : hangers) + { + auto shifted{before}; + for (auto *point : {shifted.at, shifted.rope, shifted.pole, shifted.attach}) + { + point[0] += Shift.x; + point[2] += Shift.y; + } + catenary_scene_update(index, shifted); + tool.spots.erase(index); + } +} + +// the second mast: towards the cursor, as far as the nearest beam of the catalogue spans +bool catenary_editor::catenary_gantry_second(glm::dvec3 const &Point, glm::dvec3 &Second, std::string &Beam) const +{ + auto const &tool{m_catenary}; + auto const *catalog{catenary_scene_catalog()}; + if (catalog == nullptr || false == tool.gantry_first.has_value()) + return false; + auto const first{*tool.gantry_first}; + auto const delta{plan_of(Point) - plan_of(first)}; + auto distance{glm::length(delta)}; + if (tool.gantry_square && tool.gantry_track.has_value()) + distance = std::abs(glm::dot(right_of(*tool.gantry_track), delta)); // square to the track: only the distance across it counts + if (distance < 1.0) + return false; + // the beam runs between the faces of the masts: their axes are farther apart by the offsets of both + auto const *a{catalog->mast(tool.masts.empty() ? std::string{} : tool.masts[std::clamp(tool.mast_first, 0, static_cast(tool.masts.size()) - 1)])}; + auto const *b{catalog->mast(tool.masts.empty() ? std::string{} : tool.masts[std::clamp(tool.mast_second, 0, static_cast(tool.masts.size()) - 1)])}; + // a single element: from the face of its one mast for its length; otherwise between the faces of the masts 0.20 m less than the beam + auto const offsets{tool.gantry_single ? (a != nullptr ? a->offset : 0.0) : (a != nullptr ? a->offset : 0.0) + (b != nullptr ? b->offset : 0.0) - 0.20}; + auto best{std::numeric_limits::max()}; + double span{0.0}; + for (auto const &beam : catalog->beams()) + if (beam.second.single == tool.gantry_single && std::abs(beam.second.span + offsets - distance) < best) + { + best = std::abs(beam.second.span + offsets - distance); + span = beam.second.span; + Beam = beam.first; + } + if (span <= 0.0) + return false; + auto direction{delta / distance}; + if (tool.gantry_square && tool.gantry_track.has_value()) + { + // square to the track by the first mast, on the side of the cursor + auto const across{right_of(*tool.gantry_track)}; + direction = glm::dot(across, delta) >= 0.0 ? across : -across; + } + // both masts at the height of the first one (the rail heads of the track by it); their heights can be changed by 0.20 m later + Second = {first.x + direction.x * (span + offsets), first.y, first.z + direction.y * (span + offsets)}; + return true; +} + +int catenary_editor::catenary_gantry_hit() const +{ + screen_projection const projection; + auto best{kPolePick * kPolePick}; + int result{-1}; + for (std::size_t index = 0; index < catenary_gantry_size(); ++index) + { + glm::dvec3 from, to; + double bottom{0.0}; + if (false == catenary_gantry_beam(index, from, to, bottom) || glm::distance(from, glm::dvec3{Global.pCamera.Pos}) > kCatenaryRange) + continue; + auto const distance{mouse_to_segment(projection, from, to)}; + if (distance < best) + { + best = distance; + result = static_cast(index); + } + } + return result; +} + +// the cantilever straight on a mast of the gantry: the mast on the side of the cursor, the cantilever across the track at the +// mast (the spot moved along the track to it), the card from the distance of the face of the mast +bool catenary_editor::catenary_on_mast(catenary::gantry const &Gantry, double const Stagger, catenary_spot &Spot) const +{ + auto const *catalog{catenary_scene_catalog()}; + auto const right{right_of(Spot.forward) * static_cast(Spot.side)}; + int best{-1}; + auto nearest{std::numeric_limits::max()}; + for (int k = 0; k < 2; ++k) + { + if (Gantry.masts[k].empty()) + continue; + glm::dvec3 const mast{Gantry.points[k][0], Gantry.points[k][1], Gantry.points[k][2]}; + auto const across{glm::dot(plan_of(mast) - plan_of(Spot.axis), right)}; + if (across > 0.3 && across < nearest) + { + nearest = across; + best = k; + } + } + Spot.card = 0; + if (best < 0) + { + Spot.problem = STR("No mast of this gantry on that side of the track"); + return true; + } + auto const &name{Gantry.masts[best]}; + auto const *pole{catalog != nullptr ? catalog->pole(name) : nullptr}; + if (pole == nullptr || false == pole->gantry) + { + Spot.problem = format(STR_C("Mast %s has no mounts for a cantilever (strut/stay in its entry in traction_supports.txt)"), name.c_str()); + return true; + } + glm::dvec3 const mast{Gantry.points[best][0], Gantry.points[best][1], Gantry.points[best][2]}; + // along the track onto the mast + Spot.parameter = nearest_parameter(*Spot.track, Spot.path, mast); + Spot.axis = bezier{Spot.track->m_paths[Spot.path]}.point(Spot.parameter); + Spot.forward = forward_at(*Spot.track, Spot.path, Spot.parameter); + auto const out{right_of(Spot.forward) * static_cast(Spot.side)}; + auto const clearance{glm::dot(plan_of(mast) - plan_of(Spot.axis), out) - 0.5 * pole->depth(0.0)}; + catenary_compose(Spot, clearance, Stagger, Spot.toward, name); + Spot.data.pole[0] = mast.x; + Spot.data.pole[1] = mast.y; + Spot.data.pole[2] = mast.z; + return true; +} + +// whether a pole of this type can't stand there: its group is no_switch (concrete 1701, 1711) and the track it stands by is a switch, +// or a switch path passes within kSwitchRange of its foot (also a switch on the neighbouring track) +bool catenary_editor::catenary_switch_error(std::string const &Pole, glm::dvec3 const &Foot, TTrack const *Track) const +{ + auto const *group{catenary_scene_structures().polegroup_of(Pole)}; + if (group == nullptr || false == group->no_switch) + return false; + if (Track != nullptr && Track->eType == tt_Switch) + return true; + auto const &tracks{simulation::Paths.sequence()}; + if (tracks.size() != m_switch_tracks) + { + m_switch_tracks = tracks.size(); + m_switch_paths.clear(); + m_switch_checks.clear(); + for (auto const *track : tracks) + if (track != nullptr && track->eType == tt_Switch) + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + m_switch_paths.emplace_back(track, path); + } + for (auto const &[track, path] : m_switch_paths) + { + if (track->m_editorremoved) + continue; + bezier const curve{track->m_paths[path]}; + if (geometry::plan_distance(curve.point(0.5), Foot) > curve.plan_length() * 0.5 + kSwitchRange) + continue; + if (geometry::plan_distance(curve.point(nearest_parameter(*track, path, Foot)), Foot) < kSwitchRange) + return true; + } + return false; +} + +bool catenary_editor::catenary_switch_error(std::size_t const Index) const +{ + auto const *data{catenary_scene_get(Index)}; + if (data == nullptr || false == data->has_pole) + return false; + auto const foot{pole_of(*data)}; + auto &check{m_switch_checks[Index]}; + if (check.pole != data->pole_type || check.foot != foot) + { + check.pole = data->pole_type; + check.foot = foot; + check.error = catenary_switch_error(data->pole_type, foot, nullptr); + } + return check.error; +} + +void catenary_editor::catenary_compose(catenary_spot &Spot, double const Clearance, double const Stagger, bool const Toward, std::string const &Pole) const +{ + auto const &tool{m_catenary}; + Spot.card = 0; + Spot.problem.clear(); + Spot.toward = Toward; + auto const *catalog{catenary_scene_catalog()}; + auto const *pole{catalog == nullptr ? nullptr : false == Pole.empty() ? catalog->pole(Pole) : tool.pole >= 0 && tool.pole < static_cast(tool.poles.size()) ? catalog->pole(tool.poles[tool.pole]) : nullptr}; + if (pole == nullptr) + { + Spot.problem = STR("No pole: the catalogue (data/traction_cantilevers.txt, data/traction_supports.txt) isn't loaded or has no poles"); + return; + } + auto const height{static_cast(tool.height)}; + auto const railhead{Spot.axis.y + kRailTop}; + auto const right{right_of(Spot.forward) * static_cast(Spot.side)}; // from the track to the pole + glm::dvec3 const out{right.x, 0.0, right.y}; + auto const axisdistance{Clearance + 0.5 * pole->depth(0.0)}; + auto const offset{Toward ? Stagger : -Stagger}; + auto const polepoint{glm::dvec3{Spot.axis.x, railhead, Spot.axis.z} + out * axisdistance}; + auto const wire{glm::dvec3{Spot.axis.x, railhead + height, Spot.axis.z} + out * offset}; + Spot.reach = axisdistance - offset - 0.5 * pole->depth(height); + // the card: from the group of the described catalogue, or (without it) from the tubular families 11xx/12xx + catenary::cantilever_scheme const *scheme{nullptr}; + auto const &structures{catenary_scene_structures()}; + auto const described{false == structures.empty() && structures.group(tool.catalog_group) != nullptr}; + int decade{0}; + std::string pickproblem; + Spot.family.clear(); + Spot.execution_changed = false; + if (described) + { + auto const pick{catenary::pick_card(structures, *catalog, tool.catalog_group, tool.catalog_family, tool.catalog_card, Spot.curve, Toward, tool.execution, Spot.reach)}; + scheme = pick.scheme; + if (pick.family != nullptr) + Spot.family = pick.family->id; + Spot.execution_changed = pick.execution_changed; + pickproblem = pick.problem; + } + else + { + catenary::suspension_family family; + family.swinging = tool.swinging; + family.curve = Spot.curve; + family.toward_pole = Toward; + decade = catenary::family_decade(family); + scheme = catalog->scheme_for_reach(decade, tool.execution, Spot.reach); + } + auto &data{Spot.data}; + data = catenary::suspension(); + data.range_max = 1000.0; + data.execution = tool.execution; + data.has_at = true; + data.at[0] = wire.x; + data.at[1] = wire.y; + data.at[2] = wire.z; + data.has_pole = true; + data.pole_type = pole->name; + data.pole[0] = polepoint.x; + data.pole[1] = polepoint.y; + data.pole[2] = polepoint.z; + Spot.switch_error = catenary_switch_error(pole->name, polepoint, Spot.track); + if (tool.insulators25) + data.insulators = "25"; + if (scheme == nullptr) + { + if (described) + Spot.problem = pickproblem.find("no group") == 0 ? pickproblem : format(STR_C("No card of the group for A = %.2f m"), Spot.reach); + else + Spot.problem = format(STR_C("No card %d0-%d9 in execution %d for A = %.2f m (cards cover A 1.45-4.45 m, not all of them are in every family)"), decade / 10, decade / 10, tool.execution, Spot.reach); + return; + } + Spot.card = scheme->card; + data.card = scheme->card; + data.execution = scheme->execution; + data.has_rope = true; + data.rope[0] = wire.x; + // the system height: the card's, or the one set in the panel (the solver raises the stay mount with the messenger) + data.rope[1] = wire.y + (tool.system_height > 0.0f ? static_cast(tool.system_height) : scheme->height); + data.rope[2] = wire.z; +} + +// where the suspension stands by the track: the place it was put at in this session, or the nearest path to its wires +catenary_editor::catenary_spot const *catenary_editor::catenary_spot_of(std::size_t const Index) +{ + auto &tool{m_catenary}; + if (auto const found{tool.spots.find(Index)}; found != tool.spots.end()) + return &found->second; + auto const *data{catenary_scene_get(Index)}; + if (data == nullptr || false == data->has_at) + return nullptr; + auto const pole{pole_of(*data)}; + auto const wire{wire_of(*data)}; + TTrack *nearest{nullptr}; + int nearestpath{0}; + auto best{12.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + { + bezier const curve{track->m_paths[path]}; + if (geometry::plan_distance(curve.point(0.5), wire) > curve.plan_length() * 0.5 + best) + continue; + auto const distance{geometry::plan_distance(curve.point(nearest_parameter(*track, path, wire)), wire)}; + if (distance < best) + { + best = distance; + nearest = track; + nearestpath = path; + } + } + } + if (nearest == nullptr) + return nullptr; + catenary_spot spot; + { + without_gantry const plain{tool.gantry_selected}; + catenary_spot_at(*nearest, nearestpath, wire, pole, 0, spot); + } + // what the suspension has, not what the panel would make + auto const right{right_of(spot.forward) * static_cast(spot.side)}; + spot.toward = glm::dot(plan_of(wire) - plan_of(spot.axis), right) >= 0.0; + spot.data = *data; + spot.card = data->card; + auto const decade{(data->card / 10) % 10}; + if (data->card / 100 == 11 || data->card / 100 == 12) + spot.curve = decade == 3 || decade == 4; + return &(tool.spots[Index] = spot); +} + +std::string catenary_editor::catenary_intent_label(catenary_spot const &Spot) const +{ + auto const where{Spot.curve ? format(STR_C("curve R %.0f m"), Spot.radius) : std::string{STR("straight")}}; + auto const offset{Spot.toward ? STR("towards the pole") : STR("away from the pole")}; + if (Spot.card == 0) + return format(STR_C("Pole here, %s, wires %s: %s"), where.c_str(), offset.c_str(), Spot.problem.c_str()); + auto label{format(STR_C("Click: pole %s, card %04d execution %d, A = %.2f m (%s, wires %s)"), Spot.data.pole_type.c_str(), Spot.card, Spot.data.execution, Spot.reach, where.c_str(), offset.c_str())}; + if (auto const *family{catenary_scene_structures().family(Spot.family)}; family != nullptr) + label += "\n" + family->title; + if (Spot.execution_changed) + label += "\n" + format(STR_C("execution %d has no card for this A, execution %d taken"), m_catenary.execution, Spot.data.execution); + if (Spot.switch_error) + label += "\n" + format(STR_C("ERROR: pole %s doesn't stand by a switch, take a steel one"), Spot.data.pole_type.c_str()); + if (m_catenary.chaining && m_catenary.last >= 0 && catenary_scene_get(static_cast(m_catenary.last)) != nullptr) + label += "\n" + format(STR_C("with the span from the last pole, %.1f m"), glm::length(plan_of(wire_of(Spot.data)) - plan_of(wire_of(*catenary_scene_get(static_cast(m_catenary.last)))))); + return label; +} + +int catenary_editor::catenary_hit() const +{ + screen_projection const projection; + auto best{kPolePick * kPolePick}; + int result{-1}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data == nullptr || false == data->has_at) + continue; + auto const foot{pole_of(*data)}; + if (glm::distance(foot, camera) > kCatenaryRange) + continue; + auto const distance{mouse_to_segment(projection, foot, foot + glm::dvec3{0.0, pole_top(*data), 0.0})}; + if (distance < best) + { + best = distance; + result = static_cast(index); + } + } + return result; +} + +// the chain whose span is under the cursor; -1 if none +int catenary_editor::catenary_span_hit() const +{ + screen_projection const projection; + auto best{kPolePick * kPolePick * 0.5f}; + int result{-1}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (std::size_t chain = 0; chain < catenary_chain_size(); ++chain) + { + if (catenary_chain_get(chain) == nullptr) + continue; + auto const members{catenary_chain_members(chain)}; + for (std::size_t k = 0; k + 1 < members.size(); ++k) + { + auto const *a{catenary_scene_get(members[k])}; + auto const *b{catenary_scene_get(members[k + 1])}; + if (a == nullptr || b == nullptr || glm::distance(wire_of(*a), camera) > kCatenaryRange) + continue; + auto const distance{mouse_to_segment(projection, wire_of(*a), wire_of(*b))}; + if (distance < best) + { + best = distance; + result = static_cast(chain); + } + } + } + return result; +} + +void catenary_editor::catenary_select(int const Index) +{ + auto &tool{m_catenary}; + tool.selected = Index; + tool.probed = false; + auto const *data{Index >= 0 ? catenary_scene_get(static_cast(Index)) : nullptr}; + if (data == nullptr) + { + tool.selected = -1; + return; + } + // the chain it's in is shown with it + std::size_t position{0}; + tool.chain_selected = catenary_chain_of(static_cast(Index), position); + // the settings of the panel take what the suspension has, so that changing them changes it + if (auto const found{std::find(tool.poles.begin(), tool.poles.end(), data->pole_type)}; found != tool.poles.end()) + tool.pole = static_cast(found - tool.poles.begin()); + tool.execution = std::clamp(data->execution, 1, 5); + tool.swinging = data->card / 100 != 12; + // the group of its card in the described catalogue: changing the settings keeps it in that group + if (auto const *family{catenary_scene_structures().family_of(data->card)}; family != nullptr) + { + tool.catalog_group = family->group; + if (auto const *group{catenary_scene_structures().group(family->group)}; group != nullptr) + tool.catalog_kind = group->category; + tool.catalog_family.clear(); + tool.catalog_card = 0; + } + tool.insulators25 = data->insulators == "25"; + auto const decade{(data->card / 10) % 10}; + if (data->card / 100 == 11 || data->card / 100 == 12) + tool.bend = (decade == 3 || decade == 4) ? 2 : 1; + auto const *spot{catenary_spot_of(static_cast(Index))}; + if (spot == nullptr) + { + tool.status = STR("The suspension isn't by any track: it can't be changed here"); + return; + } + auto const right{right_of(spot->forward) * static_cast(spot->side)}; + auto const *catalog{catenary_scene_catalog()}; + auto const *pole{catalog != nullptr ? catalog->pole(data->pole_type) : nullptr}; + auto const railhead{spot->axis.y + kRailTop}; + auto const offset{glm::dot(plan_of(wire_of(*data)) - plan_of(spot->axis), right)}; + tool.stagger = static_cast(std::abs(offset)); + tool.toward = offset >= 0.0; + tool.height = static_cast(data->at[1] - railhead); + // its system height: own when it differs from the card's + if (auto const *catalog{catenary_scene_catalog()}; catalog != nullptr && data->has_rope) + { + auto const *scheme{catalog->scheme(data->card, data->execution)}; + auto const hk{data->rope[1] - data->at[1]}; + tool.system_height = scheme != nullptr && std::abs(hk - scheme->height) > 0.005 ? static_cast(hk) : 0.0f; + } + if (pole != nullptr) + tool.clearance = static_cast(glm::dot(plan_of(pole_of(*data)) - plan_of(spot->axis), right) - 0.5 * pole->depth(0.0)); +} + +void catenary_editor::catenary_apply(catenary_spot const &Spot) +{ + auto &tool{m_catenary}; + if (tool.selected < 0) + return; + auto const index{static_cast(tool.selected)}; + auto const *current{catenary_scene_get(index)}; + if (current == nullptr || Spot.card == 0) + return; + auto data{Spot.data}; + // what the panel doesn't set stays as it was + data.name = current->name; + data.range_max = current->range_max; + data.range_min = current->range_min; + data.skin = current->skin; + data.overrides = current->overrides; + data.umbrella = current->umbrella; + // dragging a slider changes it many times: all of that is taken back at once + auto const &last{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; + if (tool.undo.empty() || last.chain || last.index != index || last.removed || false == last.before.has_value()) + tool.undo.push_back({++tool.group, false, index, *current, std::nullopt, false}); + catenary_scene_update(index, data); + tool.spots[index] = Spot; + tool.spots[index].data = data; + tool.probed = false; +} + +// the poles of the chain get the settings of the selected one: pole, its face from the track, execution, height of the wires, +// insulators. Each keeps its own stagger, so the zigzag stays as it is +void catenary_editor::catenary_copy(std::vector const &Targets) +{ + auto &tool{m_catenary}; + ++tool.group; + std::size_t copied{0}, failed{0}; + for (auto const index : Targets) + { + if (static_cast(index) == tool.selected) + continue; + auto const *current{catenary_scene_get(index)}; + auto const *found{catenary_spot_of(index)}; + if (current == nullptr || found == nullptr) + { + ++failed; + continue; + } + auto spot{*found}; + auto const right{right_of(spot.forward) * static_cast(spot.side)}; + auto const offset{glm::dot(plan_of(wire_of(*current)) - plan_of(spot.axis), right)}; + auto const *catalog{catenary_scene_catalog()}; + auto const *own{catalog != nullptr ? catalog->pole(current->pole_type) : nullptr}; + auto const drop{own != nullptr && false == own->drop.empty()}; + if (own != nullptr && own->gantry) + { + catenary_compose(spot, glm::dot(plan_of(pole_of(*current)) - plan_of(spot.axis), right) - 0.5 * own->depth(0.0), std::abs(offset), offset >= 0.0, current->pole_type); + std::copy(std::begin(current->pole), std::end(current->pole), spot.data.pole); + } + else + catenary_compose(spot, tool.clearance, std::abs(offset), offset >= 0.0, drop ? current->pole_type : std::string{}); + if (drop) + spot.data.pole[1] = current->pole[1]; + if (spot.card == 0) + { + ++failed; + continue; + } + auto data{spot.data}; + data.name = current->name; + data.range_max = current->range_max; + data.range_min = current->range_min; + data.skin = current->skin; + data.overrides = current->overrides; + data.umbrella = current->umbrella; + tool.undo.push_back({tool.group, false, index, *current, std::nullopt, false}); + catenary_scene_update(index, data); + tool.spots[index] = spot; + tool.spots[index].data = data; + ++copied; + } + tool.status = failed == 0 ? format(STR_C("Settings copied to %zu poles"), copied) : format(STR_C("Settings copied to %zu poles, %zu couldn't take them (no card for their A or not by a track)"), copied, failed); +} + +// the stagger the next pole of the chain gets: the other way than the last one on a straight; after the user put three the same +// way in a row they're kept that way, until one goes the other way again +bool catenary_editor::catenary_propose_toward(bool &Toward) +{ + auto &tool{m_catenary}; + if (false == tool.alternate || tool.last < 0) + return false; + std::vector order; + if (tool.chain >= 0 && catenary_chain_get(static_cast(tool.chain)) != nullptr) + { + order = catenary_chain_members(static_cast(tool.chain)); + if (false == tool.append) + std::reverse(order.begin(), order.end()); + } + else + order = {static_cast(tool.last)}; + int streak{0}; + bool last{true}; + for (auto item{order.rbegin()}; item != order.rend(); ++item) + { + auto const *spot{catenary_spot_of(*item)}; + if (spot == nullptr) + break; + if (streak == 0) + last = spot->toward; + else if (spot->toward != last || spot->curve) + break; + if (spot->curve) + { + // a curve decides the stagger itself: the next one simply goes the other way + streak = 1; + break; + } + ++streak; + } + if (streak == 0) + return false; + Toward = streak >= 3 ? last : false == last; + return true; +} + +void catenary_editor::catenary_place(catenary_spot const &Spot) +{ + auto &tool{m_catenary}; + ++tool.group; + auto data{Spot.data}; + std::string const file{tool.file}; + data.name = catenary_scene_new_name(file); + // a drop on a gantry takes the texture of the gantry + if (auto const *gantry{tool.gantry_selected >= 0 ? catenary_gantry_get(static_cast(tool.gantry_selected)) : nullptr}; gantry != nullptr && data.pole_type != tool.poles[std::clamp(tool.pole, 0, static_cast(tool.poles.size()) - 1)]) + data.skin = gantry->skin; + auto const index{catenary_scene_add(file, data)}; + tool.spots[index] = Spot; + tool.spots[index].data = data; + tool.undo.push_back({tool.group, false, index, std::nullopt, std::nullopt, false}); + tool.selected = -1; + tool.status = format(STR_C("Pole %s with card %d execution %d placed, A = %.2f m"), data.pole_type.c_str(), Spot.card, data.execution, Spot.reach); + if (catenary_scene_parts(index) == 0) + tool.status += STR(" - the cantilever didn't assemble, see the log"); + // the span from the last pole: a new chain of the two, or one more pole of the chain + auto const *previous{tool.chaining && tool.last >= 0 ? catenary_scene_get(static_cast(tool.last)) : nullptr}; + if (previous != nullptr && catenary_scene_file_of(static_cast(tool.last)) == file) + { + auto previousdata{*previous}; + if (previousdata.name.empty()) + { + // a pole from before the editor had no name, a chain needs it + tool.undo.push_back({tool.group, false, static_cast(tool.last), previousdata, std::nullopt, false}); + previousdata.name = catenary_scene_new_name(file); + catenary_scene_update(static_cast(tool.last), previousdata); + } + auto const *chain{tool.chain >= 0 ? catenary_chain_get(static_cast(tool.chain)) : nullptr}; + if (chain != nullptr && catenary_chain_file(static_cast(tool.chain)) == file) + { + auto changed{*chain}; + tool.undo.push_back({tool.group, true, static_cast(tool.chain), std::nullopt, *chain, false}); + if (tool.append) + changed.suspensions.push_back(data.name); + else + changed.suspensions.insert(changed.suspensions.begin(), data.name); + catenary_chain_update(static_cast(tool.chain), changed); + } + else + { + catenary::chain created; + created.name = catenary_scene_new_chain_name(file); + created.power = tool.power; + created.voltage = tool.voltage; + created.material = kMaterials[std::clamp(tool.material, 0, 1)]; + created.state = kStateValues[std::clamp(tool.state, 0, 2)]; + created.type = tool.networks.empty() ? std::string{} : tool.networks[std::clamp(tool.network, 0, static_cast(tool.networks.size()) - 1)]; + created.suspensions = {previousdata.name, data.name}; + tool.chain = static_cast(catenary_chain_add(file, created)); + tool.append = true; + tool.undo.push_back({tool.group, true, static_cast(tool.chain), std::nullopt, std::nullopt, false}); + } + tool.chain_selected = tool.chain; + } + else + tool.chain = -1; // nothing to go on from: the next span starts a new chain + tool.last = static_cast(index); + // the next one: the other way, unless the user keeps putting them the same way + if (bool toward{tool.toward}; catenary_propose_toward(toward)) + tool.toward = toward; +} + +// the span from the suspension the wires are being hung from (tool.connect_from) to To: To goes on the chain at its end, +// From goes on the chain of To at its end, the two chains become one, or the two make a new chain +void catenary_editor::catenary_connect(std::size_t const To) +{ + auto &tool{m_catenary}; + auto const from{static_cast(tool.connect_from)}; + auto const *a{catenary_scene_get(from)}; + auto const *b{catenary_scene_get(To)}; + if (a == nullptr) + { + tool.connect_from = -1; + return; + } + if (b == nullptr || To == from) + return; + auto const file{catenary_scene_file_of(from)}; + if (catenary_scene_file_of(To) != file) + { + tool.status = STR("The poles are in different files: a chain can't join them"); + return; + } + std::size_t pa{0}, pb{0}; + auto const ca{catenary_chain_of(from, pa)}; + auto const cb{catenary_chain_of(To, pb)}; + auto const *chaina{ca >= 0 ? catenary_chain_get(static_cast(ca)) : nullptr}; + auto const *chainb{cb >= 0 ? catenary_chain_get(static_cast(cb)) : nullptr}; + if (chaina != nullptr && ca == cb) + { + tool.status = STR("Both poles are in this chain already"); + return; + } + auto const end = [](catenary::chain const *Chain, std::size_t const Position) { + return Chain == nullptr || Position == 0 || Position + 1 == Chain->suspensions.size(); + }; + if (false == end(chaina, pa) || false == end(chainb, pb)) + { + tool.status = STR("The pole is in the middle of a chain: the spans go on from the ends of a chain only"); + return; + } + ++tool.group; + // a pole from before the editor had no name, a chain needs it + auto const named = [&](std::size_t const Index) { + auto data{*catenary_scene_get(Index)}; + if (false == data.name.empty()) + return data.name; + tool.undo.push_back({tool.group, false, Index, data, std::nullopt, false}); + data.name = catenary_scene_new_name(file); + catenary_scene_update(Index, data); + return data.name; + }; + auto const namea{named(from)}; + auto const nameb{named(To)}; + int result{-1}; + if (chaina != nullptr) + { + // From's chain with From last, then To (and To's chain from To on) + auto changed{*chaina}; + if (pa == 0 && changed.suspensions.size() > 1) + std::reverse(changed.suspensions.begin(), changed.suspensions.end()); + if (chainb != nullptr) + { + auto other{chainb->suspensions}; + if (pb != 0) + std::reverse(other.begin(), other.end()); + changed.suspensions.insert(changed.suspensions.end(), other.begin(), other.end()); + tool.undo.push_back({tool.group, true, static_cast(cb), std::nullopt, *chainb, false}); + catenary_chain_remove(static_cast(cb)); + } + else + changed.suspensions.push_back(nameb); + tool.undo.push_back({tool.group, true, static_cast(ca), std::nullopt, *chaina, false}); + catenary_chain_update(static_cast(ca), changed); + result = ca; + } + else if (chainb != nullptr) + { + // From before To on To's chain + auto changed{*chainb}; + if (pb == 0) + changed.suspensions.insert(changed.suspensions.begin(), namea); + else + changed.suspensions.push_back(namea); + tool.undo.push_back({tool.group, true, static_cast(cb), std::nullopt, *chainb, false}); + catenary_chain_update(static_cast(cb), changed); + result = cb; + } + else + { + catenary::chain created; + created.name = catenary_scene_new_chain_name(file); + created.power = tool.power; + created.voltage = tool.voltage; + created.material = kMaterials[std::clamp(tool.material, 0, 1)]; + created.state = kStateValues[std::clamp(tool.state, 0, 2)]; + created.type = tool.networks.empty() ? std::string{} : tool.networks[std::clamp(tool.network, 0, static_cast(tool.networks.size()) - 1)]; + created.suspensions = {namea, nameb}; + result = static_cast(catenary_chain_add(file, created)); + tool.undo.push_back({tool.group, true, static_cast(result), std::nullopt, std::nullopt, false}); + } + tool.chain_selected = result; + // the next click goes on from this pole + tool.connect_from = static_cast(To); + tool.selected = static_cast(To); + tool.probed = false; + tool.status = format(STR_C("Span %s - %s hung; LMB the next pole, or stop with the button"), namea.c_str(), nameb.c_str()); +} + +// what hangs on a span of a gantry: the drops on its beam and the cantilevers straight on its masts +void catenary_editor::catenary_gantry_hangers(std::size_t const Gantry, std::vector> &Hangers) const +{ + glm::dvec3 from, to; + double bottom{0.0}; + auto const *catalog{catenary_scene_catalog()}; + auto const *gantry{catenary_gantry_get(Gantry)}; + if (catalog == nullptr || gantry == nullptr || false == catenary_gantry_beam(Gantry, from, to, bottom)) + return; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + auto const *pole{data != nullptr && data->has_pole ? catalog->pole(data->pole_type) : nullptr}; + if (pole == nullptr || std::any_of(Hangers.begin(), Hangers.end(), [index](auto const &Hanger) { return Hanger.first == index; })) + continue; + glm::dvec3 const point{data->pole[0], data->pole[1], data->pole[2]}; + if (pole->gantry) + { + for (int k = 0; k < 2; ++k) + if (gantry->masts[k] == data->pole_type && glm::length(plan_of(point) - plan_of(glm::dvec3{gantry->points[k][0], 0.0, gantry->points[k][2]})) < 0.1) + { + Hangers.emplace_back(index, *data); + break; + } + continue; + } + if (pole->drop.empty()) + continue; + auto const u{to - from}; + auto const along{std::clamp(glm::dot(point - from, u) / std::max(glm::dot(u, u), 1e-9), 0.0, 1.0)}; + if (glm::distance(point, from + u * along) < 0.3) + Hangers.emplace_back(index, *data); + } +} + +void catenary_editor::catenary_press(track_intent const &Intent) +{ + auto &tool{m_catenary}; + tool.status.clear(); + auto const &mouse{ImGui::GetIO().MousePos}; + tool.pressed = {mouse.x, mouse.y}; + if (is_intent(Intent, intent_detail::select)) + { + if (tool.connect_from >= 0) + { + // hanging the wires on: the span from the last pole to this one + catenary_connect(static_cast(Intent.index)); + return; + } + catenary_select(Intent.index); + // a pole selected on the gantries page: the cantilever on the gantry, its settings are shown there; on the wires page it's + // the pole the wires go from + if (tool.page == 1 && tool.gantry_selected < 0) + tool.page = 0; + // held and dragged: the pole goes along the track + tool.dragging = tool.selected >= 0 ? 1 : 0; + return; + } + if (is_intent(Intent, intent_detail::gantry)) + { + tool.gantry_selected = Intent.index; + tool.selected = -1; + tool.page = 1; + tool.status = STR("The cantilevers now go on drops on this gantry: LMB over a track; drag the beam to move the gantry"); + // held and dragged: the whole gantry goes with the cursor + tool.dragging = 2; + tool.drag_start = e.cursor_ground(); + // held at its distance from the track: the first mast goes along the track, at the axis distance it has now + if (auto const *gantry{catenary_gantry_get(static_cast(Intent.index))}; gantry != nullptr) + { + tool.drag_anchor = {gantry->points[0][0], gantry->points[0][1], gantry->points[0][2]}; + double face{0.0}; + glm::dvec2 away{0.0}; + auto const *mast{catenary_scene_catalog() != nullptr ? catenary_scene_catalog()->mast(gantry->masts[0]) : nullptr}; + tool.drag_axis = catenary_mast_face(gantry->masts[0], tool.drag_anchor, face, away) ? face + (mast != nullptr ? mast->offset : 0.0) : 0.0; + } + // a gantry of many spans goes whole: all the spans joined on their masts, with what hangs on them + tool.drag_group.clear(); + tool.drag_undo = 0; + tool.drag_drops.clear(); + for (auto const span : gantry_group(static_cast(tool.gantry_selected))) + { + tool.drag_group.emplace_back(span, *catenary_gantry_get(span)); + catenary_gantry_hangers(span, tool.drag_drops); + } + return; + } + if (is_intent(Intent, intent_detail::mast)) + { + auto const point{catenary_mast_point()}; + if (false == tool.gantry_first.has_value()) + { + tool.gantry_first = point; + tool.gantry_track.reset(); + // the track nearest to the first mast (within 20 m): the beam can go square to it + auto best{20.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + { + bezier const curve{track->m_paths[path]}; + if (geometry::plan_distance(curve.point(0.5), point) > curve.plan_length() * 0.5 + best) + continue; + auto const parameter{nearest_parameter(*track, path, point)}; + auto const distance{geometry::plan_distance(curve.point(parameter), point)}; + if (distance < best) + { + best = distance; + tool.gantry_track = forward_at(*track, path, parameter); + // the gantry stands at the height of the rail heads of that track + tool.gantry_first->y = curve.point(parameter).y + kRailTop; + } + } + } + return; + } + glm::dvec3 second; + std::string beam; + if (false == catenary_gantry_second(point, second, beam) || tool.masts.empty()) + return; + catenary::gantry gantry; + std::string const file{tool.file}; + gantry.name = catenary_scene_new_gantry_name(file); + gantry.beam = beam; + gantry.high = tool.gantry_high; + gantry.side = 1; + gantry.skin = tool.gantry_skin; + gantry.masts[0] = tool.masts[std::clamp(tool.mast_first, 0, static_cast(tool.masts.size()) - 1)]; + gantry.masts[1] = tool.gantry_single ? std::string{} : tool.masts[std::clamp(tool.mast_second, 0, static_cast(tool.masts.size()) - 1)]; + auto const first{*tool.gantry_first}; + gantry.points[0][0] = first.x; + gantry.points[0][1] = first.y; + gantry.points[0][2] = first.z; + gantry.points[1][0] = second.x; + gantry.points[1][1] = second.y; + gantry.points[1][2] = second.z; + auto const index{catenary_gantry_add(file, gantry)}; + catenary_change change; + change.group = ++tool.group; + change.gantry = true; + change.index = index; + tool.undo.push_back(change); + tool.gantry_selected = static_cast(index); + tool.gantry_placing = false; + tool.gantry_single = false; + tool.gantry_first.reset(); + tool.status = format(STR_C("Gantry %s with beam %s placed; LMB over a track puts a drop with a cantilever on it"), gantry.name.c_str(), beam.c_str()); + double face{0.0}; + glm::dvec2 away{0.0}; + if (false == gantry.masts[1].empty() && catenary_mast_face(gantry.masts[1], second, face, away)) + tool.status += format(STR_C(" (second mast %.2f m from the track axis)"), face); + return; + } + if (is_intent(Intent, intent_detail::chain)) + { + tool.selected = -1; + tool.page = 2; + tool.chain_selected = Intent.index; + if (auto const *chain{catenary_chain_get(static_cast(Intent.index))}; chain != nullptr) + { + // the panel shows what the chain has + std::snprintf(tool.power, sizeof(tool.power), "%s", chain->power.c_str()); + tool.voltage = static_cast(chain->voltage); + tool.material = chain->material == "al" ? 1 : 0; + tool.state = chain->state == 2 ? 1 : chain->state == 1 ? 2 : 0; + if (auto const found{std::find(tool.networks.begin(), tool.networks.end(), chain->type)}; found != tool.networks.end()) + tool.network = static_cast(found - tool.networks.begin()); + } + return; + } + if (Intent.track == nullptr) + return; + catenary_spot spot; + if (false == catenary_spot_at(*Intent.track, Intent.path, e.m_hover.point, e.cursor_level(e.m_hover.point.y), 0, spot)) + return; + if (spot.card == 0) + { + tool.status = spot.problem; + return; + } + catenary_place(spot); +} + +// the panel settings of the chain go to the selected chain +void catenary_editor::catenary_chain_apply() +{ + auto &tool{m_catenary}; + if (tool.chain_selected < 0) + return; + auto const index{static_cast(tool.chain_selected)}; + auto const *chain{catenary_chain_get(index)}; + if (chain == nullptr) + return; + auto changed{*chain}; + changed.power = tool.power; + changed.voltage = tool.voltage; + changed.material = kMaterials[std::clamp(tool.material, 0, 1)]; + changed.state = kStateValues[std::clamp(tool.state, 0, 2)]; + if (false == tool.networks.empty()) + changed.type = tool.networks[std::clamp(tool.network, 0, static_cast(tool.networks.size()) - 1)]; + auto const &last{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; + if (tool.undo.empty() || false == last.chain || last.index != index || false == last.chain_before.has_value()) + tool.undo.push_back({++tool.group, true, index, std::nullopt, *chain, false}); + catenary_chain_update(index, changed); +} + +bool catenary_editor::catenary_delete_selected() +{ + auto &tool{m_catenary}; + tool.connect_from = -1; + if (tool.selected < 0) + return false; + auto const index{static_cast(tool.selected)}; + auto const *current{catenary_scene_get(index)}; + if (current == nullptr) + return false; + ++tool.group; + // the chains it's in lose it: their lists are kept to be taken back + for (std::size_t chain = 0; chain < catenary_chain_size(); ++chain) + { + auto const *data{catenary_chain_get(chain)}; + if (data != nullptr && false == current->name.empty() && catenary_chain_file(chain) == catenary_scene_file_of(index) + && std::find(data->suspensions.begin(), data->suspensions.end(), current->name) != data->suspensions.end()) + tool.undo.push_back({tool.group, true, chain, std::nullopt, *data, false}); + } + tool.undo.push_back({tool.group, false, index, *current, std::nullopt, true}); + catenary_scene_remove(index); + tool.selected = -1; + if (tool.last == static_cast(index)) + tool.last = -1; + tool.status = STR("Pole removed"); + return true; +} + +bool catenary_editor::catenary_undo() +{ + auto &tool{m_catenary}; + tool.connect_from = -1; + // in the middle of a drag, or with nothing of its own: the editor takes back its own changes + if (tool.dragging != 0 || tool.undo.empty()) + return false; + auto const group{tool.undo.back().group}; + while (false == tool.undo.empty() && tool.undo.back().group == group) + { + auto const change{tool.undo.back()}; + tool.undo.pop_back(); + if (change.anchor) + { + if (change.anchor_before.has_value()) + catenary_anchor_update(change.index, *change.anchor_before); + else + catenary_anchor_remove(change.index); + continue; + } + if (change.gantry) + { + if (change.gantry_before.has_value()) + catenary_gantry_update(change.index, *change.gantry_before); + else + { + catenary_gantry_remove(change.index); + if (tool.gantry_selected == static_cast(change.index)) + tool.gantry_selected = -1; + } + continue; + } + if (change.chain) + { + if (change.chain_before.has_value()) + catenary_chain_update(change.index, *change.chain_before); + else + { + catenary_chain_remove(change.index); + if (tool.chain == static_cast(change.index)) + tool.chain = -1; + } + continue; + } + if (false == change.before.has_value()) + { + catenary_scene_remove(change.index); + if (tool.last == static_cast(change.index)) + tool.last = -1; + } + else if (change.removed) + catenary_scene_restore(change.index, *change.before); + else + catenary_scene_update(change.index, *change.before); + tool.spots.erase(change.index); + } + // the next span goes on from the end of the chain as it is now + if (tool.chain >= 0 && catenary_chain_get(static_cast(tool.chain)) != nullptr) + { + auto const members{catenary_chain_members(static_cast(tool.chain))}; + tool.last = members.empty() ? -1 : static_cast(tool.append ? members.back() : members.front()); + } + else if (tool.last >= 0 && catenary_scene_get(static_cast(tool.last)) == nullptr) + tool.last = -1; + tool.selected = -1; + tool.probed = false; + tool.status = STR("Taken back"); + return true; +} + +void catenary_editor::render_catenary_chain_ui() +{ + auto &tool{m_catenary}; + auto const *chain{tool.chain_selected >= 0 ? catenary_chain_get(static_cast(tool.chain_selected)) : nullptr}; + ImGui::Separator(); + if (chain == nullptr) + { + ImGui::TextDisabled("%s", STR_C("New chains take these settings; LMB on a span selects its chain")); + } + else + { + ImGui::TextColored(ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", format(STR_C("Chain %s: %zu poles, %zu spans; changes apply to all of them"), chain->name.c_str(), chain->suspensions.size(), chain->suspensions.size() - 1).c_str()); + } + auto changed{false}; + ImGui::PushItemWidth(180.0f); + if (false == tool.networks.empty() && ImGui::BeginCombo(STR_C("Network type"), tool.networks[std::clamp(tool.network, 0, static_cast(tool.networks.size()) - 1)].c_str())) + { + for (int i = 0; i < static_cast(tool.networks.size()); ++i) + if (ImGui::Selectable(tool.networks[i].c_str(), i == tool.network)) + { + tool.network = i; + changed = true; + } + ImGui::EndCombo(); + } + if (ImGui::Combo(STR_C("Material"), &tool.material, kMaterials, 2)) + changed = true; + std::string const statenames[] = {STR(kStates[0]), STR(kStates[1]), STR(kStates[2])}; + char const *const states[] = {statenames[0].c_str(), statenames[1].c_str(), statenames[2].c_str()}; + if (ImGui::Combo(STR_C("Wires"), &tool.state, states, 3)) + changed = true; + ImGui::InputText(STR_C("Power source"), tool.power, sizeof(tool.power)); + if (ImGui::IsItemDeactivatedAfterEdit()) + changed = true; + if (ImGui::InputFloat(STR_C("Voltage (V)"), &tool.voltage, 0.0f, 0.0f, "%.0f")) + changed = true; + ImGui::PopItemWidth(); + if (changed && chain != nullptr) + catenary_chain_apply(); + if (chain != nullptr) + { + if (ImGui::Button(STR_C("Continue the chain from its end"))) + { + tool.chain = tool.chain_selected; + tool.append = true; + auto const members{catenary_chain_members(static_cast(tool.chain))}; + tool.last = members.empty() ? -1 : static_cast(members.back()); + tool.selected = -1; + if (bool toward{tool.toward}; catenary_propose_toward(toward)) + tool.toward = toward; + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("from its start"))) + { + tool.chain = tool.chain_selected; + tool.append = false; + auto const members{catenary_chain_members(static_cast(tool.chain))}; + tool.last = members.empty() ? -1 : static_cast(members.front()); + tool.selected = -1; + if (bool toward{tool.toward}; catenary_propose_toward(toward)) + tool.toward = toward; + } + render_catenary_anchor_ui(static_cast(tool.chain_selected)); + } +} + +std::size_t catenary_editor::catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool const Start) const +{ + auto const &name{Start ? Chain.anchor_start : Chain.anchor_end}; + if (name.empty()) + return catenary_anchor_size(); + for (std::size_t index = 0; index < catenary_anchor_size(); ++index) + if (auto const *anchor{catenary_anchor_get(index)}; anchor != nullptr && anchor->name == name && catenary_anchor_file(index) == File) + return index; + return catenary_anchor_size(); +} + +namespace +{ + +// the colours of the weights as the models have them (executions 1-4) +char const *const kWeightColors[] = {"zolty", "szary", "zolty_pedzel", "szary_brzydki"}; +char const *const kWeightColorNames[] = {"yellow", "grey", "yellow, brushed", "grey, shabby"}; + +int weight_color_index(std::string const &Name) +{ + for (int i = 0; i < 4; ++i) + if (Name == kWeightColors[i]) + return i; + return 1; +} + +} // namespace + +void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) +{ + auto &tool{m_catenary}; + auto const *chain{catenary_chain_get(Chain)}; + if (chain == nullptr) + return; + auto const file{catenary_chain_file(Chain)}; + auto const members{catenary_chain_members(Chain)}; + if (members.size() < 2) + return; + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("Anchors (weight tensioning, card 23-0461)")); + for (int end = 0; end < 2; ++end) + { + auto const start{end == 0}; + ImGui::PushID(end); + // the last two poles at this end: the wires come along them to the anchor + auto const *last{catenary_scene_get(start ? members.front() : members.back())}; + auto const *previous{catenary_scene_get(start ? members[1] : members[members.size() - 2])}; + auto const index{catenary_anchor_of(*chain, file, start)}; + auto const *anchor{index < catenary_anchor_size() ? catenary_anchor_get(index) : nullptr}; + if (last == nullptr || previous == nullptr || false == last->has_at || false == previous->has_at) + { + ImGui::PopID(); + continue; + } + // along the wires, away from the chain + glm::dvec2 along{last->at[0] - previous->at[0], last->at[2] - previous->at[2]}; + if (glm::length(along) < 0.1) + { + ImGui::PopID(); + continue; + } + along = glm::normalize(along); + auto const base{last->has_pole ? glm::dvec2{last->pole[0], last->pole[2]} : glm::dvec2{last->at[0], last->at[2]}}; + auto const baseheight{last->has_pole ? last->pole[1] : last->at[1] - static_cast(tool.height)}; + if (anchor == nullptr) + { + if (ImGui::Button(start ? STR_C("Anchor the start of the chain") : STR_C("Anchor the end of the chain"))) + { + // the anchor pole 24 m past the last pole, in line with it (catalogue 22 m; Glowacki 24 m for big tensions) + catenary::anchor data; + data.name = catenary_scene_new_anchor_name(file); + data.network = chain->type; + data.pole_type = tool.poles.empty() ? std::string{} : tool.poles[std::clamp(tool.pole, 0, static_cast(tool.poles.size()) - 1)]; + auto const at{base + along * 24.0}; + data.pole[0] = at.x; + data.pole[1] = baseheight; + data.pole[2] = at.y; + std::copy(std::begin(last->at), std::end(last->at), data.toward); + // from the middle anchor: half of the chain, by default + double length{0.0}; + for (std::size_t k = 1; k < members.size(); ++k) + { + auto const *a{catenary_scene_get(members[k - 1])}; + auto const *b{catenary_scene_get(members[k])}; + if (a != nullptr && b != nullptr) + length += std::hypot(b->at[0] - a->at[0], b->at[2] - a->at[2]); + } + data.distance = std::clamp(std::round(0.5 * length / 10.0) * 10.0, 100.0, 750.0); + data.color = "szary"; + data.top_color = "zolty"; + auto changed{*chain}; + (start ? changed.anchor_start : changed.anchor_end) = data.name; + ++tool.group; + catenary_change added; + added.group = tool.group; + added.anchor = true; + added.index = catenary_anchor_add(file, data); + tool.undo.push_back(added); + catenary_change linked; + linked.group = tool.group; + linked.chain = true; + linked.index = Chain; + linked.chain_before = *chain; + tool.undo.push_back(linked); + catenary_chain_update(Chain, changed); + tool.status = STR("Anchored: the last span goes without droppers to the tensioning device"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("An anchor pole 24 m past the last pole, in line with it, with the weight tensioning device; the last span hangs without droppers")); + ImGui::PopID(); + continue; + } + ImGui::TextColored(ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", format(start ? STR_C("Start: anchor %s") : STR_C("End: anchor %s"), anchor->name.c_str()).c_str()); + auto data{*anchor}; + auto edited{false}; + ImGui::PushItemWidth(180.0f); + { + // the anchor pole along the line of the last two poles + glm::dvec2 const pole{data.pole[0], data.pole[2]}; + auto distance{static_cast(glm::dot(pole - base, along))}; + if (ImGui::SliderFloat(STR_C("Anchor pole from the last pole (m)"), &distance, 10.0f, 40.0f, "%.1f")) + { + auto const at{base + along * static_cast(distance)}; + data.pole[0] = at.x; + data.pole[2] = at.y; + edited = true; + } + } + if (ImGui::BeginCombo(STR_C("Anchor pole"), data.pole_type.c_str())) + { + auto chosen{static_cast(std::find(tool.poles.begin(), tool.poles.end(), data.pole_type) - tool.poles.begin())}; + if (grouped_items(tool.poles, chosen) && chosen >= 0 && chosen < static_cast(tool.poles.size())) + { + data.pole_type = tool.poles[chosen]; + edited = true; + } + ImGui::EndCombo(); + } + { + auto distance{static_cast(data.distance)}; + if (ImGui::SliderFloat(STR_C("From the middle anchor (m)"), &distance, 50.0f, 900.0f, "%.0f")) + { + data.distance = distance; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Half-section length (from the middle or fixed anchor): the weights and the pulley move with the air temperature this much more")); + } + std::string const colornames[] = {STR(kWeightColorNames[0]), STR(kWeightColorNames[1]), STR(kWeightColorNames[2]), STR(kWeightColorNames[3])}; + char const *const colors[] = {colornames[0].c_str(), colornames[1].c_str(), colornames[2].c_str(), colornames[3].c_str()}; + { + auto color{weight_color_index(data.color)}; + if (ImGui::Combo(STR_C("Weights"), &color, colors, 4)) + { + data.color = kWeightColors[color]; + edited = true; + } + auto top{data.top_color.empty() ? color : weight_color_index(data.top_color)}; + if (ImGui::Combo(STR_C("Top weight"), &top, colors, 4)) + { + data.top_color = kWeightColors[top]; + edited = true; + } + } + { + auto missing{data.missing}; + if (ImGui::SliderInt(STR_C("Weights missing"), &missing, 0, 10)) + { + data.missing = missing; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Stolen from the top of the stack: about 100 daN less tension each")); + auto elongation{static_cast(data.elongation * 100.0)}; + if (ImGui::SliderFloat(STR_C("Wires stretched since regulated (cm)"), &elongation, 0.0f, 60.0f, "%.0f")) + { + data.elongation = elongation / 100.0; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Creep and wear of an old network: the weights hang 4 times this much lower; on the ground the network loses its tension")); + } + ImGui::PopItemWidth(); + if (edited) + { + // one undo step for one drag of a slider + auto const &lastchange{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; + if (tool.undo.empty() || false == lastchange.anchor || lastchange.index != index || false == ImGui::IsMouseDragging(0)) + { + catenary_change change; + change.group = ++tool.group; + change.anchor = true; + change.index = index; + change.anchor_before = *anchor; + tool.undo.push_back(change); + } + catenary_anchor_update(index, data); + } + if (auto const *result{catenary_anchor_result(index)}; result != nullptr) + { + ImGui::TextDisabled("%s", format(STR_C("%.1f deg C: pulley %.2f m from the pole, weights %.2f m over the rails; %d weights, messenger %.0f daN, contact wires %.0f daN"), + Global.AirTemperature, result->b, result->s, result->weights, result->messenger_tension, result->contact_tension).c_str()); + for (auto const &warning : result->warnings) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "%s", warning.c_str()); + } + if (ImGui::Button(STR_C("Remove the anchor"))) + { + auto changed{*chain}; + (start ? changed.anchor_start : changed.anchor_end).clear(); + ++tool.group; + catenary_change removed; + removed.group = tool.group; + removed.anchor = true; + removed.index = index; + removed.anchor_before = *anchor; + tool.undo.push_back(removed); + catenary_change unlinked; + unlinked.group = tool.group; + unlinked.chain = true; + unlinked.index = Chain; + unlinked.chain_before = *chain; + tool.undo.push_back(unlinked); + catenary_anchor_remove(index); + catenary_chain_update(Chain, changed); + } + ImGui::PopID(); + } +} + +// the cantilever from the described catalogue: category, group, family (automatic by the track), card (automatic by A), +// execution. Returns true when anything changed +bool catenary_editor::render_catenary_catalog_ui() +{ + auto &tool{m_catenary}; + auto const &structures{catenary_scene_structures()}; + auto const *catalog{catenary_scene_catalog()}; + auto changed{false}; + auto const *group{structures.group(tool.catalog_group)}; + std::string categorytitle; + for (auto const &category : structures.categories()) + if (category.id == tool.catalog_kind) + categorytitle = category.title; + if (ImGui::BeginCombo(STR_C("Kind"), categorytitle.c_str())) + { + for (auto const &category : structures.categories()) + { + if (ImGui::Selectable(category.title.c_str(), category.id == tool.catalog_kind) && category.id != tool.catalog_kind) + { + tool.catalog_kind = category.id; + // the first group of the kind + for (auto const &candidate : structures.groups()) + if (candidate.category == category.id) + { + tool.catalog_group = candidate.id; + break; + } + tool.catalog_family.clear(); + tool.catalog_card = 0; + changed = true; + } + if (ImGui::IsItemHovered() && false == category.text.empty()) + ImGui::SetTooltip("%s", category.text.c_str()); + } + ImGui::EndCombo(); + } + group = structures.group(tool.catalog_group); + ImGui::PushItemWidth(320.0f); + if (ImGui::BeginCombo(STR_C("Group"), group != nullptr ? group->title.c_str() : "")) + { + for (auto const &candidate : structures.groups()) + { + if (candidate.category != tool.catalog_kind) + continue; + if (ImGui::Selectable(candidate.title.c_str(), candidate.id == tool.catalog_group) && candidate.id != tool.catalog_group) + { + tool.catalog_group = candidate.id; + tool.catalog_family.clear(); + tool.catalog_card = 0; + changed = true; + } + if (ImGui::IsItemHovered() && false == candidate.text.empty()) + ImGui::SetTooltip("%s", candidate.text.c_str()); + } + ImGui::EndCombo(); + } + group = structures.group(tool.catalog_group); + if (group != nullptr && false == group->text.empty()) + { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 480.0f); + ImGui::TextDisabled("%s", group->text.c_str()); + ImGui::PopTextWrapPos(); + } + // the family: by the track and the stagger, or the one chosen + auto const *family{structures.family(tool.catalog_family)}; + auto const automatic{STR("by the track and the stagger")}; + if (ImGui::BeginCombo(STR_C("Family"), family != nullptr ? family->title.c_str() : automatic.c_str())) + { + if (ImGui::Selectable(automatic.c_str(), family == nullptr)) + { + tool.catalog_family.clear(); + tool.catalog_card = 0; + changed = true; + } + for (auto const &candidate : structures.families()) + { + if (candidate.group != tool.catalog_group) + continue; + if (ImGui::Selectable(candidate.title.c_str(), candidate.id == tool.catalog_family)) + { + tool.catalog_family = candidate.id; + tool.catalog_card = 0; + changed = true; + } + if (ImGui::IsItemHovered() && false == candidate.text.empty()) + ImGui::SetTooltip("%s", candidate.text.c_str()); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Automatic: the family for a straight or a curve and for the wires towards or away from the pole, as the place by the track has it")); + // the card: by the distance A, or the one chosen (of the chosen family) + family = structures.family(tool.catalog_family); + if (family != nullptr && family->cards.size() > 1) + { + auto const byreach{STR("by the distance A")}; + if (ImGui::BeginCombo(STR_C("Card"), tool.catalog_card > 0 ? format("%04d", tool.catalog_card).c_str() : byreach.c_str())) + { + if (ImGui::Selectable(byreach.c_str(), tool.catalog_card <= 0)) + { + tool.catalog_card = 0; + changed = true; + } + for (auto const card : family->cards) + { + if (catalog == nullptr || std::none_of(catalog->schemes().begin(), catalog->schemes().end(), [card](auto const &Scheme) { return Scheme.first.first == card; })) + continue; + if (ImGui::Selectable(format("%04d", card).c_str(), card == tool.catalog_card)) + { + tool.catalog_card = card; + changed = true; + } + } + ImGui::EndCombo(); + } + } + // the execution: chosen, or from the band A + if (group != nullptr && group->execution_user) + { + std::string current{format(STR_C("execution %d"), tool.execution)}; + for (auto const &execution : group->executions) + if (execution.first == tool.execution) + current = execution.second; + if (ImGui::BeginCombo(STR_C("Execution"), current.c_str())) + { + for (auto const &execution : group->executions) + if (ImGui::Selectable(execution.second.c_str(), execution.first == tool.execution)) + { + tool.execution = execution.first; + changed = true; + } + ImGui::EndCombo(); + } + } + else if (group != nullptr) + ImGui::TextDisabled("%s", STR_C("Execution: from the distance A (the band of the card)")); + ImGui::PopItemWidth(); + return changed; +} + +// the wires page: the selected chain and its settings, the spans hung from the selected pole to the next ones +void catenary_editor::render_catenary_wires_ui() +{ + auto &tool{m_catenary}; + auto const *data{tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr}; + if (data == nullptr) + ImGui::TextDisabled("%s", STR_C("LMB on a pole selects it: the wires can then be hung from it to the next poles; LMB on a span selects its chain")); + else + { + ImGui::TextColored(ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", format(STR_C("Pole %s"), data->name.empty() ? "-" : data->name.c_str()).c_str()); + if (ImGui::Button(tool.connect_from >= 0 ? STR_C("Stop hanging the wires") : STR_C("Hang the wires to another pole"))) + { + tool.connect_from = tool.connect_from >= 0 ? -1 : tool.selected; + tool.status = tool.connect_from >= 0 ? STR("LMB the next pole: the span from this one is hung to it; then the next one, and so on") : std::string{}; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Joins this pole with the poles clicked after it: it goes on the chain at its end, a new chain of the two, or two chains become one")); + } + render_catenary_chain_ui(); +} + +void catenary_editor::catenary_choose_page(int const Page) +{ + auto &tool{m_catenary}; + if (Page == tool.page) + return; + tool.page = Page; + // what was going on in the page left stays behind + tool.gantry_placing = false; + tool.gantry_single = false; + tool.gantry_first.reset(); + tool.connect_from = -1; + if (Page != 1) + tool.gantry_selected = -1; // the poles go by the track again, not on the drops of a gantry + tool.status.clear(); +} + +// the settings of the cantilevers (the next ones placed, or the selected one) and the selected pole +void catenary_editor::render_catenary_cantilever_ui() +{ + auto &tool{m_catenary}; + auto const *catalog{catenary_scene_catalog()}; + auto changed{false}; + ImGui::PushItemWidth(180.0f); + if (tool.selected < 0) + { + ImGui::InputText(STR_C("Catenary file"), tool.file, sizeof(tool.file)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("File in the scenery directory the new poles go to; a new one is added to the scenery with the directive catenary endcatenary when it's saved")); + ImGui::Checkbox(STR_C("Chain: a span from the last pole"), &tool.chaining); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Each pole placed is joined with a span to the one placed before it, one chain from anchor to anchor")); + if (tool.last >= 0 && catenary_scene_get(static_cast(tool.last)) != nullptr) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("End the chain"))) + { + tool.chain = -1; + tool.last = -1; + } + } + } + else + ImGui::TextColored(ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", format(STR_C("Selected pole (%s): changes apply to it at once"), catenary_scene_file_of(static_cast(tool.selected)).c_str()).c_str()); + + // on the gantries the drop or the mast is the pole, its distance is the one of the gantry + if (tool.page == 0) + { + if (ImGui::BeginCombo(STR_C("Pole"), tool.poles[std::clamp(tool.pole, 0, static_cast(tool.poles.size()) - 1)].c_str())) + { + if (grouped_items(tool.poles, tool.pole)) + changed = true; + ImGui::EndCombo(); + } + changed |= ImGui::SliderFloat(STR_C("Face of the pole from the track axis (m)"), &tool.clearance, 1.60f, 4.50f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Structure gauge 2.20 or 2.50 m by the catalogue (card 0007); drag it and the cantilever follows, the card changes with band A. Ctrl+click types a value")); + } + changed |= ImGui::SliderFloat(STR_C("Stagger of the wires (m)"), &tool.stagger, 0.0f, 0.45f, "%.2f"); + if (ImGui::Checkbox(STR_C("Wires towards the pole"), &tool.toward)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("On a straight the wires zigzag: towards the pole and away from it in turn. On a curve they go to its outside, which decides it")); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("In turn"), &tool.alternate); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The next pole of the chain gets the wires the other way than the last one; after three the same way in a row it keeps that way, until one goes the other way")); + std::string const bendnames[] = {STR("by the track"), STR("straight"), STR("curve")}; + char const *const bends[] = {bendnames[0].c_str(), bendnames[1].c_str(), bendnames[2].c_str()}; + if (ImGui::Combo(STR_C("Cards for"), &tool.bend, bends, 3)) + changed = true; + if (false == catenary_scene_structures().empty()) + changed |= render_catenary_catalog_ui(); + else + { + if (ImGui::Checkbox(STR_C("Swinging cantilever (11xx)"), &tool.swinging)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Off: the fixed ones (12xx)")); + if (ImGui::BeginCombo(STR_C("Execution"), STR(kExecutions[std::clamp(tool.execution, 1, 5) - 1]).c_str())) + { + for (int i = 1; i <= 5; ++i) + if (ImGui::Selectable(STR(kExecutions[i - 1]).c_str(), i == tool.execution)) + { + tool.execution = i; + changed = true; + } + ImGui::EndCombo(); + } + } + changed |= ImGui::SliderFloat(STR_C("Contact wires over the rails (m)"), &tool.height, 4.90f, 6.50f, "%.2f"); + { + // the system height (hk): from the card, or set by hand + auto own{tool.system_height > 0.0f}; + if (ImGui::Checkbox(STR_C("Own system height"), &own)) + { + tool.system_height = own ? 1.70f : 0.0f; + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Off: the system height of the card (1.70 / 1.30 ...). On: the messenger at the height set here over the contact wires; the stay mount goes with it, the solver warns when the cantilever can't take it")); + if (own) + { + ImGui::SameLine(); + ImGui::PushItemWidth(100.0f); + changed |= ImGui::SliderFloat(STR_C("hk (m)"), &tool.system_height, 0.25f, 2.50f, "%.2f"); + ImGui::PopItemWidth(); + } + } + changed |= ImGui::Checkbox(STR_C("25 kV insulators"), &tool.insulators25); + ImGui::RadioButton(STR_C("Wind zone I"), &tool.zone, 1); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Wind zone II"), &tool.zone, 2); + if (tool.networks.empty()) + ImGui::TextDisabled("%s", STR_C("No data/traction_layout.txt: the spans aren't checked")); + ImGui::PopItemWidth(); + + // the selected pole follows the settings at once + if (tool.selected >= 0 && changed) + { + if (auto const *found{catenary_spot_of(static_cast(tool.selected))}; found != nullptr) + { + auto spot{*found}; + spot.curve = tool.bend == 2 || (tool.bend == 0 && spot.radius > 0.0 && spot.radius < kCurveRadius); + // a drop on a gantry stays the drop it is, hanging from the beam + auto const *current{catenary_scene_get(static_cast(tool.selected))}; + auto const *own{current != nullptr ? catalog->pole(current->pole_type) : nullptr}; + auto const drop{own != nullptr && false == own->drop.empty()}; + if (own != nullptr && own->gantry) + { + // on a mast of a gantry: the mast stays, the distance is its own + auto const right{right_of(spot.forward) * static_cast(spot.side)}; + catenary_compose(spot, glm::dot(plan_of(pole_of(*current)) - plan_of(spot.axis), right) - 0.5 * own->depth(0.0), tool.stagger, tool.toward, current->pole_type); + std::copy(std::begin(current->pole), std::end(current->pole), spot.data.pole); + } + else + catenary_compose(spot, tool.clearance, tool.stagger, tool.toward, drop ? current->pole_type : std::string{}); + if (drop) + spot.data.pole[1] = current->pole[1]; + if (spot.card != 0) + catenary_apply(spot); + else + tool.status = spot.problem; + } + } + if (tool.selected >= 0) + { + auto const *data{catenary_scene_get(static_cast(tool.selected))}; + if (data == nullptr) + tool.selected = -1; + else + { + if (false == tool.probed) + { + tool.probe_good = catenary_scene_probe(*data, tool.probe); + tool.probed = true; + } + auto const *spot{catenary_spot_of(static_cast(tool.selected))}; + ImGui::Text("%s", format(STR_C("Card %d execution %d, A = %.2f m, %zu parts"), data->card, data->execution, spot != nullptr ? spot->reach : 0.0, catenary_scene_parts(static_cast(tool.selected))).c_str()); + if (false == tool.probe_good) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", STR_C("The cantilever doesn't assemble:")); + for (auto const &warning : tool.probe.warnings) + ImGui::TextWrapped("- %s", warning.c_str()); + // a drop on a gantry: another length of it + if (auto const *own{catalog->pole(data->pole_type)}; own != nullptr && false == own->drop.empty()) + { + ImGui::PushItemWidth(160.0f); + if (ImGui::BeginCombo(STR_C("Drop of the pole"), data->pole_type.c_str())) + { + for (auto const &pole : catalog->poles()) + if (pole.second.drop == own->drop && ImGui::Selectable(format("%s %.1f m", pole.first.c_str(), pole.second.length).c_str(), pole.first == data->pole_type)) + { + auto changed{*data}; + changed.pole_type = pole.first; + tool.undo.push_back({++tool.group, false, static_cast(tool.selected), *data, std::nullopt, false}); + catenary_scene_update(static_cast(tool.selected), changed); + if (auto found{tool.spots.find(static_cast(tool.selected))}; found != tool.spots.end()) + found->second.data = changed; + tool.probed = false; + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + data = catenary_scene_get(static_cast(tool.selected)); + if (data == nullptr) + return; + } + // the wires from this pole to others already standing + if (ImGui::Button(tool.connect_from >= 0 ? STR_C("Stop hanging the wires") : STR_C("Hang the wires to another pole"))) + { + tool.connect_from = tool.connect_from >= 0 ? -1 : tool.selected; + tool.status = tool.connect_from >= 0 ? STR("LMB the next pole: the span from this one is hung to it; then the next one, and so on") : std::string{}; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Joins this pole with the poles clicked after it: it goes on the chain at its end, a new chain of the two, or two chains become one")); + // copying to the neighbours in the chain + std::size_t position{0}; + auto const chain{catenary_chain_of(static_cast(tool.selected), position)}; + if (chain >= 0) + { + auto const members{catenary_chain_members(static_cast(chain))}; + ImGui::TextUnformatted(STR_C("Copy to:")); + ImGui::SameLine(); + if (position > 0 && ImGui::SmallButton(STR_C("previous"))) + catenary_copy({members[position - 1]}); + ImGui::SameLine(); + if (position + 1 < members.size() && ImGui::SmallButton(STR_C("next"))) + catenary_copy({members[position + 1]}); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("both"))) + { + std::vector targets; + if (position > 0) + targets.push_back(members[position - 1]); + if (position + 1 < members.size()) + targets.push_back(members[position + 1]); + catenary_copy(targets); + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("the whole chain"))) + catenary_copy(members); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The pole, its face from the track, execution, height of the wires and insulators; each pole keeps its stagger")); + } + if (ImGui::Button(STR_C("Remove the pole"))) + catenary_delete_selected(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Done"))) + tool.selected = -1; + } + } +} + +void catenary_editor::render_catenary_ui() +{ + auto &tool{m_catenary}; + catenary_scan(); + auto const *catalog{catenary_scene_catalog()}; + if (catalog == nullptr || tool.poles.empty()) + { + ImGui::TextWrapped("%s", STR_C("The cantilever catalogue isn't loaded: data/traction_cantilevers.txt and data/traction_supports.txt (with the poles) are needed")); + return; + } + // the page chosen in the toolbar: poles, gantries, wires + if (tool.page == 1) + { + // gantries + ImGui::Separator(); + if (false == tool.masts.empty()) + { + ImGui::PushItemWidth(160.0f); + auto const combo = [&](char const *Label, int &Chosen) { + if (ImGui::BeginCombo(Label, tool.masts[std::clamp(Chosen, 0, static_cast(tool.masts.size()) - 1)].c_str())) + { + grouped_items(tool.masts, Chosen, [&](std::string const &Name) { + auto const *mast{catalog->mast(Name)}; + return mast == nullptr ? std::string{} : mast->through ? STR(" (through)") : STR(" (end)"); + }); + ImGui::EndCombo(); + } + }; + { + // the second mast follows the first until the user picks it himself + auto const before{tool.mast_first}; + combo(STR_C("First mast"), tool.mast_first); + if (tool.mast_first != before && false == tool.mast_second_own) + tool.mast_second = tool.mast_first; + } + ImGui::SameLine(); + { + auto const before{tool.mast_second}; + combo(STR_C("Second mast"), tool.mast_second); + if (tool.mast_second != before) + tool.mast_second_own = true; + } + ImGui::PopItemWidth(); + ImGui::TextUnformatted(STR_C("Beam:")); + ImGui::SameLine(); + if (ImGui::RadioButton(STR_C("lower mount"), false == tool.gantry_high)) + tool.gantry_high = false; + ImGui::SameLine(); + if (ImGui::RadioButton(STR_C("upper mount"), tool.gantry_high)) + tool.gantry_high = true; + ImGui::Checkbox(STR_C("Square to the track"), &tool.gantry_square); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The beam goes square to the track the first mast is put by")); + ImGui::Checkbox(STR_C("First mast from the track"), &tool.gantry_snap); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The first mast goes with its face at this distance from the axis of the track under the cursor, on the side of the cursor; the second one as far as the beam spans")); + if (tool.gantry_snap) + { + ImGui::SameLine(); + ImGui::PushItemWidth(120.0f); + // a gantry selected: the distance of its first mast, changed here it moves the whole gantry square to that track + auto const *selected{tool.gantry_selected >= 0 && false == tool.gantry_placing ? catenary_gantry_get(static_cast(tool.gantry_selected)) : nullptr}; + double face{0.0}; + glm::dvec2 away{0.0}; + auto const measured{selected != nullptr && catenary_mast_face(selected->masts[0], glm::dvec3{selected->points[0][0], selected->points[0][1], selected->points[0][2]}, face, away)}; + if (measured) + tool.gantry_face = static_cast(face); + if (ImGui::DragFloat(STR_C("Face from the track axis (m)##placing"), &tool.gantry_face, 0.01f, 0.5f, 20.0f, "%.2f") && measured && std::abs(tool.gantry_face - face) > 1e-4) + { + catenary_gantry_shift(away * (static_cast(tool.gantry_face) - face)); + tool.status = format(STR_C("Gantry %s: mast %d %.2f m from the track axis"), selected->name.c_str(), 1, tool.gantry_face); + } + if (ImGui::IsItemActivated()) + tool.shift_group = 0; + ImGui::PopItemWidth(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("From the axis of the track to the face of the first mast. Placing: the first mast goes there. A gantry selected: drag or Ctrl+click and type, the gantry moves square to the track; dragged with the mouse it goes only along the track, at this distance")); + } + ImGui::SameLine(); + if (tool.gantry_skin[0] == '\0') + std::snprintf(tool.gantry_skin, sizeof(tool.gantry_skin), "%s", kGantrySkins[0]); + ImGui::PushItemWidth(200.0f); + if (ImGui::BeginCombo(STR_C("Texture"), tool.gantry_skin)) + { + for (auto const *skin : kGantrySkins) + if (ImGui::Selectable(skin, std::string{skin} == tool.gantry_skin)) + std::snprintf(tool.gantry_skin, sizeof(tool.gantry_skin), "%s", skin); + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Replaceable texture of the masts, the beam and the drops of the new gantry (the models have none of their own)")); + if (ImGui::Button(tool.gantry_placing ? STR_C("Cancel the gantry") : STR_C("Place a gantry"))) + { + tool.gantry_placing = false == tool.gantry_placing; + tool.gantry_single = false; + tool.gantry_first.reset(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("LMB the first mast, LMB the second: the beam is the one of the catalogue nearest to that distance")); + if (false == tool.gantry_placing) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Place a single beam element"))) + { + tool.gantry_placing = true; + tool.gantry_single = true; + tool.gantry_first.reset(); + tool.status = STR("LMB the through mast (the first mast above), LMB where the element points: the element of the catalogue nearest to that length"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A single beam element (3017-3025, 1910) on one through mast: on one side fixed to the mast, on the other end to a console")); + } + if (auto const *gantry{tool.gantry_selected >= 0 ? catenary_gantry_get(static_cast(tool.gantry_selected)) : nullptr}; gantry != nullptr) + { + ImGui::TextColored(ImVec4(0.3f, 0.85f, 1.0f, 1.0f), "%s", format(STR_C("Gantry %s, beam %s: the cantilevers go on drops on it"), gantry->name.c_str(), gantry->beam.c_str()).c_str()); + { + // the mount of the beam of this span, changed in place (the drops and the cantilevers on them follow) + auto high{gantry->high}; + ImGui::TextUnformatted(STR_C("Beam of this gantry:")); + ImGui::SameLine(); + if (ImGui::RadioButton((std::string{STR_C("lower mount")} + "##selected").c_str(), false == high)) + high = false; + ImGui::SameLine(); + if (ImGui::RadioButton((std::string{STR_C("upper mount")} + "##selected").c_str(), high)) + high = true; + if (high != gantry->high) + { + auto const selected{static_cast(tool.gantry_selected)}; + // the drops on the beam: where along it they hang, to hang them again on it at its new level + std::vector> hangers; + catenary_gantry_hangers(selected, hangers); + glm::dvec3 from, to; + double bottom{0.0}; + catenary_gantry_beam(selected, from, to, bottom); + std::vector> along; + for (auto const &[index, data] : hangers) + { + auto const *pole{catalog->pole(data.pole_type)}; + if (pole == nullptr || pole->gantry) + continue; // the cantilevers on the masts follow the beam by themselves + auto const u{plan_of(to) - plan_of(from)}; + along.emplace_back(index, std::clamp(glm::dot(plan_of(glm::dvec3{data.pole[0], 0.0, data.pole[2]}) - plan_of(from), u) / std::max(glm::dot(u, u), 1e-9), 0.0, 1.0)); + } + auto changed{*gantry}; + changed.high = high; + ++tool.group; + catenary_change change; + change.group = tool.group; + change.gantry = true; + change.index = selected; + change.gantry_before = *gantry; + tool.undo.push_back(change); + catenary_gantry_update(selected, changed); + auto const *beam{catalog->beam(changed.beam)}; + std::vector> drops; + for (auto const &pole : catalog->poles()) + if (beam != nullptr && pole.second.drop == beam->drop) + drops.emplace_back(pole.second.length, pole.first); + std::sort(drops.begin(), drops.end()); + if (catenary_gantry_beam(selected, from, to, bottom) && false == drops.empty()) + for (auto const &[index, t] : along) + { + auto const *before{catenary_scene_get(index)}; + if (before == nullptr) + continue; + tool.undo.push_back({tool.group, false, index, *before, std::nullopt, false}); + auto data{*before}; + data.pole[1] = from.y + (to.y - from.y) * t; + // the shortest drop which still reaches down to the lowest mount of the cantilever + for (auto const &drop : drops) + { + data.pole_type = drop.second; + catenary::cantilever_result probe; + catenary_scene_probe(data, probe); + if (std::none_of(probe.warnings.begin(), probe.warnings.end(), [](std::string const &Warning) { return Warning.find("below the lowest mount") != std::string::npos; })) + break; + } + catenary_scene_update(index, data); + tool.spots.erase(index); + } + tool.status = format(high ? STR_C("Gantry %s: the beam on the upper mount") : STR_C("Gantry %s: the beam on the lower mount"), changed.name.c_str()); + } + } + // another beam: the first mast stays, the second goes as far as the beam spans + ImGui::PushItemWidth(120.0f); + if (ImGui::BeginCombo(STR_C("Beam"), gantry->beam.c_str())) + { + for (auto const &beam : catalog->beams()) + if (beam.second.single == gantry->masts[1].empty() && ImGui::Selectable(format("%s %.1f m", beam.first.c_str(), beam.second.span).c_str(), beam.first == gantry->beam)) + catenary_gantry_reshape(beam.first, 0.0, false); + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + ImGui::SameLine(); + auto const step = [&](int const Direction) { + // the beam one step (0.1 m) longer or shorter + auto const *current{catalog->beam(gantry->beam)}; + if (current == nullptr) + return; + std::string next; + auto best{std::numeric_limits::max()}; + for (auto const &beam : catalog->beams()) + { + if (beam.second.single != current->single) + continue; + auto const difference{(beam.second.span - current->span) * Direction}; + if (difference > 0.01 && difference < best) + { + best = difference; + next = beam.first; + } + } + if (false == next.empty()) + catenary_gantry_reshape(next, 0.0, false); + }; + if (ImGui::SmallButton("-")) + step(-1); + ImGui::SameLine(); + if (ImGui::SmallButton("+")) + step(1); + ImGui::SameLine(); + if (ImGui::Button((STR("Square to the track") + "##squareselected").c_str())) + catenary_gantry_reshape({}, 0.0, true); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Take the masts"))) + catenary_gantry_reshape({}, 0.0, false); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The masts chosen above go to this gantry; the first mast stays, the second goes as far as the beam needs")); + // a gantry of many spans: the next span from a through mast at an end of this one, in line with it + for (int k = 0; k < 2; ++k) + { + auto const *type{catalog->mast(gantry->masts[k])}; + auto const free{spans_at(gantry->points[k], static_cast(tool.gantry_selected)) == 0}; + if (k == 1) + ImGui::SameLine(); + ImGui::BeginDisabled(type == nullptr || false == type->through || false == free); + if (ImGui::Button(format(STR_C("Next span from mast %d"), k + 1).c_str())) + { + tool.gantry_placing = true; + glm::dvec3 const from{gantry->points[k][0], gantry->points[k][1], gantry->points[k][2]}; + glm::dvec3 const other{gantry->points[1 - k][0], gantry->points[1 - k][1], gantry->points[1 - k][2]}; + tool.gantry_first = from; + // in line: "square to the track" with a track along which right_of() is the way out of this span + auto const out{glm::normalize(plan_of(from) - plan_of(other))}; + tool.gantry_track = glm::dvec2{out.y, -out.x}; + tool.gantry_square = true; + std::snprintf(tool.gantry_skin, sizeof(tool.gantry_skin), "%s", gantry->skin.c_str()); + if (auto const found{std::find(tool.masts.begin(), tool.masts.end(), gantry->masts[k])}; found != tool.masts.end()) + tool.mast_first = static_cast(found - tool.masts.begin()); + tool.status = STR("LMB: the next mast of the gantry, as far as the nearest beam spans (the mast type: the second one above)"); + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", type != nullptr && false == type->through ? STR_C("An end mast: a beam can go on one side of it only; a through mast is needed") : false == free ? STR_C("This mast has beams on both sides already") : STR_C("The beam of the next span on the other side of this through mast, in line with this span")); + ImGui::SameLine(); + ImGui::BeginDisabled(type == nullptr || false == type->through || false == free || gantry->masts[k].empty()); + if (ImGui::Button(format(STR_C("Single element from mast %d"), k + 1).c_str())) + { + tool.gantry_placing = true; + tool.gantry_single = true; + glm::dvec3 const from{gantry->points[k][0], gantry->points[k][1], gantry->points[k][2]}; + glm::dvec3 const other{gantry->points[1 - k][0], gantry->points[1 - k][1], gantry->points[1 - k][2]}; + tool.gantry_first = from; + auto const out{glm::normalize(plan_of(from) - plan_of(other))}; + tool.gantry_track = glm::dvec2{out.y, -out.x}; + tool.gantry_square = true; + std::snprintf(tool.gantry_skin, sizeof(tool.gantry_skin), "%s", gantry->skin.c_str()); + if (auto const found{std::find(tool.masts.begin(), tool.masts.end(), gantry->masts[k])}; found != tool.masts.end()) + tool.mast_first = static_cast(found - tool.masts.begin()); + tool.status = STR("LMB: as far as the single element should reach, in line with this span"); + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C("A single beam element (3017-3025, 1910) on the other side of this through mast, in line with this span")); + } + { + float turn{0.0f}; + ImGui::PushItemWidth(120.0f); + if (ImGui::DragFloat(STR_C("Turn (degrees)"), &turn, 0.1f, -10.0f, 10.0f, "%.1f") && turn != 0.0f) + catenary_gantry_reshape({}, turn, false); + ImGui::PopItemWidth(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Drag sideways: turns the gantry around its first mast")); + } + gantry = catenary_gantry_get(static_cast(tool.gantry_selected)); + if (gantry == nullptr) + return; + // the heights of the masts: their zero is at the rail heads; the foundation head (variants -1...-4) takes the ground. + // both together, and each by itself no more than 0.20 m from the other + auto const heighten = [&](catenary::gantry const &Changed) { + auto const &last{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; + if (tool.undo.empty() || false == last.gantry || last.index != static_cast(tool.gantry_selected) || false == last.gantry_before.has_value()) + { + catenary_change change; + change.group = ++tool.group; + change.gantry = true; + change.index = static_cast(tool.gantry_selected); + change.gantry_before = *gantry; + tool.undo.push_back(change); + } + catenary_gantry_update(static_cast(tool.gantry_selected), Changed); + }; + { + auto both{static_cast(gantry->points[0][1])}; + ImGui::PushItemWidth(120.0f); + if (ImGui::DragFloat(STR_C("Gantry at the height (m)"), &both, 0.01f, -1000.0f, 5000.0f, "%.2f")) + { + auto changed{*gantry}; + auto const shift{static_cast(both) - gantry->points[0][1]}; + changed.points[0][1] += shift; + changed.points[1][1] += shift; + heighten(changed); + } + ImGui::PopItemWidth(); + gantry = catenary_gantry_get(static_cast(tool.gantry_selected)); + if (gantry == nullptr) + return; + } + for (int mast = 0; mast < 2; ++mast) + { + if (gantry->masts[mast].empty()) + continue; // a single element has no second mast + auto height{static_cast(gantry->points[mast][1])}; + ImGui::PushItemWidth(120.0f); + ImGui::PushID(mast); + auto const label{format(STR_C("Mast %d at the height (m)"), mast + 1)}; + if (ImGui::DragFloat(label.c_str(), &height, 0.01f, -1000.0f, 5000.0f, "%.2f")) + { + auto changed{*gantry}; + auto const other{gantry->points[1 - mast][1]}; + changed.points[mast][1] = std::clamp(static_cast(height), other - 0.20, other + 0.20); + heighten(changed); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The zero of the mast is at the rail heads (the clearance of the beam is counted from it); the ground is taken by the foundation head of the variant -1...-4. Drag or Ctrl+click to type")); + ImGui::PopID(); + ImGui::PopItemWidth(); + if (mast == 0) + ImGui::SameLine(); + } + gantry = catenary_gantry_get(static_cast(tool.gantry_selected)); + if (gantry == nullptr) + return; + if (ImGui::Button(STR_C("Give it the texture"))) + { + // the gantry and the drops hanging from its beam + auto changed{*gantry}; + changed.skin = tool.gantry_skin; + catenary_change change; + change.group = ++tool.group; + change.gantry = true; + change.index = static_cast(tool.gantry_selected); + change.gantry_before = *gantry; + tool.undo.push_back(change); + glm::dvec3 from, to; + double bottom{0.0}; + catenary_gantry_beam(static_cast(tool.gantry_selected), from, to, bottom); + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data == nullptr || false == data->has_pole) + continue; + auto const *pole{catalog->pole(data->pole_type)}; + auto const point{glm::dvec3{data->pole[0], data->pole[1], data->pole[2]}}; + auto const u{to - from}; + auto const t{std::clamp(glm::dot(point - from, u) / std::max(glm::dot(u, u), 1e-9), 0.0, 1.0)}; + if (pole == nullptr || pole->drop.empty() || glm::distance(point, from + u * t) > 0.3) + continue; + tool.undo.push_back({tool.group, false, index, *data, std::nullopt, false}); + auto skinned{*data}; + skinned.skin = tool.gantry_skin; + catenary_scene_update(index, skinned); + } + catenary_gantry_update(static_cast(tool.gantry_selected), changed); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Remove the gantry"))) + { + catenary_change change; + change.group = ++tool.group; + change.gantry = true; + change.index = static_cast(tool.gantry_selected); + change.gantry_before = *gantry; + tool.undo.push_back(change); + catenary_gantry_remove(static_cast(tool.gantry_selected)); + tool.gantry_selected = -1; + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Back to the poles"))) + tool.gantry_selected = -1; + // the next cantilevers: on drops of the beam, or straight on the masts + ImGui::RadioButton(STR_C("On drops"), &tool.gantry_hang, 0); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("LMB over a track hangs the cantilever on a drop moved along the beam to the track")); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("On the masts"), &tool.gantry_hang, 1); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("LMB by a track puts the cantilever straight on the mast of the gantry on that side; the card from the distance of the mast")); + // which drop the next cantilevers hang on + if (auto const *beam{tool.gantry_hang == 0 ? catalog->beam(gantry->beam) : nullptr}; beam != nullptr) + { + ImGui::PushItemWidth(160.0f); + auto const automatic{STR("the shortest which fits")}; + if (ImGui::BeginCombo(STR_C("Drop"), tool.drop_choice.empty() ? automatic.c_str() : tool.drop_choice.c_str())) + { + if (ImGui::Selectable(automatic.c_str(), tool.drop_choice.empty())) + tool.drop_choice.clear(); + for (auto const &pole : catalog->poles()) + if (pole.second.drop == beam->drop && ImGui::Selectable(format("%s %.1f m", pole.first.c_str(), pole.second.length).c_str(), pole.first == tool.drop_choice)) + tool.drop_choice = pole.first; + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + } + } + } + // the cantilevers going on the drops and on the masts of the selected gantry, and the selected one of them + if ((tool.gantry_selected >= 0 || tool.selected >= 0) && ImGui::CollapsingHeader(STR_C("Cantilevers"), ImGuiTreeNodeFlags_DefaultOpen)) + { + render_catenary_cantilever_ui(); + } + } + else if (tool.page == 2) + { + render_catenary_wires_ui(); + } + else + { + render_catenary_cantilever_ui(); + } + if (false == tool.undo.empty()) + { + if (ImGui::Button(STR_C("Take back"))) + catenary_undo(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Takes back the last change of the catenary (also Ctrl+Z while this tool is open)")); + } + if (catenary_scene_changed()) + ImGui::TextDisabled("%s", STR_C("The catenary files are written when the scenery is saved")); + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void catenary_editor::draw_catenary_overlay() const +{ + auto const &tool{m_catenary}; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + auto const draw_pole = [&](catenary::suspension const &Data, ImU32 const Colour, float const Width) { + auto const foot{pole_of(Data)}; + auto const wire{wire_of(Data)}; + projection.line(drawlist, foot, foot + glm::dvec3{0.0, pole_top(Data), 0.0}, Colour, Width); + projection.line(drawlist, glm::dvec3{foot.x, wire.y, foot.z}, wire, Colour, Width * 0.6f); + ImVec2 point; + if (projection.project(wire, point)) + drawlist->AddCircleFilled(point, Width + 1.5f, Colour); + }; + // gantries: the masts and the line of the bottom of the beam, where the drops hang from + auto const *catalog{catenary_scene_catalog()}; + for (std::size_t index = 0; index < catenary_gantry_size(); ++index) + { + glm::dvec3 from, to; + double bottom{0.0}; + auto const *gantry{catenary_gantry_get(index)}; + if (gantry == nullptr || false == catenary_gantry_beam(index, from, to, bottom) || glm::distance(from, camera) > kCatenaryRange) + continue; + auto const selected{static_cast(index) == tool.gantry_selected}; + auto const hovered{is_intent(e.m_intent, intent_detail::gantry) && e.m_intent.index == static_cast(index)}; + auto const broken{catenary_gantry_parts(index) == 0}; + auto const colour{selected ? overlay_color::selected : hovered ? overlay_color::highlight : broken ? overlay_color::invalid : IM_COL32(255, 160, 60, 170)}; + projection.line(drawlist, from, to, colour, selected || hovered ? 3.0f : 1.5f); + for (int mast = 0; mast < 2; ++mast) + { + if (gantry->masts[mast].empty()) + continue; // the tip of a single element + auto const *type{catalog != nullptr ? catalog->mast(gantry->masts[mast]) : nullptr}; + glm::dvec3 const foot{gantry->points[mast][0], gantry->points[mast][1], gantry->points[mast][2]}; + projection.line(drawlist, foot, foot + glm::dvec3{0.0, type != nullptr ? type->height : 8.0, 0.0}, colour, selected || hovered ? 3.0f : 1.5f); + } + ImVec2 middle; + if ((selected || hovered || broken) && projection.project(0.5 * (from + to) + glm::dvec3{0.0, 1.2, 0.0}, middle)) + drawlist->AddText(middle, colour, (gantry->name + " " + gantry->beam).c_str()); + } + // the gantry being placed: the first mast, and the second one where the beam would reach + if (tool.gantry_placing && tool.gantry_first.has_value()) + { + auto const first{*tool.gantry_first}; + projection.line(drawlist, first, first + glm::dvec3{0.0, 8.0, 0.0}, overlay_color::grip, 3.0f); + glm::dvec3 second; + std::string beam; + if (catenary_gantry_second(catenary_mast_point(), second, beam)) + { + if (false == tool.gantry_single) + projection.line(drawlist, second, second + glm::dvec3{0.0, 8.0, 0.0}, overlay_color::marked, 3.0f); + projection.line(drawlist, first + glm::dvec3{0.0, 7.5, 0.0}, second + glm::dvec3{0.0, 7.5, 0.0}, overlay_color::marked, 2.0f); + ImVec2 middle; + if (projection.project(0.5 * (first + second) + glm::dvec3{0.0, 8.2, 0.0}, middle)) + drawlist->AddText(middle, overlay_color::marked, format("%s %.1f m", beam.c_str(), glm::length(plan_of(second) - plan_of(first))).c_str()); + } + } + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data == nullptr || false == data->has_at || glm::distance(pole_of(*data), camera) > kCatenaryRange) + continue; + auto const selected{static_cast(index) == tool.selected}; + auto const hovered{is_intent(e.m_intent, intent_detail::select) && e.m_intent.index == static_cast(index)}; + auto const broken{catenary_scene_parts(index) == 0}; + auto const byswitch{catenary_switch_error(index)}; + auto const last{static_cast(index) == tool.last && tool.chaining}; + auto const colour{selected ? overlay_color::selected : hovered ? overlay_color::highlight : broken || byswitch ? overlay_color::invalid : last ? overlay_color::grip : IM_COL32(255, 210, 60, 150)}; + draw_pole(*data, byswitch && false == selected ? overlay_color::invalid : colour, selected || hovered || last || byswitch ? 3.0f : 1.5f); + ImVec2 head; + if ((selected || hovered || broken || byswitch) && projection.project(pole_of(*data) + glm::dvec3{0.0, pole_top(*data) + 0.3, 0.0}, head)) + { + auto label{format("%d-%d", data->card, data->execution) + (broken ? std::string{" "} + STR("doesn't assemble") : std::string{})}; + if (byswitch) + label += " " + format(STR_C("%s by a switch"), data->pole_type.c_str()); + drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str()); + } + } + // spans against the catalogue: green within the limit, red over it; the selected chain brighter + auto const &network{tool.networks.empty() ? std::string{} : tool.networks[std::clamp(tool.network, 0, static_cast(tool.networks.size()) - 1)]}; + for (auto const &span : spans_around(camera, network, tool.zone)) + { + auto const a{wire_of(*catenary_scene_get(span.from))}; + auto const b{wire_of(*catenary_scene_get(span.to))}; + auto const over{span.limit > 0.0 && span.length > span.limit + 0.05}; + auto const chosen{span.chain >= 0 && (span.chain == tool.chain_selected || (is_intent(e.m_intent, intent_detail::chain) && e.m_intent.index == span.chain))}; + auto const colour{over ? overlay_color::invalid : chosen ? overlay_color::selected : span.chain >= 0 ? IM_COL32(90, 230, 120, 200) : IM_COL32(90, 230, 120, 110)}; + projection.line(drawlist, a, b, colour, over || chosen ? 2.5f : 1.2f); + ImVec2 middle; + if (projection.project(0.5 * (a + b), middle)) + { + auto const label{span.limit > 0.0 ? format("%.1f / %.0f m", span.length, span.limit) : format("%.1f m", span.length)}; + auto const text{span.radius > 0.0 && span.radius < kCurveRadius ? label + format(" R %.0f", span.radius) : label}; + drawlist->AddText(ImVec2(middle.x + 4.0f, middle.y + 2.0f), colour, text.c_str()); + } + } + // what a click puts there, with the span from the last pole + if (is_intent(e.m_intent, intent_detail::place) && e.m_hover.track != nullptr) + { + catenary_spot spot; + if (catenary_spot_at(*e.m_hover.track, e.m_hover.path, e.m_hover.point, e.cursor_level(e.m_hover.point.y), 0, spot)) + { + auto const colour{spot.card != 0 && false == spot.switch_error ? overlay_color::marked : overlay_color::invalid}; + draw_pole(spot.data, colour, 3.0f); + projection.line(drawlist, spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, pole_of(spot.data), colour, 1.5f); + if (tool.chaining && tool.last >= 0) + if (auto const *previous{catenary_scene_get(static_cast(tool.last))}; previous != nullptr) + projection.line(drawlist, wire_of(*previous), wire_of(spot.data), colour, 1.5f); + } + } +} + +float const kDragStart{6.0f}; + +void catenary_editor::catenary_drag_update() +{ + auto &tool{m_catenary}; + if (tool.dragging == 0) + return; + if (false == ImGui::GetIO().MouseDown[0]) + { + catenary_release(); + return; + } + auto const &mouse{ImGui::GetIO().MousePos}; + if (glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) <= kDragStart) + return; + if (tool.dragging == 1) + { + // the pole along the track: the place under the cursor, the side and the stagger it has + auto const *found{tool.selected >= 0 ? catenary_spot_of(static_cast(tool.selected)) : nullptr}; + auto const *current{tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr}; + if (found == nullptr || current == nullptr || found->track == nullptr || found->track->m_editorremoved) + return; + auto *track{e.m_hover.track != nullptr ? e.m_hover.track : found->track}; + auto const path{e.m_hover.track != nullptr ? e.m_hover.path : found->path}; + auto const side{found->side}; + catenary_spot spot; + { + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(*track, path, e.cursor_level(found->axis.y), e.cursor_level(found->axis.y), side, spot)) + return; + } + auto const *catalog{catenary_scene_catalog()}; + auto const *own{catalog != nullptr ? catalog->pole(current->pole_type) : nullptr}; + auto const drop{own != nullptr && false == own->drop.empty()}; + if (drop || (own != nullptr && own->gantry)) + return; // a drop, or a cantilever on a mast, goes with its gantry + spot.curve = tool.bend == 2 || (tool.bend == 0 && spot.radius > 0.0 && spot.radius < kCurveRadius); + catenary_compose(spot, tool.clearance, tool.stagger, tool.toward); + if (spot.card != 0) + catenary_apply(spot); + return; + } + if (tool.dragging == 2 && false == tool.drag_group.empty() && tool.gantry_selected >= 0) + { + auto const now{e.cursor_ground()}; + auto dx{now.x - tool.drag_start.x}, dz{now.z - tool.drag_start.z}; + // held at its distance from the track: the first mast only along the track + if (glm::dvec3 place; tool.gantry_snap && tool.drag_axis > 0.0 && catenary_along_track(tool.drag_anchor + glm::dvec3{dx, 0.0, dz}, tool.drag_axis, place)) + { + dx = place.x - tool.drag_anchor.x; + dz = place.z - tool.drag_anchor.z; + } + // the first move of the drag opens one undo group for all the spans and what hangs on them + if (tool.drag_undo == 0) + { + tool.drag_undo = ++tool.group; + for (auto const &[span, before] : tool.drag_group) + { + catenary_change change; + change.group = tool.drag_undo; + change.gantry = true; + change.index = span; + change.gantry_before = before; + tool.undo.push_back(change); + } + for (auto const &[index, before] : tool.drag_drops) + tool.undo.push_back({tool.drag_undo, false, index, before, std::nullopt, false}); + } + for (auto const &[span, before] : tool.drag_group) + { + auto moved{before}; + for (auto &point : moved.points) + { + point[0] += dx; + point[2] += dz; + } + catenary_gantry_update(span, moved); + } + for (auto const &[index, before] : tool.drag_drops) + { + if (catenary_scene_get(index) == nullptr) + continue; + auto shifted{before}; + for (auto *point : {shifted.at, shifted.rope, shifted.pole, shifted.attach}) + { + point[0] += dx; + point[2] += dz; + } + catenary_scene_update(index, shifted); + tool.spots.erase(index); + } + } +} + +void catenary_editor::catenary_release() +{ + auto &tool{m_catenary}; + tool.dragging = 0; + tool.drag_group.clear(); + tool.drag_undo = 0; + tool.drag_drops.clear(); + tool.probed = false; +} + +void catenary_editor::catenary_gantry_reshape(std::string const &Beam, double const Turn, bool const Square) +{ + auto &tool{m_catenary}; + auto const *catalog{catenary_scene_catalog()}; + auto const *gantry{tool.gantry_selected >= 0 ? catenary_gantry_get(static_cast(tool.gantry_selected)) : nullptr}; + if (catalog == nullptr || gantry == nullptr) + return; + auto changed{*gantry}; + if (false == Beam.empty()) + changed.beam = Beam; + if (false == tool.masts.empty() && Beam.empty() && Turn == 0.0 && false == Square) + { + // the masts chosen in the panel + changed.masts[0] = tool.masts[std::clamp(tool.mast_first, 0, static_cast(tool.masts.size()) - 1)]; + if (false == gantry->masts[1].empty()) + changed.masts[1] = tool.masts[std::clamp(tool.mast_second, 0, static_cast(tool.masts.size()) - 1)]; + } + auto const single{changed.masts[1].empty()}; + auto const *beam{catalog->beam(changed.beam)}; + auto const *a{catalog->mast(changed.masts[0])}; + auto const *b{single ? nullptr : catalog->mast(changed.masts[1])}; + if (beam == nullptr || a == nullptr || (b == nullptr && false == single)) + return; + glm::dvec3 const first{gantry->points[0][0], gantry->points[0][1], gantry->points[0][2]}; + glm::dvec3 const second{gantry->points[1][0], gantry->points[1][1], gantry->points[1][2]}; + auto direction{plan_of(second) - plan_of(first)}; + direction = glm::length(direction) > 1e-6 ? glm::normalize(direction) : glm::dvec2{1.0, 0.0}; + auto const middle{0.5 * (plan_of(first) + plan_of(second))}; + if (Turn != 0.0) + { + auto const angle{glm::radians(Turn)}; + direction = {direction.x * std::cos(angle) - direction.y * std::sin(angle), direction.x * std::sin(angle) + direction.y * std::cos(angle)}; + } + if (Square) + { + // square to the track nearest to the middle of the gantry + auto best{std::numeric_limits::max()}; + glm::dvec2 along{0.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + { + bezier const curve{track->m_paths[path]}; + glm::dvec3 const probe{middle.x, 0.0, middle.y}; + if (geometry::plan_distance(curve.point(0.5), probe) > curve.plan_length() * 0.5 + std::min(best, 60.0)) + continue; + auto const parameter{nearest_parameter(*track, path, probe)}; + auto const distance{geometry::plan_distance(curve.point(parameter), probe)}; + if (distance < best) + { + best = distance; + along = forward_at(*track, path, parameter); + } + } + } + if (glm::length(along) < 0.5) + { + tool.status = STR("No track near the gantry"); + return; + } + auto const across{right_of(along)}; + direction = glm::dot(across, direction) >= 0.0 ? across : -across; + } + // a single element: the tip point at its length from the face of the mast (only its direction matters) + auto const spacing{single ? beam->span + a->offset : catenary::gantry_spacing(*beam, *a, *b)}; + // turned or squared: around its first mast (the gantry is set out from it on the ground); a new beam or masts: the first mast stays + glm::dvec2 const start{plan_of(first)}; + auto const end{start + direction * spacing}; + changed.points[0][0] = start.x; + changed.points[0][2] = start.y; + changed.points[1][0] = end.x; + changed.points[1][2] = end.y; + auto const selected{static_cast(tool.gantry_selected)}; + // the new places of the spans: this one, and in a gantry of many spans the others joined with it + std::vector> moves; + auto const group{gantry_group(selected)}; + if (group.size() > 1 && (Turn != 0.0 || Square)) + { + // turned or squared: the whole gantry around its middle, by the angle this span turns + auto const before{plan_of(second) - plan_of(first)}; + auto const after{direction}; + auto const angle{std::atan2(before.x * after.y - before.y * after.x, glm::dot(before, after))}; + // around the first mast of the gantry: the first mast of its first span (they're all in one line, at one angle) + auto const firstspan{*std::min_element(group.begin(), group.end())}; + glm::dvec2 const middle{catenary_gantry_get(firstspan)->points[0][0], catenary_gantry_get(firstspan)->points[0][2]}; + auto const c{std::cos(angle)}, si{std::sin(angle)}; + for (auto const span : group) + { + auto turned{*catenary_gantry_get(span)}; + for (auto &point : turned.points) + { + glm::dvec2 const p{point[0] - middle.x, point[2] - middle.y}; + point[0] = middle.x + p.x * c - p.y * si; + point[2] = middle.y + p.x * si + p.y * c; + } + moves.emplace_back(span, turned); + } + } + else + { + moves.emplace_back(selected, changed); + // another beam or masts: the spans beyond a mast which moved go with it + for (int k = 0; k < 2; ++k) + { + glm::dvec2 const shift{changed.points[k][0] - gantry->points[k][0], changed.points[k][2] - gantry->points[k][2]}; + if (glm::length(shift) < 1e-4) + continue; + // the spans joined at this mast, and everything joined with them except through this span + for (std::size_t other = 0; other < catenary_gantry_size(); ++other) + { + auto const *candidate{catenary_gantry_get(other)}; + if (candidate == nullptr || other == selected || false == (same_mast(candidate->points[0], gantry->points[k]) || same_mast(candidate->points[1], gantry->points[k]))) + continue; + for (auto const span : gantry_group(other, selected)) + { + if (std::any_of(moves.begin(), moves.end(), [span](auto const &Move) { return Move.first == span; })) + continue; + auto shifted{*catenary_gantry_get(span)}; + for (auto &point : shifted.points) + { + point[0] += shift.x; + point[2] += shift.y; + } + moves.emplace_back(span, shifted); + } + } + } + } + auto const &last{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; + // turning in small steps is taken back at once + if (Turn == 0.0 || tool.undo.empty() || false == last.gantry || last.index != selected || false == last.gantry_before.has_value()) + { + ++tool.group; + // the selected span last: the next small turn finds it at the top of the undo and joins this group + for (auto move{moves.rbegin()}; move != moves.rend(); ++move) + { + catenary_change change; + change.group = tool.group; + change.gantry = true; + change.index = move->first; + change.gantry_before = *catenary_gantry_get(move->first); + tool.undo.push_back(change); + } + } + for (auto const &move : moves) + catenary_gantry_update(move.first, move.second); + tool.status = format(STR_C("Gantry %s: beam %s, masts %.3f m apart"), changed.name.c_str(), changed.beam.c_str(), spacing); +} + +// the hooks of editor_mode (editormode.h), and what they did in the editor before the tool got a file of its own + +bool catenary_editor::catenary_intent(track_intent &Intent) +{ + using kind = track_intent::kind; + if (m_catenary.gantry_placing) + { + Intent.position = catenary_mast_point(); + Intent.action = m_catenary.gantry_first.has_value() ? STR("Click: the second mast, as far as the nearest beam spans") : STR("Click: the first mast of the gantry"); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::mast); + return true; + } + if (auto const hit{catenary_hit()}; hit >= 0) + { + Intent.index = hit; + Intent.action = m_catenary.connect_from >= 0 ? STR("Click: hang the span to this pole") : STR("Click: select the pole"); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::select); + return true; + } + if (auto const hit{catenary_gantry_hit()}; hit >= 0) + { + Intent.index = hit; + Intent.action = hit == m_catenary.gantry_selected ? STR("Drag: move the gantry") : STR("Click: put the cantilevers on drops on this gantry, drag: move it"); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::gantry); + return true; + } + if (auto const hit{catenary_span_hit()}; hit >= 0) + { + Intent.index = hit; + Intent.action = STR("Click: select the chain of this span"); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::chain); + return true; + } + // the wires page places nothing, the gantries page places only on the selected gantry + if (e.m_hover.track != nullptr && m_catenary.page != 2 && (m_catenary.page != 1 || m_catenary.gantry_selected >= 0)) + { + catenary_spot spot; + if (catenary_spot_at(*e.m_hover.track, e.m_hover.path, e.m_hover.point, e.cursor_level(e.m_hover.point.y), 0, spot)) + { + Intent.track = e.m_hover.track; + Intent.path = e.m_hover.path; + Intent.position = spot.axis; + Intent.action = catenary_intent_label(spot); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::place); + return true; + } + } + return false; +} + +std::string catenary_editor::catenary_mode_name() const +{ + if (m_catenary.page == 2) + return m_catenary.connect_from >= 0 ? STR_C("Wires: LMB on the next pole hangs the span to it") : STR_C("Wires: LMB on a span selects its chain, LMB on a pole selects it"); + if (m_catenary.page == 1) + { + if (m_catenary.gantry_placing) + return STR_C("Gantries: LMB the masts"); + return m_catenary.gantry_selected >= 0 ? STR_C("Gantries: LMB by a track puts a cantilever on the selected gantry, drag the beam to move it") : STR_C("Gantries: LMB on a gantry selects it, or place a new one below"); + } + if (m_catenary.selected >= 0) + return STR_C("Catenary: the selected pole follows the settings below"); + return STR_C("Catenary: LMB by the track places a pole on that side, LMB on a pole selects it"); +} + +void catenary_editor::catenary_key_hints(std::vector &Hints) const +{ + Hints = {{"LMB by a track", "places a pole on that side, with the card for its distance"}, {"LMB on a pole", "selects it: the settings then change it at once"}}; + if (m_catenary.selected >= 0) + Hints.push_back({"Del", "removes the selected pole"}); +} + +bool catenary_editor::catenary_on_key(int const Key, int const Action, int const Mods) +{ + return false; +} + +catenary_editor &editor_mode::catenary_tool() +{ + if (false == static_cast(m_catenary_editor)) + m_catenary_editor = std::make_shared(*this); + return *m_catenary_editor; +} + +void editor_mode::catenary_scan() { catenary_tool().catenary_scan(); } +bool editor_mode::catenary_intent(track_intent &Intent) { return catenary_tool().catenary_intent(Intent); } +void editor_mode::catenary_press(track_intent const &Intent) { catenary_tool().catenary_press(Intent); } +void editor_mode::catenary_release() { catenary_tool().catenary_release(); } +bool editor_mode::catenary_dragging() const { return m_catenary_editor && m_catenary_editor->m_catenary.dragging != 0; } +void editor_mode::catenary_drag_update() { catenary_tool().catenary_drag_update(); } +void editor_mode::draw_catenary_overlay() const +{ + if (m_catenary_editor) + m_catenary_editor->draw_catenary_overlay(); +} +void editor_mode::render_catenary_ui() { catenary_tool().render_catenary_ui(); } +bool editor_mode::catenary_delete_selected() { return catenary_tool().catenary_delete_selected(); } +bool editor_mode::catenary_undo() { return catenary_tool().catenary_undo(); } +int editor_mode::catenary_page() { return catenary_tool().m_catenary.page; } +void editor_mode::catenary_choose_page(int const Page) { catenary_tool().catenary_choose_page(Page); } +bool editor_mode::catenary_on_key(int const Key, int const Action, int const Mods) { return catenary_tool().catenary_on_key(Key, Action, Mods); } +std::string editor_mode::catenary_mode_name() const { return m_catenary_editor ? m_catenary_editor->catenary_mode_name() : STR_C("Catenary: LMB by the track places a pole on that side, LMB on a pole selects it"); } +void editor_mode::catenary_key_hints(std::vector &Hints) const +{ + if (m_catenary_editor) + m_catenary_editor->catenary_key_hints(Hints); +} diff --git a/application/editormodecatenary.h b/application/editormodecatenary.h new file mode 100644 index 00000000..c17abab0 --- /dev/null +++ b/application/editormodecatenary.h @@ -0,0 +1,208 @@ +/* +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/. +*/ + +// The catenary tool of the editor (sieć trakcyjna): its state and workings. The editor (editormode.h) holds it behind a +// pointer and calls it through a few fixed hooks, so a change here rebuilds only editormodecatenary.cpp, not the files +// which include editormode.h. The tool is a friend of editor_mode and reaches the editor through `e`. + +#pragma once + +#include "application/editormode.h" +#include "world/catenary_suspension.h" +#include "world/catenary_cantilever.h" + +#include +#include +#include +#include + +struct catenary_editor +{ + explicit catenary_editor(editor_mode &Editor) : e{Editor} {} + editor_mode &e; + using track_intent = editor_mode::track_intent; + using key_hint = editor_mode::key_hint; + // what a catenary intent (track_intent::kind::catenary) is about, in its detail + enum class intent_detail { place, select, chain, gantry, mast }; + static bool is_intent(track_intent const &Intent, intent_detail const Detail) + { + return Intent.what == track_intent::kind::catenary && Intent.detail == static_cast(Detail); + } + + // catenary: poles with cantilevers by the track, put where the user sees them (on the orthophoto), the cantilever card + // chosen from the catalogue by the distance from the face of the pole; spans checked against the catalogue tables + struct catenary_spot + { + TTrack *track{nullptr}; + int path{0}; + double parameter{0.0}; + glm::dvec3 axis{0.0}; // the point of the path, under the rail heads + glm::dvec2 forward{0.0, 1.0}; + int side{1}; // the pole on the right (+1) or on the left (-1) of the direction of the path + double radius{0.0}; // of the track there, 0 on a straight + bool curve{false}; + bool toward{true}; // the wires are offset towards the pole + double reach{0.0}; // A: from the wires to the face of the pole + int card{0}; + std::string problem; // why there's no card, empty when there is one + std::string family; // the family of the card in the described catalogue; empty without it + bool execution_changed{false}; // the execution chosen in the panel has no card for this A, another one was taken + bool switch_error{false}; // a pole of a group that doesn't stand by switches (concrete), standing by one + catenary::suspension data; + }; + struct catenary_change + { + int group{0}; // changes of one action are taken back together + bool chain{false}; // a change of a chain, otherwise of a suspension + std::size_t index{0}; + std::optional before; // none: the suspension was added + std::optional chain_before; // none: the chain was added + bool removed{false}; // the suspension was removed + bool gantry{false}; // a change of a gantry + std::optional gantry_before; // none: the gantry was added + bool anchor{false}; // a change of an anchor + std::optional anchor_before; // none: the anchor was added + }; + struct catenary_tool + { + std::vector poles; + std::vector networks; + int pole{0}; + int network{0}; + int zone{1}; + int execution{1}; + bool swinging{true}; + bool toward{true}; + bool alternate{true}; // on a straight the next one goes the other way + int bend{0}; // 0: from the track, 1: straight, 2: curve + float stagger{0.30f}; + float clearance{2.50f}; // from the axis of the track to the face of the pole + float height{5.60f}; // of the contact wires over the rails + float system_height{0.0f}; // hk: of the messenger over the contact wires; 0 = the card's + bool insulators25{false}; + char file[128]{}; + int selected{-1}; + // chains: the poles placed one after another are joined by spans + bool chaining{true}; + int chain{-1}; // the chain the next pole is added to + int last{-1}; // the pole the next span starts at + bool append{true}; // the next pole goes after the last one of the chain, otherwise before the first + int chain_selected{-1}; // the chain shown in the panel + int connect_from{-1}; // hanging the wires on from this suspension: the next poles clicked get the spans + char power[64]{"none"}; + float voltage{3000.0f}; + int material{0}; // 0 cu, 1 al + int state{0}; // 0 new, 1 worn, 2 tarnished (state 0, 2, 1 in the entry) + std::map spots; // where the suspensions are by the track, by their numbers + std::vector undo; + int group{0}; + catenary::cantilever_result probe; + bool probed{false}; + bool probe_good{false}; + std::string status; + bool scanned{false}; + // gantries: two masts and the beam between them, drops with cantilevers on the beam + std::vector masts; + int mast_first{0}; + int mast_second{0}; + bool mast_second_own{false}; // the second mast was picked by the user: it no longer follows the first + bool gantry_high{false}; + int gantry_side{1}; + bool gantry_placing{false}; // the next two clicks put the masts + bool gantry_single{false}; // what's placed is a single beam element on one through mast: the second click sets its direction + std::optional gantry_first; // the first mast, waiting for the second + std::optional gantry_track; // the direction of the track by the first mast, for the beam square to it + bool gantry_square{true}; // the beam goes square to the track + bool gantry_snap{true}; // the first mast goes at gantry_face from the axis of the track under the cursor + int shift_group{0}; // the undo group of the distance of a mast being dragged; 0: the next change opens one + float gantry_face{2.50f}; // from the axis of the track to the face of the mast (towards the track) + char gantry_skin[128]{}; // the replaceable texture of the masts, the beam and the drops + int dragging{0}; // 1: the selected pole along the track, 2: the selected gantry along the track + glm::vec2 pressed{0.f}; + glm::dvec3 drag_start{0.0}; // the point of the ground the drag started at + std::vector> drag_group; // the spans of the dragged gantry as they were when the drag started + int drag_undo{0}; + glm::dvec3 drag_anchor{0.0}; // the first mast of the dragged gantry where the drag started + double drag_axis{0.0}; // its axis from the axis of the track then: the drag keeps it there ("first mast from the track") // the undo group of the drag, opened at its first move + std::vector> drag_drops; // the drops of the gantry, as they were + int gantry_selected{-1}; // the gantry the drops go to + std::string drop_choice; // the drop for the next cantilevers on a gantry; empty: the shortest one which fits + // the described catalogue (data/traction_catalog.txt): the group the user picks, the family and the card in it + // (empty / 0: chosen by the track, the stagger and the distance A) + std::string catalog_kind; + std::string catalog_group; + std::string catalog_family; + int catalog_card{0}; + int page{0}; // the page of the catenary field of work: 0 poles, 1 gantries, 2 wires + int gantry_hang{0}; // the next cantilevers on the selected gantry: 0 on drops of the beam, 1 straight on its masts + }; + catenary_tool m_catenary; + void catenary_scan(); + bool catenary_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Side, catenary_spot &Spot) const; + // a pole of this type can't stand there: its group doesn't stand by switches and a switch is by it + bool catenary_switch_error(std::string const &Pole, glm::dvec3 const &Foot, TTrack const *Track) const; + bool catenary_switch_error(std::size_t const Index) const; // of a placed pole, remembered until it moves or changes type + struct switch_check + { + glm::dvec3 foot{0.0}; + std::string pole; + bool error{false}; + }; + mutable std::vector> m_switch_paths; // the paths of switches, gathered again when the number of tracks changes + mutable std::size_t m_switch_tracks{static_cast(-1)}; + mutable std::map m_switch_checks; + void catenary_compose(catenary_spot &Spot, double const Clearance, double const Stagger, bool const Toward, std::string const &Pole = {}) const; + bool catenary_on_mast(catenary::gantry const &Gantry, double const Stagger, catenary_spot &Spot) const; + catenary_spot const *catenary_spot_of(std::size_t const Index); + int catenary_hit() const; + int catenary_span_hit() const; + void catenary_press(track_intent const &Intent); + void catenary_select(int const Index); + void catenary_apply(catenary_spot const &Spot); + void catenary_copy(std::vector const &Targets); + bool catenary_propose_toward(bool &Toward); + void catenary_place(catenary_spot const &Spot); + void catenary_chain_apply(); + void catenary_connect(std::size_t const To); + int catenary_gantry_hit() const; + bool catenary_gantry_beam(std::size_t const Index, glm::dvec3 &From, glm::dvec3 &To, double &Bottom) const; + bool catenary_gantry_second(glm::dvec3 const &Point, glm::dvec3 &Second, std::string &Beam) const; + glm::dvec3 catenary_mast_point() const; + // the face of the mast from the axis of the nearest track (within 20 m); false: no track near + bool catenary_mast_face(std::string const &Mast, glm::dvec3 const &Point, double &Face, glm::dvec2 &Away) const; + // the selected gantry (all its spans and what hangs on them) moved in the plan + void catenary_gantry_shift(glm::dvec2 const &Shift); + // the place of the first mast at Axis from the axis of the track nearest to Point, on the side of Point; false: no track near + bool catenary_along_track(glm::dvec3 const &Point, double const Axis, glm::dvec3 &Place) const; + bool catenary_on_gantry(catenary_spot &Spot); + void catenary_drag_update(); + // the selected gantry anew: another beam, masts, turned around its middle (degrees), or square to the nearest track + void catenary_gantry_reshape(std::string const &Beam, double const Turn, bool const Square); + void catenary_release(); + void catenary_gantry_hangers(std::size_t const Gantry, std::vector> &Hangers) const; + bool catenary_delete_selected(); + bool catenary_undo(); + void render_catenary_ui(); + bool render_catenary_catalog_ui(); + void render_catenary_chain_ui(); + // the anchors at both ends of the selected chain: add, settings, remove + void render_catenary_anchor_ui(std::size_t Chain); + // the anchor of the chain at its start (Start) or end; catenary_anchor_size() when none + std::size_t catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool Start) const; + void render_catenary_cantilever_ui(); + void render_catenary_wires_ui(); + void catenary_choose_page(int const Page); + void draw_catenary_overlay() const; + std::string catenary_intent_label(catenary_spot const &Spot) const; + // the hooks of editor_mode + bool catenary_intent(track_intent &Intent); + std::string catenary_mode_name() const; + void catenary_key_hints(std::vector &Hints) const; + bool catenary_on_key(int const Key, int const Action, int const Mods); +}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b78..b9edc9ac 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -577,9 +577,9 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve profile_restore(); } -std::array const &editor_mode::track_modes() +std::array const &editor_mode::track_modes() { - static std::array const modes{{ + static std::array const modes{{ {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters"}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track"}, {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch"}, @@ -587,6 +587,7 @@ std::array const &editor_mode::track_modes() {"Straight", "G", track_tab::straights, "The whole straight through the selected path: drag its ends or the middle, break it, shift it"}, {"Curve", "C", track_tab::route, "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves"}, {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by"}, + {"Catenary", "N", track_tab::catenary, "Poles with cantilevers by the track, the card chosen from the catalogue by the distance of the pole; the spans checked against the catalogue"}, {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, a vehicle to drive"}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"}, @@ -644,6 +645,9 @@ void editor_mode::show_track_tab(track_tab const Tab) signal_scan(); signal_refresh(true); break; + case track_tab::catenary: + catenary_scan(); + break; default: break; } @@ -719,6 +723,8 @@ std::string editor_mode::track_mode_name() const if (m_signal.placing) return STR_C("Signal: drag along the track sets the direction of the trains, release to place it"); return signal_armed() != nullptr ? STR_C("Signal: LMB by the track places it on that side") : STR_C("Signal: choose its type below"); + case track_tab::catenary: + return catenary_mode_name(); case track_tab::turntable: if (m_turntable.placing) return STR_C("Turntable: drag to turn the bridge, release to place it"); @@ -737,6 +743,7 @@ void editor_mode::render_track_inspector() if (false == m_track_window_open || m_track_tab != track_tab::lineside) m_sweep.open = m_hekto.open = m_fouling.open = m_parallel.open = m_vehicle.open = false; ui()->set_track(m_track_window_open); + ui()->set_track_title(m_track_tab == track_tab::catenary ? STR("Catenary") : std::string{}); if (false == m_track_window_open) { // a closed window takes its mode with it @@ -774,9 +781,13 @@ void editor_mode::render_track_tool_options() { auto *track{selected_track()}; // the tools themselves are in the toolbar - render_track_search(); - ImGui::Spacing(); - render_track_guide(); + if (m_track_tab != track_tab::catenary) + { + // the catenary is a field of work of its own: no search of the paths there + render_track_search(); + ImGui::Spacing(); + render_track_guide(); + } switch (m_track_tab) { case track_tab::lay: @@ -804,6 +815,7 @@ void editor_mode::render_track_tool_options() break; case track_tab::lineside: render_lineside_ui(); break; case track_tab::signals: render_signal_ui(); break; + case track_tab::catenary: render_catenary_ui(); break; case track_tab::turntable: render_turntable_ui(); break; default: render_track_set_ui(); @@ -3515,6 +3527,10 @@ std::vector editor_mode::track_key_hints(bool const All) if (m_signal.selected != 0) hints.push_back({"Del", "removes the selected signal with its event"}); } + else if (m_track_window_open && m_track_tab == track_tab::catenary) + { + catenary_key_hints(hints); + } else if (m_track_window_open && m_track_tab == track_tab::joints) hints = {{"LMB on a row", "show the place"}, {"Fix", "joins the ends, aligns them"}}; else if (m_track_window_open && m_track_tab == track_tab::infra) @@ -5053,8 +5069,11 @@ void editor_mode::draw_build_overlay() const } }; auto const &display{ImGui::GetIO().DisplaySize}; + // the ends of all the paths; not in the catenary, where they're of no use and cost a lot in a big scenery for (auto const *track : simulation::Paths.sequence()) { + if (m_track_tab == track_tab::catenary) + break; if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) continue; for (auto const &path : track->m_paths) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 9e19d1b2..492df7af 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -40,6 +40,7 @@ namespace using geometry::bezier; float const kHoverPixels{12.0f}; +double const kCatenaryHoverRange{1500.0}; // m from the camera: the paths the catenary looks at under the cursor double const kHoverLabelDelay{0.35}; // s the cursor rests on a path before its data shows up double const kFocusDistance{25.0}; auto const kContextClick{std::chrono::milliseconds(350)}; @@ -106,6 +107,9 @@ editor_mode::track_hover editor_mode::track_under_cursor() const for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { bezier const curve{track->m_paths[i]}; + // the catenary works close to the camera: the paths far away aren't looked at (in a big scenery they cost a lot) + if (m_track_tab == track_tab::catenary && glm::distance(curve.point(0.5), glm::dvec3{Global.pCamera.Pos}) > curve.plan_length() * 0.5 + kCatenaryHoverRange) + continue; auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; auto previous{curve.point(0.0)}; ImVec2 from; @@ -181,7 +185,7 @@ void editor_mode::draw_track_hover() const bool editor_mode::track_gesture_on() const { return mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing || m_straights.dragging || m_straights.tool_mouse || - m_point_drag.active || m_handle_drag.active || m_sweep.dragging || m_turntable.placing || m_signal.placing || m_signal.moving || ImGuizmo::IsUsing(); + m_point_drag.active || m_handle_drag.active || m_sweep.dragging || m_turntable.placing || m_signal.placing || m_signal.moving || catenary_dragging() || ImGuizmo::IsUsing(); } editor_mode::track_intent editor_mode::track_intent_at(int const Mods) @@ -220,6 +224,11 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) } if (m_track_tab == track_tab::turntable) return turntable_intent(ground, shift); + if (m_track_tab == track_tab::catenary) + { + catenary_intent(intent); + return intent; + } if (m_track_tab == track_tab::signals) { if (auto const hit{signal_hit()}; hit >= 0) @@ -453,6 +462,7 @@ void editor_mode::track_press(track_intent const &Intent) case kind::turntable_fit: turntable_press(Intent); return; case kind::signal_place: case kind::signal_move: signal_press(Intent); return; + case kind::catenary: catenary_press(Intent); return; case kind::select: case kind::none: { diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index 817658e3..7c3b199b 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -185,6 +185,15 @@ void editor_mode::work_area_tools(std::vector &Buttons) [this]() { m_gauge.open = !m_gauge.open; }}); break; } + case work_area::catenary: + { + // the pages of the catenary: poles with cantilevers, gantries, the wires of the chains + auto const page{catenary_page()}; + Buttons.push_back({icon::catenary, STR_C("Poles"), nullptr, STR_C("Poles with cantilevers: LMB by a track places one, LMB on a pole selects it"), page == 0, [this]() { catenary_choose_page(0); }}); + Buttons.push_back({icon::gantry, STR_C("Gantries"), nullptr, STR_C("Gantries: masts and beams, the cantilevers on their drops and masts"), page == 1, [this]() { catenary_choose_page(1); }}); + Buttons.push_back({icon::wires, STR_C("Wires"), nullptr, STR_C("The chains: the type of the network, the wires, the spans between the poles"), page == 2, [this]() { catenary_choose_page(2); }}); + break; + } case work_area::roads: { auto &tool{m_roadtool}; diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index ee1a7edc..7a0e9cdf 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -736,7 +736,7 @@ void editor_ui::render_tool_options() name = STR_C("Copy to bank"); break; case nodebank_panel::TRACK: - name = STR_C("Tracks"); + name = m_tracktitle.empty() ? STR_C("Tracks") : m_tracktitle.c_str(); break; default: break; diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 020fd4be..ccd7d6ed 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -55,6 +55,8 @@ class editor_ui : public ui_layer m_filloptions = std::move(Renderer); } // the open track window takes the mouse over, whichever toolset tab is chosen + // the title of the tool options of the tracks; empty: "Tracks" + void set_track_title(std::string Title) { m_tracktitle = std::move(Title); } void set_track(bool const Track) { m_track = Track; @@ -207,6 +209,7 @@ class editor_ui : public ui_layer bool m_sceneopen{true}; std::function m_sceneselect; std::function m_tracktools; + std::string m_tracktitle; std::function m_trackselection; int m_toolmode{-1}; // edit mode or field of work the tool options were drawn for the last time workspace m_workspace{workspace::none}; diff --git a/editor/editorIcons.hpp b/editor/editorIcons.hpp index 91c9a5f3..7d1571f8 100644 --- a/editor/editorIcons.hpp +++ b/editor/editorIcons.hpp @@ -50,11 +50,15 @@ enum class icon infra, gauge, turntable, + catenary, // the fields of work work_surroundings, work_tracks, work_roads, work_terrain, + work_catenary, + gantry, + wires, count }; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 418d908f..7e553fa9 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -2280,9 +2280,17 @@ std::vector editor_track::standard_switch_templat double b; }; definition const definitions[] = { + // a = R tg(alfa/2) (do punktu matematycznego), b = a + prosta za lukiem (wzdluz toru zwrotnego). Geometria 49E1 jak DB EW 49-190: + // prosta za lukiem 1:9 6,092 m (Glowacki s. 418), 1:7,5 4,817 m, 1:7,5/6,6 (luk do 1:6,6) 1,415 m; dlugosc 1:7,5 i 1:6,6 ta sama 30,04 m + // (s21-modellgleis.de, zusiwiki). 1:12 R500 - krzyzownica lukowa, luk do konca: b = a (rozstaw na koncu 1,729 m sie zgadza). + {"Rz 1:6,6 R190", 190.0, 6.6, 14.312, 15.727}, + {"Rz 1:7,5 R190", 190.0, 7.5, 12.611, 17.428}, {"Rz 1:9 R190", 190.0, 9.0, 10.523, 16.615}, {"Rz 1:9 R300", 300.0, 9.0, 16.615, 16.615}, - {"Rz 1:12 R500", 500.0, 12.0, 20.797, 0.0}, + {"Rz 1:12 R500", 500.0, 12.0, 20.797, 20.797}, + // 1:14 R500 - krzyzownica prosta; rozstaw na koncu 1,749 m jak w 49-300-1:14 (b 24,537 = 10,701 + prosta 13,836, s21-modellgleis.de), + // wiec ten sam koniec: b = 24,537, prosta za lukiem 6,703 m + {"Rz 1:14 R500", 500.0, 14.0, 17.834, 24.537}, {"Rz 1:14 R760", 760.0, 14.0, 0.0, 0.0}, {"Rz 1:18,5 R1200", 1200.0, 18.5, 32.409, 34.943}, {"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0}, diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png index 50878092d771399b35673dff962b91e6b053b04e..8214e55180c634ca579c04d7f6cb5ba03ea88ecd 100644 GIT binary patch literal 51260 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 _L^|=X<2GbaqHA$M* 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 diff --git a/editor/icons/editor_icons.py b/editor/icons/editor_icons.py index 9ef66aa1..94b4fd2d 100644 --- a/editor/icons/editor_icons.py +++ b/editor/icons/editor_icons.py @@ -321,6 +321,19 @@ def turntable(d): line(d, [(32 + math.cos(r) * 24, 32 + math.sin(r) * 24), (32 + math.cos(r) * 31, 32 + math.sin(r) * 31)], W - 4) +def catenary(d): + # a lattice pole with a swinging cantilever, the wire under its end + line(d, [(10, 60), (12, 6)], W - 8) + line(d, [(22, 60), (20, 6)], W - 8) + for y0 in (8, 26, 44): + line(d, [(11, y0), (21, y0 + 16)], W - 12) + line(d, [(21, y0), (11, y0 + 16)], W - 12) + line(d, [(22, 12), (56, 12)], W - 8) + line(d, [(22, 40), (54, 14)], W - 6) + line(d, [(46, 20), (46, 36)], W - 10) + line(d, [(30, 38), (62, 38)], W - 6) + + # the fields of work of the editor, in the row over the toolbar @@ -363,11 +376,46 @@ def work_terrain(d): line(d, [at(u, v) for v in steps], W - 6) +def work_catenary(d): + # a pole with its cantilever and the span of the catenary going off from it: the messenger sagging, the droppers, the contact wire + def sag(x): + return 12 + 14 * (1 - ((x - 42) / 20.0) ** 2) + line(d, [(8, 60), (8, 6)], W - 2) + line(d, [(8, 12), (24, 12)], W - 8) + line(d, [(8, 30), (24, 12)], W - 8) + line(d, [(x, sag(x)) for x in range(22, 63, 2)], W - 8) + for x in (30, 42, 54): + line(d, [(x, sag(x)), (x, 36)], W - 12) + line(d, [(22, 38), (62, 38)], W - 6) + line(d, [(2, 60), (34, 60)], W - 8) + + +def gantry(d): + # two lattice masts and the beam over the tracks, a drop with the wire under it + for x in (8, 56): + line(d, [(x, 60), (x, 8)], W - 2) + rect(d, 8, 8, 56, 18, outline=True, width=W - 8) + for x0 in (14, 26, 38): + line(d, [(x0, 18), (x0 + 12, 8)], W - 12) + line(d, [(32, 18), (32, 34)], W - 8) + line(d, [(18, 40), (46, 40)], W - 6) + + +def wires(d): + # one span: the messenger sagging between its points, the droppers and the contact wire under it + def sag(x): + return 10 + 20 * (1 - ((x - 32) / 28.0) ** 2) + line(d, [(x, sag(x)) for x in range(4, 61, 2)], W - 6) + for x in (14, 23, 32, 41, 50): + line(d, [(x, sag(x)), (x, 46)], W - 12) + line(d, [(4, 48), (60, 48)], W - 4) + + ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale, local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road, place, lanes, lay_track, switch, straight, curve, signal, objects, - vehicle, profile, speed, joints, infra, gauge, turntable, work_surroundings, - work_tracks, work_roads, work_terrain] + vehicle, profile, speed, joints, infra, gauge, turntable, catenary, work_surroundings, + work_tracks, work_roads, work_terrain, work_catenary, gantry, wires] def cutouts(name, d): diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 58b30109..17e0fdbe 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5213,3 +5213,684 @@ msgstr "Punkt znikania" msgid "Pedestrian crossing" msgstr "Przejście dla pieszych" + +msgid "No pole: the catalogue (data/traction_cantilevers.txt, data/traction_supports.txt) isn't loaded or has no poles" +msgstr "Brak słupa: katalog (data/traction_cantilevers.txt, data/traction_supports.txt) nie jest wczytany albo nie ma w nim słupów" + +msgid "No card %d0-%d9 in execution %d for A = %.2f m (cards cover A 1.45-4.45 m, not all of them are in every family)" +msgstr "Brak karty %d0-%d9 w wykonaniu %d dla A = %.2f m (karty obejmują A 1,45-4,45 m, nie w każdej rodzinie są wszystkie)" + +msgid "curve R %.0f m" +msgstr "łuk R %.0f m" + +msgid "towards the pole" +msgstr "do słupa" + +msgid "away from the pole" +msgstr "od słupa" + +msgid "Pole here, %s, wires %s: %s" +msgstr "Słup tutaj, %s, odsuw %s: %s" + +msgid "Click: pole %s, card %d execution %d, A = %.2f m (%s, wires %s)" +msgstr "Klik: słup %s, karta %d wykonanie %d, A = %.2f m (%s, odsuw %s)" + +msgid "The suspension isn't by any track: it can't be changed here" +msgstr "Podwieszenie nie stoi przy żadnym torze: tutaj go nie zmienisz" + +msgid "Pole %s with card %d execution %d placed, A = %.2f m" +msgstr "Postawiony słup %s z kartą %d wykonanie %d, A = %.2f m" + +msgid " - the cantilever didn't assemble, see the log" +msgstr " - wysięgnik się nie złożył, szczegóły w logu" + +msgid "Pole removed" +msgstr "Słup usunięty" + +msgid "Taken back" +msgstr "Cofnięte" + +msgid "The cantilever catalogue isn't loaded: data/traction_cantilevers.txt and data/traction_supports.txt (with the poles) are needed" +msgstr "Katalog wysięgników nie jest wczytany: potrzebne są data/traction_cantilevers.txt i data/traction_supports.txt (ze słupami)" + +msgid "Catenary file" +msgstr "Plik sieci" + +msgid "File in the scenery directory the new poles go to; a new one is added to the scenery with the directive catenary endcatenary when it's saved" +msgstr "Plik w katalogu scenerii, do którego trafiają nowe słupy; nowy plik jest dopisywany do scenerii dyrektywą catenary endcatenary przy zapisie" + +msgid "Selected pole (%s): changes apply to it at once" +msgstr "Zaznaczony słup (%s): zmiany od razu na nim" + +msgid "Pole" +msgstr "Słup" + +msgid "Face of the pole from the track axis (m)" +msgstr "Czoło słupa od osi toru (m)" + +msgid "Structure gauge 2.20 or 2.50 m by the catalogue (card 0007); drag it and the cantilever follows, the card changes with band A. Ctrl+click types a value" +msgstr "Skrajnia 2,20 albo 2,50 m wg katalogu (karta 0007); przeciągnij, a wysięgnik idzie za tym, karta zmienia się z pasmem A. Ctrl+klik wpisuje wartość" + +msgid "Stagger of the wires (m)" +msgstr "Odsuw przewodów (m)" + +msgid "Wires towards the pole" +msgstr "Odsuw do słupa" + +msgid "On a straight the wires zigzag: towards the pole and away from it in turn. On a curve they go to its outside, which decides it" +msgstr "Na prostej odsuw na zmianę do słupa i od słupa. Na łuku przewody idą na zewnątrz łuku i to rozstrzyga" + +msgid "In turn" +msgstr "Na zmianę" + +msgid "After a pole on a straight is placed, the next one gets the wires the other way" +msgstr "Po postawieniu słupa na prostej następny dostaje odsuw w drugą stronę" + +msgid "by the track" +msgstr "wg toru" + +msgid "curve" +msgstr "łuk" + +msgid "Cards for" +msgstr "Karty dla" + +msgid "Swinging cantilever (11xx)" +msgstr "Wysięgnik przechylny (11xx)" + +msgid "Off: the fixed ones (12xx)" +msgstr "Wyłączone: nieprzechylne (12xx)" + +msgid "Execution" +msgstr "Wykonanie" + +msgid "Contact wires over the rails (m)" +msgstr "Przewody jezdne nad główką szyny (m)" + +msgid "25 kV insulators" +msgstr "Izolatory 25 kV" + +msgid "Network (for the spans)" +msgstr "Typ sieci (do przęseł)" + +msgid "Wind zone I" +msgstr "Strefa wiatrowa I" + +msgid "Wind zone II" +msgstr "Strefa wiatrowa II" + +msgid "No data/traction_layout.txt: the spans aren't checked" +msgstr "Brak data/traction_layout.txt: rozpiętości przęseł nie są sprawdzane" + +msgid "Card %d execution %d, A = %.2f m, %zu parts" +msgstr "Karta %d wykonanie %d, A = %.2f m, części: %zu" + +msgid "The cantilever doesn't assemble:" +msgstr "Wysięgnik się nie składa:" + +msgid "Remove the pole" +msgstr "Usuń słup" + +msgid "Take back" +msgstr "Cofnij" + +msgid "Takes back the last change of the catenary (Ctrl+Z of the editor doesn't)" +msgstr "Cofa ostatnią zmianę sieci (Ctrl+Z edytora jej nie cofa)" + +msgid "The catenary files are written when the scenery is saved" +msgstr "Pliki sieci zapisują się razem ze scenerią" + +msgid "doesn't assemble" +msgstr "nie składa się" + +msgid "1 - two contact wires, light arms, hk 1.70" +msgstr "1 - sieć dwudrutowa, ramiona lekkie, hk 1,70" + +msgid "2 - two contact wires, heavy arms, hk 1.70" +msgstr "2 - sieć dwudrutowa, ramiona ciężkie, hk 1,70" + +msgid "3 - one contact wire, heavy arms, hk 1.70" +msgstr "3 - sieć jednodrutowa, ramiona ciężkie, hk 1,70" + +msgid "4 - one contact wire, heavy arms, hk 1.30" +msgstr "4 - sieć jednodrutowa, ramiona ciężkie, hk 1,30" + +msgid "5 - one contact wire, light arms, hk 1.70" +msgstr "5 - sieć jednodrutowa, ramiona lekkie, hk 1,70" + +msgid "Catenary" +msgstr "Sieć trakcyjna" + +msgid "Poles with cantilevers by the track, the card chosen from the catalogue by the distance of the pole; the spans checked against the catalogue" +msgstr "Słupy z wysięgnikami przy torze, karta z katalogu dobierana z odległości słupa; przęsła sprawdzane z tablicami katalogu" + +msgid "Catenary: the selected pole follows the settings below" +msgstr "Sieć trakcyjna: zaznaczony słup idzie za ustawieniami poniżej" + +msgid "Catenary: LMB by the track places a pole on that side, LMB on a pole selects it" +msgstr "Sieć trakcyjna: LPM przy torze stawia słup po tej stronie, LPM na słupie go zaznacza" + +msgid "places a pole on that side, with the card for its distance" +msgstr "stawia słup po tej stronie, z kartą dobraną do odległości" + +msgid "LMB on a pole" +msgstr "LPM na słupie" + +msgid "selects it: the settings then change it at once" +msgstr "zaznacza go: ustawienia od razu go zmieniają" + +msgid "removes the selected pole" +msgstr "usuwa zaznaczony słup" + +msgid "Click: select the pole" +msgstr "Klik: zaznacz słup" + +msgid "The catenary couldn't be saved: %s" +msgstr "Nie udało się zapisać sieci: %s" + +msgid "with the span from the last pole, %.1f m" +msgstr "z przęsłem od ostatniego słupa, %.1f m" + +msgid "Settings copied to %zu poles" +msgstr "Ustawienia skopiowane na słupy: %zu" + +msgid "Settings copied to %zu poles, %zu couldn't take them (no card for their A or not by a track)" +msgstr "Ustawienia skopiowane na słupy: %zu, nie dało się na: %zu (brak karty dla ich A albo nie stoją przy torze)" + +msgid "New chains take these settings; LMB on a span selects its chain" +msgstr "Nowe łańcuchy dostają te ustawienia; LPM na przęśle zaznacza jego łańcuch" + +msgid "Chain %s: %zu poles, %zu spans; changes apply to all of them" +msgstr "Łańcuch %s: słupy %zu, przęsła %zu; zmiany od razu na całym łańcuchu" + +msgid "Network type" +msgstr "Typ sieci" + +msgid "Material" +msgstr "Materiał" + +msgid "Wires" +msgstr "Druty" + +msgid "Power source" +msgstr "Zasilacz" + +msgid "Voltage (V)" +msgstr "Napięcie (V)" + +msgid "Continue the chain from its end" +msgstr "Ciągnij łańcuch od końca" + +msgid "from its start" +msgstr "od początku" + +msgid "Chain: a span from the last pole" +msgstr "Łańcuch: przęsło od ostatniego słupa" + +msgid "Each pole placed is joined with a span to the one placed before it, one chain from anchor to anchor" +msgstr "Każdy stawiany słup łączy się przęsłem z poprzednim, jeden łańcuch od kotwy do kotwy" + +msgid "End the chain" +msgstr "Zakończ łańcuch" + +msgid "The next pole of the chain gets the wires the other way than the last one; after three the same way in a row it keeps that way, until one goes the other way" +msgstr "Następny słup łańcucha dostaje odsuw w drugą stronę niż ostatni; po trzech z rzędu w tę samą stronę zostaje tak, aż któryś pójdzie w drugą" + +msgid "Copy to:" +msgstr "Kopiuj na:" + +msgid "previous" +msgstr "poprzedni" + +msgid "next" +msgstr "następny" + +msgid "both" +msgstr "oba" + +msgid "the whole chain" +msgstr "cały łańcuch" + +msgid "The pole, its face from the track, execution, height of the wires and insulators; each pole keeps its stagger" +msgstr "Słup, czoło od toru, wykonanie, wysokość drutów i izolatory; każdy słup zachowuje swój odsuw" + +msgid "Click: select the chain of this span" +msgstr "Klik: zaznacz łańcuch tego przęsła" + +msgid "new" +msgstr "nowe" + +msgid "worn" +msgstr "przejściowe" + +msgid "tarnished" +msgstr "zaśniedziałe" + +msgid "No drops in the catalogue for this beam" +msgstr "Brak w katalogu zwisów do tego dźwigara" + +msgid "The track runs along the beam of the gantry" +msgstr "Tor biegnie wzdłuż dźwigara bramki" + +msgid "The drop would be off the beam of the gantry" +msgstr "Zwis wypadłby poza dźwigarem bramki" + +msgid "The cantilevers now go on drops on this gantry: LMB over a track" +msgstr "Wysięgniki idą teraz na zwisy tej bramki: LPM nad torem" + +msgid "Gantry %s with beam %s placed; LMB over a track puts a drop with a cantilever on it" +msgstr "Postawiona bramka %s z dźwigarem %s; LPM nad torem wiesza na niej zwis z wysięgnikiem" + +msgid "First mast" +msgstr "Pierwszy słup" + +msgid "Second mast" +msgstr "Drugi słup" + +msgid "Beam:" +msgstr "Dźwigar:" + +msgid "lower mount" +msgstr "dolne mocowanie" + +msgid "upper mount" +msgstr "górne mocowanie" + +msgid "Beam on side +" +msgstr "Dźwigar po stronie +" + +msgid "Beam on side -" +msgstr "Dźwigar po stronie -" + +msgid "On which side of the masts, along the track, the beam is fixed" +msgstr "Po której stronie słupów (wzdłuż toru) mocowany jest dźwigar" + +msgid "Cancel the gantry" +msgstr "Anuluj bramkę" + +msgid "Place a gantry" +msgstr "Postaw bramkę" + +msgid "LMB the first mast, LMB the second: the beam is the one of the catalogue nearest to that distance" +msgstr "LPM pierwszy słup, LPM drugi: dźwigar z katalogu najbliższy tej odległości" + +msgid "Gantry %s, beam %s: the cantilevers go on drops on it" +msgstr "Bramka %s, dźwigar %s: wysięgniki idą na zwisy na niej" + +msgid "Remove the gantry" +msgstr "Usuń bramkę" + +msgid "Back to the poles" +msgstr "Wróć do słupów" + +msgid "Click: the second mast, as far as the nearest beam spans" +msgstr "Klik: drugi słup, w odległości najbliższego dźwigara" + +msgid "Click: the first mast of the gantry" +msgstr "Klik: pierwszy słup bramki" + +msgid "Click: put the cantilevers on drops on this gantry" +msgstr "Klik: wysięgniki na zwisach tej bramki" + +msgid "The cantilevers now go on drops on this gantry: LMB over a track; drag the beam to move the gantry" +msgstr "Wysięgniki idą teraz na zwisy tej bramki: LPM nad torem; przeciągnij dźwigar, żeby przesunąć bramkę" + +msgid "Square to the track" +msgstr "Prostopadle do toru" + +msgid "The beam goes square to the track the first mast is put by" +msgstr "Dźwigar prostopadle do toru, przy którym stoi pierwszy słup" + +msgid "Texture" +msgstr "Tekstura" + +msgid "Replaceable texture of the masts, the beam and the drops of the new gantry (the models have none of their own)" +msgstr "Tekstura wymienna słupów, dźwigara i zwisów nowej bramki (modele nie mają własnej)" + +msgid "Mast %d at the height (m)" +msgstr "Słup %d na wysokości (m)" + +msgid "The zero of the mast is at the rail heads (the clearance of the beam is counted from it); the ground is taken by the foundation head of the variant -1...-4. Drag or Ctrl+click to type" +msgstr "Zero słupa jest w główce szyny (od niej liczony jest prześwit dźwigara); teren bierze głowica fundamentu wykonania -1…-4. Przeciągnij albo Ctrl+klik, żeby wpisać" + +msgid "Give it the texture" +msgstr "Nadaj teksturę" + +msgid "Drag: move the gantry" +msgstr "Przeciągnij: przesuń bramkę" + +msgid "Click: put the cantilevers on drops on this gantry, drag: move it" +msgstr "Klik: wysięgniki na zwisach tej bramki, przeciągnij: przesuń ją" + +msgid "Beam" +msgstr "Dźwigar" + +msgid "Take the masts" +msgstr "Weź słupy" + +msgid "The masts chosen above go to this gantry; the first mast stays, the second goes as far as the beam needs" +msgstr "Słupy wybrane wyżej idą do tej bramki; pierwszy zostaje, drugi odsuwa się tyle, ile trzeba dźwigarowi" + +msgid "Turn (degrees)" +msgstr "Obrót (stopnie)" + +msgid "Drag sideways: turns the gantry around its middle" +msgstr "Przeciągnij w bok: obraca bramkę wokół jej środka" + +msgid "No track near the gantry" +msgstr "Brak toru przy bramce" + +msgid "Gantry %s: beam %s, masts %.3f m apart" +msgstr "Bramka %s: dźwigar %s, słupy %.3f m od siebie" + +msgid "Takes back the last change of the catenary (also Ctrl+Z while this tool is open)" +msgstr "Cofa ostatnią zmianę sieci (także Ctrl+Z przy otwartym tym narzędziu)" + +msgid "the shortest which fits" +msgstr "najkrótszy, który pasuje" + +msgid "Drop" +msgstr "Zwis" + +msgid "Drop of the pole" +msgstr "Zwis wysięgnika" + +msgid "Gantry at the height (m)" +msgstr "Bramka na wysokości (m)" + +msgid "On drops" +msgstr "Na zwisach" + +msgid "On the masts" +msgstr "Na słupach bramki" + +msgid "LMB over a track hangs the cantilever on a drop moved along the beam to the track" +msgstr "LPM nad torem wiesza wysięgnik na zwisie przesuniętym po dźwigarze nad tor" + +msgid "LMB by a track puts the cantilever straight on the mast of the gantry on that side; the card from the distance of the mast" +msgstr "LPM przy torze stawia wysięgnik wprost na słupie bramki po tej stronie; karta z odległości słupa" + +msgid "No mast of this gantry on that side of the track" +msgstr "Po tej stronie toru nie ma słupa tej bramki" + +msgid "Mast %s has no mounts for a cantilever (strut/stay in its entry in traction_supports.txt)" +msgstr "Słup %s nie ma mocowań wysięgnika (strut/stay w jego wpisie w traction_supports.txt)" + +msgid "Click: pole %s, card %04d execution %d, A = %.2f m (%s, wires %s)" +msgstr "Klik: słup %s, karta %04d wykonanie %d, A = %.2f m (%s, odsuw %s)" + +msgid "No card of the group for A = %.2f m" +msgstr "W tej grupie nie ma karty dla A = %.2f m" + +msgid "execution %d has no card for this A, execution %d taken" +msgstr "wykonanie %d nie ma karty dla tego A, wzięte wykonanie %d" + +msgid "Kind" +msgstr "Rodzaj" + +msgid "Family" +msgstr "Rodzina" + +msgid "Card" +msgstr "Karta" + +msgid "by the track and the stagger" +msgstr "z toru i odsuwu" + +msgid "by the distance A" +msgstr "z odległości A" + +msgid "Automatic: the family for a straight or a curve and for the wires towards or away from the pole, as the place by the track has it" +msgstr "Automatycznie: rodzina na prostą albo łuk i do odsuwu do słupa albo od słupa, tak jak wypada w tym miejscu toru" + +msgid "execution %d" +msgstr "wykonanie %d" + +msgid "Execution: from the distance A (the band of the card)" +msgstr "Wykonanie: z odległości A (pasmo karty)" + +msgid ", on piles" +msgstr ", na palach" + +msgid ", on a cast foundation" +msgstr ", na fundamencie wylewanym" + +msgid ", on the line" +msgstr ", na szlaku" + +msgid ", in a station" +msgstr ", na stacji" + +msgid ", on a platform" +msgstr ", na peronie" + +msgid "Other (not described in traction_catalog.txt)" +msgstr "Inne (bez opisu w traction_catalog.txt)" + +msgid "Next span from mast %d" +msgstr "Następne przęsło od słupa %d" + +msgid "LMB: the next mast of the gantry, as far as the nearest beam spans (the mast type: the second one above)" +msgstr "LPM: następny słup bramki, tak daleko, jak sięga najbliższy dźwigar (typ słupa: drugi z wybranych wyżej)" + +msgid "An end mast: a beam can go on one side of it only; a through mast is needed" +msgstr "Słup końcowy: dźwigar może być tylko z jednej strony; potrzebny słup przelotowy" + +msgid "This mast has beams on both sides already" +msgstr "Ten słup ma już dźwigary z obu stron" + +msgid "The beam of the next span on the other side of this through mast, in line with this span" +msgstr "Dźwigar następnego przęsła po drugiej stronie tego słupa przelotowego, w linii z tym przęsłem" + +msgid "(through)" +msgstr " (przelotowy)" + +msgid " (end)" +msgstr " (końcowy)" + +msgid "Place a single beam element" +msgstr "Postaw pojedynczy element dźwigara" + +msgid "LMB the through mast (the first mast above), LMB where the element points: the element of the catalogue nearest to that length" +msgstr "LPM: słup przelotowy (pierwszy słup wybrany wyżej), LPM: dokąd sięga element - element z katalogu najbliższy tej długości" + +msgid "A single beam element (3017-3025, 1910) on one through mast: on one side fixed to the mast, on the other end to a console" +msgstr "Pojedynczy element dźwigara (3017-3025, 1910) na jednym słupie przelotowym: z jednej strony mocowany do słupa, z drugiej do konsoli" + +msgid "Single element from mast %d" +msgstr "Pojedynczy element od słupa %d" + +msgid "LMB: as far as the single element should reach, in line with this span" +msgstr "LPM: dokąd ma sięgać pojedynczy element, w linii z tym przęsłem" + +msgid "A single beam element (3017-3025, 1910) on the other side of this through mast, in line with this span" +msgstr "Pojedynczy element dźwigara (3017-3025, 1910) po drugiej stronie tego słupa przelotowego, w linii z tym przęsłem" + +msgid "Own system height" +msgstr "Własna wysokość konstrukcyjna" + +msgid "Off: the system height of the card (1.70 / 1.30 ...). On: the messenger at the height set here over the contact wires; the stay mount goes with it, the solver warns when the cantilever can't take it" +msgstr "Wyłączone: wysokość konstrukcyjna z karty (1,70 / 1,30 ...). Włączone: lina nośna na podanej wysokości nad przewodami jezdnymi; mocowanie odciągu idzie za nią, solver ostrzega, gdy wysięgnik się nie złoży" + +msgid "hk (m)" +msgstr "hk (m)" + +msgid "Beam of this gantry:" +msgstr "Dźwigar tej bramki:" + +msgid "Gantry %s: the beam on the upper mount" +msgstr "Bramka %s: dźwigar na górnym mocowaniu" + +msgid "Gantry %s: the beam on the lower mount" +msgstr "Bramka %s: dźwigar na dolnym mocowaniu" + +msgid "Stop hanging the wires" +msgstr "Zakończ rozwieszanie" + +msgid "Hang the wires to another pole" +msgstr "Rozwieś druty do innego słupa" + +msgid "LMB the next pole: the span from this one is hung to it; then the next one, and so on" +msgstr "LPM na następny słup: rozwiesza się do niego przęsło od tego; potem kolejny i tak dalej" + +msgid "Joins this pole with the poles clicked after it: it goes on the chain at its end, a new chain of the two, or two chains become one" +msgstr "Łączy ten słup z klikniętymi po nim: dołącza do końca łańcucha, tworzy nowy łańcuch z dwóch słupów albo skleja dwa łańcuchy w jeden" + +msgid "The poles are in different files: a chain can't join them" +msgstr "Słupy są w różnych plikach: łańcuch ich nie połączy" + +msgid "Both poles are in this chain already" +msgstr "Oba słupy są już w tym łańcuchu" + +msgid "The pole is in the middle of a chain: the spans go on from the ends of a chain only" +msgstr "Słup jest w środku łańcucha: przęsła dokłada się tylko od końców łańcucha" + +msgid "Span %s - %s hung; LMB the next pole, or stop with the button" +msgstr "Przęsło %s - %s rozwieszone; LPM na następny słup albo zakończ przyciskiem" + +msgid "Click: hang the span to this pole" +msgstr "Klik: rozwieś przęsło do tego słupa" + +msgid " (second mast %.2f m from the track axis)" +msgstr " (drugi słup %.2f m od osi toru)" + +msgid "Cantilevers" +msgstr "Wysięgniki" + +msgid "Face from the track axis (m)##placing" +msgstr "Lico od osi toru (m)##placing" + +msgid "First mast from the track" +msgstr "Pierwszy słup od toru" + +msgid "From the axis of the nearest track to the face of the mast; drag or Ctrl+click and type: the whole gantry moves square to that track" +msgstr "Od osi najbliższego toru do lica słupa; przeciągnij albo Ctrl+klik i wpisz: cała bramka przesuwa się prostopadle do tego toru" + +msgid "Gantries" +msgstr "Bramki" + +msgid "Gantries: LMB by a track puts a cantilever on the selected gantry, drag the beam to move it" +msgstr "Bramki: LPM przy torze wstawia wysięgnik na zaznaczonej bramce, przeciągnij dźwigar, żeby ją przesunąć" + +msgid "Gantries: LMB on a gantry selects it, or place a new one below" +msgstr "Bramki: LPM na bramce ją zaznacza, albo postaw nową niżej" + +msgid "Gantries: LMB the masts" +msgstr "Bramki: LPM – słupy" + +msgid "Gantries: masts and beams, the cantilevers on their drops and masts" +msgstr "Bramki: słupy i dźwigary, wysięgniki na zwisach i na słupach" + +msgid "Gantry %s: mast %d %.2f m from the track axis" +msgstr "Bramka %s: słup %d %.2f m od osi toru" + +msgid "LMB on a pole selects it: the wires can then be hung from it to the next poles; LMB on a span selects its chain" +msgstr "LPM na słupie go zaznacza: potem można od niego rozwiesić druty do kolejnych słupów; LPM na przęśle zaznacza jego łańcuch" + +msgid "Mast %d: face from the track axis (m)" +msgstr "Słup %d: lico od osi toru (m)" + +msgid "Pole %s" +msgstr "Słup %s" + +msgid "Poles" +msgstr "Słupy" + +msgid "Poles with cantilevers: LMB by a track places one, LMB on a pole selects it" +msgstr "Słupy z wysięgnikami: LPM przy torze stawia słup, LPM na słupie go zaznacza" + +msgid "The chains: the type of the network, the wires, the spans between the poles" +msgstr "Łańcuchy: typ sieci, druty, przęsła między słupami" + +msgid "The first mast goes with its face at this distance from the axis of the track under the cursor, on the side of the cursor; the second one as far as the beam spans" +msgstr "Pierwszy słup staje licem w tej odległości od osi toru pod kursorem, po stronie kursora; drugi tak daleko, jak sięga dźwigar" + +msgid "The overhead catenary: poles with cantilevers, gantries, the wires of the chains" +msgstr "Sieć trakcyjna: słupy z wysięgnikami, bramki, druty łańcuchów" + +msgid "Wires: LMB on a span selects its chain, LMB on a pole selects it" +msgstr "Druty: LPM na przęśle zaznacza łańcuch, LPM na słupie zaznacza słup" + +msgid "Wires: LMB on the next pole hangs the span to it" +msgstr "Druty: LPM na następnym słupie rozwiesza do niego przęsło" + +msgid "From the axis of the track to the face of the first mast. Placing: the first mast goes there. A gantry selected: drag or Ctrl+click and type, the gantry moves square to the track; dragged with the mouse it goes only along the track, at this distance" +msgstr "Od osi toru do lica pierwszego słupa. Przy wstawianiu: tam staje pierwszy słup. Przy zaznaczonej bramce: przeciągnij albo Ctrl+klik i wpisz – bramka przesuwa się prostopadle do toru; przeciągana myszą jedzie tylko wzdłuż toru, w tej odległości" + +msgid "Drag sideways: turns the gantry around its first mast" +msgstr "Przeciągnij w bok: obraca bramkę wokół pierwszego słupa" + +msgid "ERROR: pole %s doesn't stand by a switch, take a steel one" +msgstr "BŁĄD: słup %s nie może stać przy rozjeździe, daj stalowy" + +msgid "%s by a switch" +msgstr "%s przy rozjeździe!" + +msgid "Anchors (weight tensioning, card 23-0461)" +msgstr "Kotwienia (ciężarowe, karta 23-0461)" + +msgid "Anchor the start of the chain" +msgstr "Zakotwij początek łańcucha" + +msgid "Anchor the end of the chain" +msgstr "Zakotwij koniec łańcucha" + +msgid "Anchored: the last span goes without droppers to the tensioning device" +msgstr "Zakotwiono: ostatnie przęsło bez wieszaków do urządzenia naprężającego" + +msgid "An anchor pole 24 m past the last pole, in line with it, with the weight tensioning device; the last span hangs without droppers" +msgstr "Słup kotwowy 24 m za ostatnim słupem, w jego linii, z urządzeniem naprężającym; ostatnie przęsło wisi bez wieszaków" + +msgid "Start: anchor %s" +msgstr "Początek: kotwienie %s" + +msgid "End: anchor %s" +msgstr "Koniec: kotwienie %s" + +msgid "Anchor pole from the last pole (m)" +msgstr "Słup kotwowy od ostatniego słupa (m)" + +msgid "Anchor pole" +msgstr "Słup kotwowy" + +msgid "From the middle anchor (m)" +msgstr "Od kotwienia środkowego (m)" + +msgid "Half-section length (from the middle or fixed anchor): the weights and the pulley move with the air temperature this much more" +msgstr "Długość półodcinka (od kotwienia środkowego albo stałego): o tyle więcej ciężary i rolka chodzą z temperaturą powietrza" + +msgid "Weights" +msgstr "Ciężary" + +msgid "Top weight" +msgstr "Najwyższy ciężar" + +msgid "Weights missing" +msgstr "Brakujące ciężary" + +msgid "Stolen from the top of the stack: about 100 daN less tension each" +msgstr "Zdjęte z góry stosu: każdy to ok. 100 daN naciągu mniej" + +msgid "Wires stretched since regulated (cm)" +msgstr "Wydłużenie sieci od regulacji (cm)" + +msgid "Creep and wear of an old network: the weights hang 4 times this much lower; on the ground the network loses its tension" +msgstr "Pełzanie i zużycie starej sieci: ciężary wiszą 4 razy tyle niżej; oparte o grunt - sieć traci naciąg" + +msgid "%.1f deg C: pulley %.2f m from the pole, weights %.2f m over the rails; %d weights, messenger %.0f daN, contact wires %.0f daN" +msgstr "%.1f °C: rolka %.2f m od słupa, ciężary %.2f m nad szynami; ciężarów %d, lina %.0f daN, przewody %.0f daN" + +msgid "Remove the anchor" +msgstr "Usuń kotwienie" + +msgid "yellow" +msgstr "żółte" + +msgid "grey" +msgstr "szare" + +msgid "yellow, brushed" +msgstr "żółte, pędzlowane" + +msgid "grey, shabby" +msgstr "szare, brzydkie" diff --git a/model/Model3d.cpp b/model/Model3d.cpp index 66fc08c9..b96b8c58 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -1686,6 +1686,78 @@ glm::vec3 TSubModel::offset(float const Geometrytestoffsetthreshold) const return offset; } +namespace +{ +// Fazy szczegółowości nazwane "_LOD" (części sieci trakcyjnej) dostają zakresy widoczności tutaj, +// żeby nie trzeba było wpisywać ich w modelach: kolejne fazy zmieniają się na 20, 40, 100, 300 i 400 m, +// a ostatnia faza, jaką część ma, zostaje widoczna do 400 m (drobnica: model mieszczący się w promieniu 0,5 m +// od swojego zera) albo do 500 m (części większe, a także model, którego rozmiar nie jest jeszcze znany). +// Ruszane są tylko submodele głównego poziomu, którym model zostawił zakres domyślny (MaxDistance -1, MinDistance 0). +// Wołane po Init(), żeby promień modelu był już policzony, a zakresy nie trafiały do zapisywanego pliku e3d. +// Dotyczy wyłącznie modeli sieci trakcyjnej (katalog siec_trakcyjna); w innych modelach dopisek _LOD niczego nie zmienia. +void assign_lod_ranges(TSubModel *Root, std::string const &Filename) +{ + if (Root == nullptr) + return; + { + std::string path{Filename}; + for (auto &character : path) + { + if (character == '\\') + character = '/'; + else if (character >= 'A' && character <= 'Z') + character = static_cast(character - 'A' + 'a'); + } + if (path.find("siec_trakcyjna/") == std::string::npos) + return; + } + static float const limits[]{0.f, 20.f, 40.f, 100.f, 300.f, 400.f}; + int const limitcount{6}; + auto const radius{Root->m_boundingradius}; + float const vanish{(radius > 0.f && radius < 0.5f) ? 400.f : 500.f}; + float const farthest{15000.f}; // zakres nadawany przy wczytaniu, gdy model nie podał własnego + struct lod_phase + { + TSubModel *submodel; + std::string base; + int index; + }; + std::vector phases; + for (auto *submodel = Root; submodel != nullptr; submodel = submodel->NextGet()) + { + auto const &name{submodel->pName}; + if (name.size() < 6) + continue; + auto const mark{name.size() - 5}; + auto const digit{name[mark + 4]}; + if (name[mark] != '_' || (name[mark + 1] != 'l' && name[mark + 1] != 'L') || (name[mark + 2] != 'o' && name[mark + 2] != 'O') || + (name[mark + 3] != 'd' && name[mark + 3] != 'D') || digit < '0' || digit > '9') + continue; + if (submodel->fSquareMinDist != 0.f || submodel->fSquareMaxDist < farthest * farthest * 0.99f) + continue; + phases.push_back({submodel, name.substr(0, mark), digit - '0'}); + } + for (auto const &phase : phases) + { + auto last{true}; + for (auto const &other : phases) + { + if (other.base == phase.base && other.index > phase.index) + { + last = false; + break; + } + } + auto const nearlimit{limits[std::min(phase.index, limitcount - 1)]}; + auto farlimit{phase.index + 1 < limitcount ? limits[phase.index + 1] : farthest}; + if (last) + farlimit = vanish; + phase.submodel->fSquareMinDist = nearlimit * nearlimit; + phase.submodel->fSquareMaxDist = farlimit * farlimit; + } +} +} // namespace + bool TModel3d::LoadFromFile(std::string const &FileName, bool dynamic) { // wczytanie modelu z pliku @@ -1711,18 +1783,21 @@ bool TModel3d::LoadFromFile(std::string const &FileName, bool dynamic) LoadFromTextFile(name + ".t3d", dynamic); if (!dynamic) Init(); + assign_lod_ranges(Root, name); } else if (FileExists(asBinary)) { LoadFromBinFile(asBinary, dynamic); asBinary = ""; Init(); + assign_lod_ranges(Root, name); } else if (FileExists(name + ".t3d")) { LoadFromTextFile(name + ".t3d", dynamic); if (!dynamic) Init(); + assign_lod_ranges(Root, name); } bool const result = Root ? iSubModelsCount > 0 : false; // brak pliku albo problem z wczytaniem diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index d20454c3..f4dd0212 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulationtime.h" #include "application/application.h" #include "model/AnimModel.h" +#include "world/catenary_span.h" #include "rendering/opengl33geometrybank.h" #include "rendering/screenshot.h" #include "rendering/editoroverlay.h" @@ -4823,6 +4824,72 @@ void opengl33_renderer::Render_Alpha(TTraction *Traction) draw(Traction->m_geometry); + // z bliska dodatkowo bryły przęsła: przewody i liny w prawdziwej grubości oraz osprzęt, cieniowane jak metal + if (auto const solid{catenary_span_solid(*Traction, Global.pCamera.Pos)}; solid.wires != null_handle) + { + // wygląd metalu przewodów i osprzętu wg materiału i stanu z wpisu przęsła + auto const &wiremetal{catenary_span_metal(*Traction, false)}; + auto const &fittingmetal{catenary_span_metal(*Traction, true)}; + + auto lit{false}; + if (m_renderpass.draw_mode != rendermode::shadows) + { + // shader przewodów; gdy brak go w katalogu shaderów, standardowy shader kolorowy, a w ostateczności kolor linii + static std::shared_ptr wireshader; + static bool wireshadertried{false}; + if (false == wireshadertried) + { + wireshadertried = true; + for (auto const *name : {"wire", "colored"}) + { + try + { + wireshader = Fetch_Shader(name); + break; + } + catch (...) + { + ErrorLog("Catenary: shader \"mat_" + std::string(name) + ".frag\" can't be loaded"); + } + } + } + if (wireshader) + { + wireshader->bind(); + model_ubs.emission = 0.0f; + model_ubs.alpha_mult = 1.0f; + lit = true; + } + } + auto const set_metal = [&](catenary_metal const &Metal) { + if (lit) + { + glm::vec3 const color{Metal.color[0], Metal.color[1], Metal.color[2]}; + model_ubs.param[0] = glm::vec4(color, 1.0f); + model_ubs.param[1] = glm::vec4(std::max(color.r, std::max(color.g, color.b)), Metal.metalness, Metal.reflection, Metal.gloss); + } + else + { + model_ubs.param[0] = glm::vec4(Traction->wire_color(), 1.0f); + } + }; + // bryły są nieprzezroczyste, więc muszą zapisywać głębię, inaczej zasłaniają się nawzajem wg kolejności rysowania, + // a nie odległości; przęsła rysowane są w przebiegu elementów przezroczystych, gdzie zapis głębi jest wyłączony + GLboolean depthwrite{GL_TRUE}; + ::glGetBooleanv(GL_DEPTH_WRITEMASK, &depthwrite); + ::glDepthMask(GL_TRUE); + set_metal(wiremetal); + draw(solid.wires); + ++m_renderpass.draw_stats.drawcalls; + if (solid.fittings != null_handle) + { + set_metal(fittingmetal); + draw(solid.fittings); + ++m_renderpass.draw_stats.drawcalls; + } + ::glDepthMask(depthwrite); + } + // debug data ++m_renderpass.draw_stats.traction; ++m_renderpass.draw_stats.drawcalls; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 59152c64..a4bd8f61 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "vehicle/DynObj.h" #include "model/AnimModel.h" #include "world/Traction.h" +#include "world/catenary_span.h" #include "application/application.h" #include "utilities/Logs.h" #include "rendering/openglgeometrybank.h" @@ -3659,6 +3660,21 @@ opengl_renderer::Render_Alpha( TTraction *Traction ) { linealpha } ) ); // render m_geometry.draw( Traction->m_geometry ); + // z bliska dodatkowo bryły przęsła: przewody i liny w prawdziwej grubości oraz uchwyty + if( auto const solid { catenary_span_solid( *Traction, Global.pCamera.Pos ) }; solid.wires != null_handle ) { + // bryły są nieprzezroczyste, więc muszą zapisywać głębię; w tym przebiegu zapis głębi jest wyłączony + GLboolean depthwrite { GL_TRUE }; + ::glGetBooleanv( GL_DEPTH_WRITEMASK, &depthwrite ); + ::glDepthMask( GL_TRUE ); + ::glColor4fv( glm::value_ptr( glm::vec4{ Traction->wire_color(), 1.f } ) ); + m_geometry.draw( solid.wires ); + ++m_renderpass.draw_stats.drawcalls; + if( solid.fittings != null_handle ) { + m_geometry.draw( solid.fittings ); + ++m_renderpass.draw_stats.drawcalls; + } + ::glDepthMask( depthwrite ); + } // debug data ++m_renderpass.draw_stats.traction; ++m_renderpass.draw_stats.drawcalls; diff --git a/shaders/mat_wire.frag b/shaders/mat_wire.frag new file mode 100644 index 00000000..030fddc6 --- /dev/null +++ b/shaders/mat_wire.frag @@ -0,0 +1,57 @@ +in vec3 f_normal; +in vec2 f_coord; +in vec4 f_pos; + +in vec4 f_clip_pos; +in vec4 f_clip_future_pos; + +#include + +// overhead line wires, ropes and fittings drawn as solids: untextured metal. +// param[0].rgb - metal colour (sRGB), param[0].a - opacity +// param[1].x - colour intensity, param[1].y - metalness, param[1].z - reflection, param[1].w - glossiness +#param (color, 0, 0, 4, diffuse) +#param (diffuse, 1, 0, 1, diffuse) +#param (specular, 1, 1, 1, specular) +#param (reflection, 1, 2, 1, zero) +#param (glossiness, 1, 3, 1, glossiness) + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +#include +#include +#include + +void main() +{ + // no texture: the metal colour alone makes the albedo, apply_lights() applies it to the white base + vec4 tex_color = vec4(1.0, 1.0, 1.0, param[0].a); + + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z; + // bare metal: the colour tints the highlights instead of the diffuse term + metalic = clamp(param[1].y, 0.0, 1.0); + float specularity = dot(pow(param[0].rgb, vec3(2.2)), vec3(0.5)); + glossiness = abs(param[1].w); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * alpha_mult); +#if POSTFX_ENABLED + out_color = color; +#else + out_color = tonemap(color); +#endif +#if MOTIONBLUR_ENABLED + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5;; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5;; + + out_motion = vec4(a - b, 0.0f, 0.0f); + } +#endif +} diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 70f9e307..67f55292 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -32,6 +32,7 @@ http://mozilla.org/MPL/2.0/. #include "application/application.h" #include "utilities/Logs.h" #include "vehicle/Driver.h" +#include "world/catenary_scene.h" namespace simulation { @@ -357,6 +358,7 @@ void state_manager::process_commands() { Global.AirTemperature = commanddata.param1; Global.Overcast = commanddata.param2; simulation::Environment.compute_weather(); + catenary_scene_temperature(); // ciężary kotwień idą za temperaturą } if (commanddata.command == user_command::insertmodel) { diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 1ff771d1..c4428236 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -27,6 +27,7 @@ http://mozilla.org/MPL/2.0/. #include "model/AnimModel.h" #include "rendering/lightarray.h" #include "world/TractionPower.h" +#include "world/catenary_scene.h" #include "world/Road.h" #include "world/Sweep.h" #include "world/RoadPoint.h" @@ -59,6 +60,9 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { EditorTerrain.reset(); scene::terrain_file::references().clear(); + // podwieszenia sieci trakcyjnej poprzedniej scenerii + catenary_scene_clear(); + // TODO: move initialization to separate routine so we can reuse it SafeDelete( Region ); Region = new scene::basic_region(); @@ -166,6 +170,10 @@ state_serializer::deserialize_continue(std::shared_ptr state if( lookup != state->functionmap.end() ) { lookup->second(); } + else if( token == "catenary" ) { + // sieć trakcyjna: plik z podwieszeniami, "catenary endcatenary" + catenary_scene_load( Input ); + } else { ErrorLog( "Bad scenario: unexpected token \"" + token + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); } @@ -447,6 +455,7 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc // crossings and traffic points go by the lanes, which are complete at this point simulation::Roadpoints.InitRoadpoints(); simulation::Traction.InitTraction(); + catenary_scene_resolve(); simulation::Events.InitEvents(); simulation::Events.InitLaunchers(); simulation::Memory.InitCells(); diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index cd789837..2a373114 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "vehicle/Driver.h" #include "utilities/Logs.h" #include "utilities/utilities.h" +#include "world/catenary_span.h" #include "Console.h" #include "scripting/PyInt.h" #include "utilities/Timer.h" @@ -590,6 +591,20 @@ bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string& return true; } + if (token == "catenary.procedural") + { + ParseOne(Parser, CatenaryProcedural); + return true; + } + + if (token == "catenary.solidrange") + { + // zasięg rysowania przewodów sieci trakcyjnej jako brył [m]; 0 wyłącza + ParseOne(Parser, CatenarySettings.solid_range); + CatenarySettings.solid_range = std::clamp(CatenarySettings.solid_range, 0.f, 2000.f); + return true; + } + if (token == "splinefidelity") { float splinefidelity = 0.f; @@ -1624,6 +1639,8 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.smoke", Smoke ); export_as_text( Output, "gfx.smoke.fidelity", SmokeFidelity ); export_as_text( Output, "smoothtraction", bSmoothTraction ); + export_as_text( Output, "catenary.procedural", CatenaryProcedural ); + export_as_text( Output, "catenary.solidrange", CatenarySettings.solid_range ); export_as_text( Output, "splinefidelity", SplineFidelity ); export_as_text( Output, "sleeperdistance", SleeperDistance ); export_as_text( Output, "rendercab", render_cab ); diff --git a/utilities/Globals.h b/utilities/Globals.h index a7eb4d87..5f1dd0a2 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -78,6 +78,7 @@ struct global_settings { // filesystem bool bLoadTraction{ true }; bool bSmoothTraction{ true }; + bool CatenaryProcedural{ true }; // przęsła z dopiskiem "type" rysowane wg typu sieci (lina nośna, wieszaki i Y liczone) bool CreateSwitchTrackbeds{ true }; std::string szTexturesTGA{ ".tga" }; // lista tekstur od TGA std::string szTexturesDDS{ ".dds" }; // lista tekstur od DDS diff --git a/world/Traction.cpp b/world/Traction.cpp index b9e47d5b..0d46f518 100644 --- a/world/Traction.cpp +++ b/world/Traction.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/Traction.h" +#include "world/catenary_span.h" #include "simulation/simulation.h" #include "utilities/Globals.h" @@ -138,18 +139,24 @@ TTraction::Load( cParser *parser, glm::dvec3 const &pOrigin ) { m_visible = parser->getToken() == "vis"; std::string token { parser->getToken() }; - if( token == "parallel" ) { - // jawne wskazanie innego przęsła, na które może przestawić się pantograf - parser->getTokens(); - *parser >> asParallel; - } + std::string catenarytype; while( false == token.empty() && token != "endtraction" ) { + if( token == "parallel" ) { + // jawne wskazanie innego przęsła, na które może przestawić się pantograf + parser->getTokens(); + *parser >> asParallel; + } + else if( token == "type" ) { + // symbol typu sieci, np. YwsC120-2C; starsze wersje symulatora pomijają ten dopisek + catenarytype = parser->getToken( false ); + } token = parser->getToken(); } Init(); // przeliczenie parametrów + catenary_span_assign( *this, catenarytype ); // calculate traction location location( glm::mix( pPoint2, pPoint1, 0.5 ) ); @@ -170,6 +177,14 @@ TTraction::create_geometry( gfx::geometrybank_handle const &Geometrybank ) { gfx::vertex_array vertices; + if( catenary_span_lines( *this, vertices ) ) { + // przęsło z typem sieci: lina nośna, podwieszenie Y i wieszaki policzone, zamiast przybliżenia poniżej + auto const elementcount = vertices.size() / 2; + gfx::userdata_array empty_userdata{}; + m_geometry = GfxRenderer->Insert( vertices, empty_userdata, Geometrybank, GL_LINES ); + return elementcount; + } + double ddp = std::hypot( pPoint2.x - pPoint1.x, pPoint2.z - pPoint1.z ); if( Wires == 2 ) WireOffset = 0; @@ -566,6 +581,10 @@ TTraction::wire_color() const { break; } default: {break; } + } + if( TestFlag( DamageFlag, 2 ) && false == TestFlag( DamageFlag, 1 ) ) { + // stan przejściowy między nową a zaśniedziałą: ciemniejsza + color *= 0.75f; } color *= 0.2; // w zaleźności od koloru swiatła @@ -674,6 +693,9 @@ TTraction::export_as_text_( std::ostream &Output ) const { if( false == asParallel.empty() ) { Output << "parallel " << asParallel << ' '; } + if( auto const catenarytype { catenary_span_symbol( *this ) }; false == catenarytype.empty() ) { + Output << "type " << catenarytype << ' '; + } // footer Output << "endtraction" diff --git a/world/Traction.h b/world/Traction.h index 6506dfb3..41b50fee 100644 --- a/world/Traction.h +++ b/world/Traction.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include "scene/scenenode.h" #include "world/Segment.h" @@ -17,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Names.h" class TTractionPowerSource; +struct catenary_span; class TTraction : public scene::basic_node { @@ -54,6 +56,8 @@ class TTraction : public scene::basic_node // visualization data glm::dvec3 m_origin; gfx::geometry_handle m_geometry; + // stan przęsła rysowanego wg typu sieci (dopisek "type"); pusty = przęsło rysowane po staremu. szczegóły w world/catenary_span.h + std::shared_ptr m_catenaryspan; explicit TTraction( scene::node_data const &Nodedata ); diff --git a/world/catenary_cantilever.cpp b/world/catenary_cantilever.cpp new file mode 100644 index 00000000..b08f135a --- /dev/null +++ b/world/catenary_cantilever.cpp @@ -0,0 +1,2276 @@ +#include "world/catenary_cantilever.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace catenary { + +namespace { + +bool to_number( std::string const &Text, double &Value ) { + std::istringstream stream { Text }; + stream.imbue( std::locale::classic() ); + stream >> Value; + return false == stream.fail() && stream.eof(); +} + +std::string to_text( double const Value, int const Precision = 3 ) { + std::ostringstream stream; + stream.imbue( std::locale::classic() ); + stream.setf( std::ios::fixed ); + stream.precision( Precision ); + stream << Value; + return stream.str(); +} + +// wektor w płaszczyźnie wysięgnika +struct vec2 { + double y { 0.0 }; + double z { 0.0 }; +}; +vec2 operator+( vec2 const &Left, vec2 const &Right ) { return { Left.y + Right.y, Left.z + Right.z }; } +vec2 operator-( vec2 const &Left, vec2 const &Right ) { return { Left.y - Right.y, Left.z - Right.z }; } +vec2 operator*( vec2 const &Vector, double const Factor ) { return { Vector.y * Factor, Vector.z * Factor }; } +double dot( vec2 const &Left, vec2 const &Right ) { return Left.y * Right.y + Left.z * Right.z; } +double length( vec2 const &Vector ) { return std::sqrt( dot( Vector, Vector ) ); } +vec2 unit( vec2 const &Vector ) { auto const size { length( Vector ) }; return ( size > 1e-9 ? Vector * ( 1.0 / size ) : vec2 { -1.0, 0.0 } ); } +// prostopadła skierowana w górę +vec2 upward_normal( vec2 const &Direction ) { + vec2 const normal { Direction.z, -Direction.y }; + return ( normal.z >= 0.0 ? normal : normal * -1.0 ); +} +// obrót w płaszczyźnie wysięgnika, wokół osi X +vec2 rotate( vec2 const &Vector, double const Angle ) { + auto const cosine { std::cos( Angle ) }; + auto const sine { std::sin( Angle ) }; + return { Vector.y * cosine - Vector.z * sine, Vector.y * sine + Vector.z * cosine }; +} +vec2 to_vec( cantilever_point const &Point ) { return { Point.y, Point.z }; } + +// kierunki spoczynkowe części: tak część jest ułożona w swoim modelu +vec2 const toward_track { -1.0, 0.0 }; +vec2 const toward_support { 1.0, 0.0 }; +vec2 const downward { 0.0, -1.0 }; +vec2 const upward { 0.0, 1.0 }; + +// część leżąca w płaszczyźnie wysięgnika: zero w podanym punkcie, obrócona wokół osi X (wzdłuż toru) tak, +// żeby kierunek spoczynkowy Rest z modelu pokrył się z kierunkiem Direction +cantilever_placement place( std::string const &Role, std::string const &Part, vec2 const &Origin, vec2 const &Direction, vec2 const &Rest ) { + cantilever_placement placement; + placement.role = Role; + placement.part = Part; + placement.position[ 1 ] = Origin.y; + placement.position[ 2 ] = Origin.z; + auto const angle { std::atan2( Direction.z, Direction.y ) - std::atan2( Rest.z, Rest.y ) }; + auto const cosine { std::cos( angle ) }; + auto const sine { std::sin( angle ) }; + placement.rotation[ 4 ] = cosine; placement.rotation[ 5 ] = -sine; + placement.rotation[ 7 ] = sine; placement.rotation[ 8 ] = cosine; + return placement; +} + +// obrót części o 90 stopni wokół jej własnej osi podłużnej (lokalnej Y), po ustawieniu w płaszczyźnie wysięgnika +void roll_quarter( cantilever_placement &Placement ) { + auto const cosine { Placement.rotation[ 4 ] }; + auto const sine { Placement.rotation[ 7 ] }; + // kolumny: X = -( dawne Z ), Y bez zmian, Z = dawne X + double const rolled[ 9 ] { 0.0, 0.0, 1.0, sine, cosine, 0.0, -cosine, sine, 0.0 }; + std::copy( std::begin( rolled ), std::end( rolled ), std::begin( Placement.rotation ) ); +} + +std::set const block_ends { "endpart", "endregistration", "endscheme", "endrules", "endpole", "endmount", "endassembly" }; +std::set const block_starts { "part", "registration", "scheme", "rules", "models", "pole", "mount", "assembly" }; + +// opis argumentów klocka sposobu składania; nullptr dla nieznanego +std::string const *step_signature( std::string const &Op ); +// sprawdzenie zapisu argumentów klocka +bool valid_step( std::string const &Op, std::vector const &Words, std::string &Reason ); + +} // namespace + +void cantilever_catalog::parse( std::vector const &Tokens, std::string const &Source, std::vector &Errors ) { + + auto const count { Tokens.size() }; + std::size_t index { 0 }; + auto const report = [ & ]( std::string const &Entry, std::string const &Reason ) { + Errors.push_back( "Bad cantilever file \"" + Source + "\"" + ( Entry.empty() ? "" : ", " + Entry ) + ": " + Reason ); + }; + // kolejna liczba wpisu; brak albo zły zapis psują wpis + auto const number = [ & ]( double &Value, bool &Good ) { + if( index >= count || false == to_number( Tokens[ index ], Value ) ) { Good = false; return; } + ++index; + }; + // pominięcie reszty zepsutego wpisu + auto const skip = [ & ]() { + while( index < count && block_ends.find( Tokens[ index ] ) == block_ends.end() && block_starts.find( Tokens[ index ] ) == block_starts.end() ) { ++index; } + if( index < count && block_ends.find( Tokens[ index ] ) != block_ends.end() ) { ++index; } + }; + + while( index < count ) { + auto const token { Tokens[ index++ ] }; + + if( token == "part" ) { + if( index >= count ) { report( "", "\"part\" without a name" ); break; } + cantilever_part part; + part.name = Tokens[ index++ ]; + auto const entry { "part \"" + part.name + "\"" }; + auto good { true }; + auto closed { false }; + while( index < count && good ) { + auto const key { Tokens[ index++ ] }; + if( key == "endpart" ) { closed = true; break; } + if( key == "kind" ) { if( index < count ) { part.kind = Tokens[ index++ ]; } else { good = false; } } + else if( key == "length" ) { number( part.length, good ); } + else if( key == "clearance" ) { number( part.clearance, good ); } + else if( key == "pin" ) { number( part.pin, good ); } + else if( key == "radius" ) { number( part.radius, good ); } + else if( key == "model" ) { if( index < count ) { part.model = Tokens[ index++ ]; } else { good = false; } } + else if( key == "girth" ) { number( part.girth, good ); } + else if( key == "eye" ) { number( part.eye, good ); } + else if( key == "hook" ) { number( part.hook[ 0 ], good ); number( part.hook[ 1 ], good ); } + else if( key == "end" ) { number( part.end[ 0 ], good ); number( part.end[ 1 ], good ); } + else if( key == "point" ) { + if( index < count ) { + auto const name { Tokens[ index++ ] }; + double y { 0.0 }, z { 0.0 }; + number( y, good ); number( z, good ); + part.points[ name ] = { y, z }; + } + else { good = false; } + } + else if( key == "pointx" ) { + // składowa X punktu (wzdłuż toru): pointx + if( index < count ) { + auto const name { Tokens[ index++ ] }; + double x { 0.0 }; + number( x, good ); + part.points_x[ name ] = x; + } + else { good = false; } + } + else if( key == "member" ) { + cantilever_member member; + if( index < count ) { member.part = Tokens[ index++ ]; } else { good = false; } + for( auto &value : member.position ) { number( value, good ); } + number( member.yaw, good ); + if( good ) { part.members.push_back( member ); } + } + else if( key == "holes" ) { + double value { 0.0 }; + while( index < count && to_number( Tokens[ index ], value ) ) { part.holes.push_back( value ); ++index; } + if( part.holes.empty() ) { good = false; } + } + else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } + } + if( false == good || false == closed ) { + if( good ) { report( entry, "missing \"endpart\"" ); } else if( Errors.empty() || Errors.back().find( entry ) == std::string::npos ) { report( entry, "bad or missing value" ); } + skip(); + continue; + } + if( m_parts.find( part.name ) != m_parts.end() ) { report( entry, "defined more than once, the first definition stays" ); continue; } + m_parts.emplace( part.name, part ); + continue; + } + + if( token == "registration" ) { + if( index >= count ) { report( "", "\"registration\" without a name" ); break; } + cantilever_registration registration; + registration.name = Tokens[ index++ ]; + auto const entry { "registration \"" + registration.name + "\"" }; + auto good { true }; + auto closed { false }; + while( index < count && good ) { + auto const key { Tokens[ index++ ] }; + if( key == "endregistration" ) { closed = true; break; } + if( key == "tube" ) { number( registration.tube, good ); } + else if( key == "clamp" ) { number( registration.clamp, good ); } + else if( key == "item" ) { + cantilever_item item; + if( index < count ) { item.part = Tokens[ index++ ]; } else { good = false; } + for( auto &value : item.position ) { number( value, good ); } + for( auto &value : item.rotation ) { number( value, good ); } + if( good ) { registration.items.push_back( item ); } + } + else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } + } + if( false == good || false == closed ) { + if( good ) { report( entry, "missing \"endregistration\"" ); } else if( Errors.empty() || Errors.back().find( entry ) == std::string::npos ) { report( entry, "bad or missing value" ); } + skip(); + continue; + } + if( m_registrations.find( registration.name ) != m_registrations.end() ) { report( entry, "defined more than once, the first definition stays" ); continue; } + m_registrations.emplace( registration.name, registration ); + continue; + } + + if( token == "scheme" ) { + cantilever_scheme scheme; + auto good { true }; + double card { 0.0 }, execution { 0.0 }; + auto const name { ( index < count ? Tokens[ index ] : std::string() ) + ( index + 1 < count ? " " + Tokens[ index + 1 ] : std::string() ) }; + auto const entry { "scheme \"" + name + "\"" }; + number( card, good ); + number( execution, good ); + scheme.card = static_cast( card ); + scheme.execution = static_cast( execution ); + auto closed { false }; + while( index < count && good ) { + auto const key { Tokens[ index++ ] }; + if( key == "endscheme" ) { closed = true; break; } + if( key == "reach" ) { number( scheme.reach_min, good ); number( scheme.reach_max, good ); } + else if( key == "height" ) { number( scheme.height, good ); } + else if( key == "registration" ) { if( index < count ) { scheme.registration = Tokens[ index++ ]; } else { good = false; } } + else if( key == "assembly" ) { if( index < count ) { scheme.assembly = Tokens[ index++ ]; } else { good = false; } } + else if( key == "tube_steps" ) { + double value { 0.0 }; + while( index < count && to_number( Tokens[ index ], value ) ) { scheme.tube_steps.push_back( value ); ++index; } + if( scheme.tube_steps.empty() ) { good = false; } + std::sort( scheme.tube_steps.begin(), scheme.tube_steps.end() ); + } + else if( key == "tube_prefer" ) { number( scheme.tube_prefer, good ); } + else if( key == "mount" ) { + auto const which { index < count ? Tokens[ index++ ] : std::string() }; + if( which == "stay" ) { number( scheme.mount_stay, good ); } + else if( which == "strut" ) { number( scheme.mount_strut, good ); } + else if( which == "steady" ) { number( scheme.mount_steady, good ); scheme.steady_mounted = true; } + else { report( entry, "unknown mount \"" + which + "\"" ); good = false; } + } + else if( key == "role" ) { + if( index + 1 < count ) { scheme.roles[ Tokens[ index ] ] = Tokens[ index + 1 ]; index += 2; } else { good = false; } + } + else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } + } + if( false == good || false == closed ) { + if( good ) { report( entry, "missing \"endscheme\"" ); } else if( Errors.empty() || Errors.back().find( entry ) == std::string::npos ) { report( entry, "bad or missing value" ); } + skip(); + continue; + } + auto const key { std::make_pair( scheme.card, scheme.execution ) }; + if( m_schemes.find( key ) != m_schemes.end() ) { report( entry, "defined more than once, the first definition stays" ); continue; } + m_schemes.emplace( key, scheme ); + continue; + } + + if( token == "pole" || token == "mount" ) { + auto const ispole { token == "pole" }; + if( index >= count ) { report( "", "\"" + token + "\" without a name" ); break; } + cantilever_pole pole; + cantilever_mount mount; + auto const name { Tokens[ index++ ] }; + pole.name = name; + mount.name = name; + auto const entry { token + " \"" + name + "\"" }; + auto const ending { ispole ? std::string( "endpole" ) : std::string( "endmount" ) }; + auto good { true }; + auto closed { false }; + auto const text = [ & ]( std::string &Value ) { if( index < count ) { Value = Tokens[ index++ ]; } else { good = false; } }; + while( index < count && good ) { + auto const key { Tokens[ index++ ] }; + if( key == ending ) { closed = true; break; } + if( ispole ) { + if( key == "model" ) { text( pole.model ); std::replace( pole.model.begin(), pole.model.end(), '\\', '/' ); } + else if( key == "foundation" ) { text( pole.foundation ); std::replace( pole.foundation.begin(), pole.foundation.end(), '\\', '/' ); } + else if( key == "top" ) { number( pole.top, good ); } + else if( key == "length" ) { number( pole.length, good ); } + else if( key == "depth" ) { number( pole.depth_top, good ); number( pole.depth_base, good ); } + else if( key == "mount_max" ) { number( pole.mount_max, good ); } + else if( key == "mount_min" ) { number( pole.mount_min, good ); pole.has_mount_min = true; } + else if( key == "turn" ) { number( pole.turn, good ); } + else if( key == "drop" ) { text( pole.drop ); } + else if( key == "strut" ) { text( pole.strut ); } + else if( key == "stay" ) { text( pole.stay ); } + else if( key == "steady" ) { text( pole.steady ); } + else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } + } + else { + if( key == "back" ) { text( mount.back ); } + else if( key == "hinge" ) { text( mount.hinge ); number( mount.hinge_eye[ 0 ], good ); number( mount.hinge_eye[ 1 ], good ); } + else if( key == "rod" ) { text( mount.rod ); } + else if( key == "twin" ) { text( mount.twin ); } + else if( key == "axis" || key == "eye" ) { number( mount.point[ 0 ], good ); number( mount.point[ 1 ], good ); } + else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } + } + } + if( good && closed && ispole && ( pole.length <= 0.0 || pole.depth_top <= 0.0 || pole.depth_base <= 0.0 ) ) { + report( entry, "needs \"length\" and both \"depth\" values above zero" ); continue; + } + if( false == good || false == closed ) { + if( good ) { report( entry, "missing \"" + ending + "\"" ); } else if( Errors.empty() || Errors.back().find( entry ) == std::string::npos ) { report( entry, "bad or missing value" ); } + skip(); + continue; + } + if( ispole ) { + if( m_poles.find( name ) != m_poles.end() ) { report( entry, "defined more than once, the first definition stays" ); continue; } + m_poles.emplace( name, pole ); + } + else { + if( m_mounts.find( name ) != m_mounts.end() ) { report( entry, "defined more than once, the first definition stays" ); continue; } + m_mounts.emplace( name, mount ); + } + continue; + } + + if( token == "mast" || token == "beam" ) { + // konstrukcje bramek: mast model … endmast, beam model … endbeam + auto const ismast { token == "mast" }; + if( index >= count ) { report( "", "\"" + token + "\" without a name" ); break; } + gantry_mast mast; + gantry_beam beam; + auto const name { Tokens[ index++ ] }; + mast.name = beam.name = name; + auto const entry { token + " \"" + name + "\"" }; + auto const ending { ismast ? std::string( "endmast" ) : std::string( "endbeam" ) }; + auto good { true }; + auto closed { false }; + auto const text = [ & ]( std::string &Value ) { if( index < count ) { Value = Tokens[ index++ ]; } else { good = false; } }; + while( index < count && good ) { + auto const key { Tokens[ index++ ] }; + if( key == ending ) { closed = true; break; } + if( key == "model" ) { text( ismast ? mast.model : beam.model ); auto &model { ismast ? mast.model : beam.model }; std::replace( model.begin(), model.end(), '\\', '/' ); } + else if( ismast && key == "width" ) { number( mast.width, good ); } + else if( ismast && key == "length" ) { number( mast.length, good ); } + else if( ismast && key == "height" ) { number( mast.height, good ); } + else if( ismast && key == "mount" ) { number( mast.mount_low, good ); number( mast.mount_high, good ); } + else if( ismast && key == "offset" ) { number( mast.offset, good ); } + else if( ismast && key == "through" ) { mast.through = true; } + else if( ismast && key == "end" ) { mast.through = false; } + else if( ismast && key == "strut" ) { text( mast.strut ); } + else if( ismast && key == "stay" ) { text( mast.stay ); } + else if( ismast && key == "steady" ) { text( mast.steady ); } + else if( ismast && key == "below_beam" ) { number( mast.below_beam, good ); } + else if( false == ismast && key == "span" ) { number( beam.span, good ); } + else if( false == ismast && key == "drop" ) { text( beam.drop ); } + else if( false == ismast && key == "single" ) { beam.single = true; } + else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } + } + if( false == good || false == closed ) { + if( good ) { report( entry, "missing \"" + ending + "\"" ); } else { report( entry, "bad or missing value" ); } + skip(); + continue; + } + if( ismast ) { + if( mast.model.empty() || mast.mount_low <= 0.0 ) { report( entry, "needs \"model\" and \"mount\"" ); continue; } + if( false == m_masts.emplace( name, mast ).second ) { report( entry, "defined more than once, the first definition stays" ); continue; } + // słup bramki z mocowaniami wysięgnika to też słup dla wysięgników (pod tą samą nazwą): bez modelu - stawia go + // bramka; głębokość w stronę toru to wymiar wzdłuż dźwigara, mocowania najwyżej pod dźwigarem dolnego poziomu + if( false == mast.strut.empty() && false == mast.stay.empty() ) { + cantilever_pole pole; + pole.name = name; + pole.gantry = true; + pole.top = mast.height; + pole.length = mast.height; + // ściana słupa od strony toru tam, gdzie kończy się dźwigar (gantry_beam_ends: oś + offset) - zgłoszenie 2026-10-22: + // z length (0,60) mocowania stały ~0,18 m przed ścianą słupa + pole.depth_top = pole.depth_base = mast.offset > 0.0 ? 2.0 * mast.offset : mast.length; + pole.mast_low = mast.mount_low; + pole.mast_high = mast.mount_high > 0.0 ? mast.mount_high : mast.mount_low; + pole.below_beam = mast.below_beam; + pole.mount_max = mast.mount_low - mast.below_beam; + pole.strut = mast.strut; + pole.stay = mast.stay; + pole.steady = mast.steady; + if( false == m_poles.emplace( name, pole ).second ) { report( entry, "has the name of a pole, it can't carry cantilevers" ); } + } + } + else { + if( beam.model.empty() || beam.span <= 0.0 ) { report( entry, "needs \"model\" and \"span\"" ); continue; } + if( false == m_beams.emplace( name, beam ).second ) { report( entry, "defined more than once, the first definition stays" ); } + } + continue; + } + + if( token == "assembly" ) { + if( index >= count ) { report( "", "\"assembly\" without a name" ); break; } + cantilever_assembly assembly; + assembly.name = Tokens[ index++ ]; + auto const entry { "assembly \"" + assembly.name + "\"" }; + auto good { true }; + auto closed { false }; + std::vector open; + while( index < count && good ) { + cantilever_step step; + step.op = Tokens[ index++ ]; + if( step.op == "endassembly" ) { closed = true; break; } + // liczba argumentów z opisu klocka; "line" ma po punkcie początkowym jeden z trzech sposobów + std::size_t wanted { 0 }; + if( step.op == "line" ) { + wanted = 3; + if( index + 2 < count ) { + auto const &how { Tokens[ index + 2 ] }; + wanted += ( how == "dir" ? 2 : ( how == "toward" || how == "along" ? 1 : 0 ) ); + } + } + else { + auto const signature { step_signature( step.op ) }; + if( signature == nullptr ) { report( entry, "unknown step \"" + step.op + "\"" ); good = false; break; } + wanted = signature->size(); + } + for( std::size_t word = 0; word < wanted && index < count; ++word ) { step.words.push_back( Tokens[ index++ ] ); } + std::string reason; + if( step.words.size() != wanted || false == valid_step( step.op, step.words, reason ) ) { + report( entry, "step \"" + step.op + "\": " + ( reason.empty() ? std::string( "missing arguments" ) : reason ) ); + good = false; break; + } + if( step.op == "repeat" ) { open.push_back( assembly.steps.size() ); } + if( step.op == "endrepeat" ) { + if( open.empty() ) { report( entry, "\"endrepeat\" without \"repeat\"" ); good = false; break; } + assembly.steps[ open.back() ].end = assembly.steps.size() + 1; + open.pop_back(); + } + assembly.steps.push_back( step ); + } + if( good && closed && false == open.empty() ) { report( entry, "\"repeat\" without \"endrepeat\"" ); continue; } + if( false == good || false == closed ) { + if( good ) { report( entry, "missing \"endassembly\"" ); } + skip(); + continue; + } + if( m_assemblies.find( assembly.name ) != m_assemblies.end() ) { report( entry, "defined more than once, the first definition stays" ); continue; } + m_assemblies.emplace( assembly.name, assembly ); + continue; + } + + if( token == "models" ) { + if( index >= count ) { report( "", "\"models\" without a path" ); break; } + m_models = Tokens[ index++ ]; + std::replace( m_models.begin(), m_models.end(), '\\', '/' ); + if( m_models.back() != '/' ) { m_models += '/'; } + continue; + } + + if( token == "rules" ) { + auto good { true }; + auto closed { false }; + auto changed { rules }; + while( index < count && good ) { + auto const key { Tokens[ index++ ] }; + if( key == "endrules" ) { closed = true; break; } + if( key == "stay_gap" ) { number( changed.stay_gap_min, good ); number( changed.stay_gap, good ); number( changed.stay_gap_max, good ); } + else if( key == "clamp_margin" ) { number( changed.clamp_margin, good ); } + else if( key == "cord_clamp_inset" ) { number( changed.cord_clamp_inset, good ); } + else if( key == "umbrella_spread" ) { number( changed.umbrella_spread, good ); } + else { report( "rules", "unknown keyword \"" + key + "\"" ); good = false; --index; } + } + if( false == good || false == closed ) { + if( good ) { report( "rules", "missing \"endrules\"" ); } else if( Errors.empty() || Errors.back().find( "rules" ) == std::string::npos ) { report( "rules", "bad or missing value" ); } + skip(); + continue; + } + rules = changed; + continue; + } + + report( "", "unexpected token \"" + token + "\"" ); + } + + // spójność: schemat ma wskazywać istniejące części i zestaw ramion + for( auto const &scheme : m_schemes ) { + auto const entry { "scheme \"" + std::to_string( scheme.second.card ) + " " + std::to_string( scheme.second.execution ) + "\"" }; + if( false == scheme.second.registration.empty() && m_registrations.find( scheme.second.registration ) == m_registrations.end() ) { report( entry, "unknown registration \"" + scheme.second.registration + "\"" ); } + if( m_assemblies.find( scheme.second.assembly ) == m_assemblies.end() ) { report( entry, "unknown assembly \"" + scheme.second.assembly + "\"" ); } + for( auto const &role : scheme.second.roles ) { + if( m_parts.find( role.second ) == m_parts.end() ) { report( entry, "role \"" + role.first + "\" uses unknown part \"" + role.second + "\"" ); } + } + } + for( auto const &part : m_parts ) { + for( auto const &member : part.second.members ) { + if( m_parts.find( member.part ) == m_parts.end() ) { report( "part \"" + part.first + "\"", "member uses unknown part \"" + member.part + "\"" ); } + } + } +} + +bool cantilever_catalog::load( std::string const &Filename, std::vector &Errors ) { + + std::ifstream file { Filename }; + if( false == file.is_open() ) { return false; } + std::vector tokens; + std::string line; + while( std::getline( file, line ) ) { + auto const comment { line.find( "//" ) }; + if( comment != std::string::npos ) { line.erase( comment ); } + std::istringstream words { line }; + std::string word; + while( words >> word ) { tokens.push_back( word ); } + } + parse( tokens, Filename, Errors ); + return true; +} + +cantilever_part const *cantilever_catalog::part( std::string const &Name ) const { + auto const lookup { m_parts.find( Name ) }; + return ( lookup != m_parts.end() ? &lookup->second : nullptr ); +} + +cantilever_registration const *cantilever_catalog::registration( std::string const &Name ) const { + auto const lookup { m_registrations.find( Name ) }; + return ( lookup != m_registrations.end() ? &lookup->second : nullptr ); +} + +cantilever_scheme const *cantilever_catalog::scheme( int const Card, int const Execution ) const { + auto const lookup { m_schemes.find( { Card, Execution } ) }; + return ( lookup != m_schemes.end() ? &lookup->second : nullptr ); +} + +cantilever_assembly const *cantilever_catalog::assembly( std::string const &Name ) const { + auto const lookup { m_assemblies.find( Name ) }; + return ( lookup != m_assemblies.end() ? &lookup->second : nullptr ); +} + +cantilever_pole const *cantilever_catalog::pole( std::string const &Name ) const { + auto const lookup { m_poles.find( Name ) }; + return ( lookup != m_poles.end() ? &lookup->second : nullptr ); +} + +cantilever_mount const *cantilever_catalog::mount( std::string const &Name ) const { + auto const lookup { m_mounts.find( Name ) }; + return ( lookup != m_mounts.end() ? &lookup->second : nullptr ); +} + +double cantilever_pole::depth( double const Height ) const { + // zbieżność liczona od góry słupa; nad górą wymiar górny + auto const below { std::max( 0.0, top - Height ) }; + return depth_top + ( depth_base - depth_top ) * below / length; +} + +cantilever_scheme const *cantilever_catalog::scheme_for_reach( int const Card, int const Execution, double const Reach ) const { + auto const decade { Card / 10 }; + for( auto lookup { m_schemes.lower_bound( { decade * 10, 0 } ) }; lookup != m_schemes.end() && lookup->first.first / 10 == decade; ++lookup ) { + auto const &scheme { lookup->second }; + if( scheme.execution == Execution && Reach >= scheme.reach_min && Reach < scheme.reach_max ) { return &scheme; } + } + return nullptr; +} + +gantry_mast const *cantilever_catalog::mast( std::string const &Name ) const { + auto const lookup { m_masts.find( Name ) }; + return ( lookup != m_masts.end() ? &lookup->second : nullptr ); +} + +gantry_beam const *cantilever_catalog::beam( std::string const &Name ) const { + auto const lookup { m_beams.find( Name ) }; + return ( lookup != m_beams.end() ? &lookup->second : nullptr ); +} + +cantilever_input cantilever_nominal_input( cantilever_scheme const &Scheme, double const Reach ) { + cantilever_input input; + input.stay_mount = { Reach, Scheme.mount_stay }; + input.strut_mount = { Reach, Scheme.mount_strut }; + input.steady_mount = { Reach, Scheme.mount_steady }; + input.messenger = { 0.0, Scheme.height }; + return input; +} + +cantilever_placement const *cantilever_result::find( std::string const &Role ) const { + for( auto const &placement : parts ) { if( placement.role == Role ) { return &placement; } } + return nullptr; +} + +namespace { + +// ---- sposób składania: wyrażenia, odwołania do punktów, kierunki + +// znane pola części dostępne w wyrażeniach jako . +bool part_field( cantilever_part const &Part, std::string const &Field, double &Value ) { + if( Field == "length" ) { Value = Part.length; } + else if( Field == "clearance" ) { Value = Part.clearance; } + else if( Field == "pin" ) { Value = Part.pin; } + else if( Field == "radius" ) { Value = Part.radius; } + else if( Field == "eye" ) { Value = Part.eye; } + else if( Field == "hook.y" ) { Value = Part.hook[ 0 ]; } + else if( Field == "hook.z" ) { Value = Part.hook[ 1 ]; } + else if( Field == "end.y" ) { Value = Part.end[ 0 ]; } + else if( Field == "end.z" ) { Value = Part.end[ 1 ]; } + else { return false; } + return true; +} +bool is_part_field( std::string const &Field ) { + cantilever_part const dummy; + double value; + return part_field( dummy, Field, value ); +} + +// wyrażenie: składniki rozdzielone znakami + i -, bez spacji, np. "strut_insulator.length+rules.clamp_margin", "-steady.hook.y" +struct term { double sign { 1.0 }; std::string text; double number { 0.0 }; bool constant { false }; }; + +bool split_expression( std::string const &Text, std::vector &Terms ) { + Terms.clear(); + if( Text.empty() ) { return false; } + std::size_t start { 0 }; + double sign { 1.0 }; + if( Text[ 0 ] == '+' || Text[ 0 ] == '-' ) { sign = ( Text[ 0 ] == '-' ? -1.0 : 1.0 ); start = 1; } + while( start <= Text.size() ) { + // koniec składnika: następny znak + albo -, który nie jest częścią zapisu wykładnika liczby + auto end { start }; + while( end < Text.size() ) { + auto const c { Text[ end ] }; + if( ( c == '+' || c == '-' ) && end > start && false == ( ( Text[ end - 1 ] == 'e' || Text[ end - 1 ] == 'E' ) && std::isdigit( static_cast( Text[ start ] ) ) ) ) { break; } + ++end; + } + term item; + item.sign = sign; + item.text = Text.substr( start, end - start ); + if( item.text.empty() ) { return false; } + item.constant = to_number( item.text, item.number ); + if( false == item.constant && ( std::isdigit( static_cast( item.text[ 0 ] ) ) || item.text[ 0 ] == '.' ) ) { return false; } + Terms.push_back( item ); + if( end >= Text.size() ) { break; } + sign = ( Text[ end ] == '-' ? -1.0 : 1.0 ); + start = end + 1; + if( start >= Text.size() ) { return false; } + } + return false == Terms.empty(); +} + +bool is_name( std::string const &Text ) { + if( Text.empty() ) { return false; } + for( auto const c : Text ) { + if( false == ( std::isalnum( static_cast( c ) ) || c == '_' || c == '.' ) ) { return false; } + } + return true; +} + +// rodzaje argumentów klocków: n nazwa nowego symbolu, p punkt (nazwa albo @), l człon, e wyrażenie, +// E wyrażenie albo "-", r rola (z ? na początku: opcjonalna), R rola albo "-", d kierunek, a oś spoczynkowa części, i liczba całkowita +std::map const step_arguments { + { "point", "np" }, { "shift", "npee" }, { "side", "nlee" }, { "line", "np" }, { "hang", "rpp" }, + { "tangent", "nppen" }, { "level", "lnpe" }, { "project", "lnpee" }, { "limit", "lnee" }, { "window", "lnEE" }, + { "chain", "lnpeernEEE" }, { "place", "rpda" }, { "roll", "rR" }, { "stretch", "rppa" }, { "registration", "" }, + { "repeat", "ip" }, { "endrepeat", "" }, { "vee", "rppea" }, { "hangarms", "rrrlp" }, { "anchor", "nper" }, + { "flip", "r" }, { "snap", "lnrn" }, { "platform", "ppp" }, { "armcords", "rrpe" }, { "setarms", "rp" }, { "atleast", "lne" }, { "export", "p" }, { "hangat", "rp" }, { "across", "lnpe" }, { "hangto", "rplne" }, { "armropes", "rp" }, { "tee", "ppp" }, { "tee_bar", "pppn" } }; + +bool valid_argument( char const Kind, std::string const &Word ) { + std::vector terms; + switch( Kind ) { + case 'n': case 'l': return is_name( Word ); + case 'e': return split_expression( Word, terms ); + case 'E': return Word == "-" || split_expression( Word, terms ); + case 'r': return is_name( Word[ 0 ] == '?' ? Word.substr( 1 ) : Word ); + case 'R': return Word == "-" || is_name( Word ); + case 'p': { + auto const at { Word.find( '@' ) }; + if( at == std::string::npos ) { return is_name( Word ); } + return is_name( Word.substr( 0, at ) ) && split_expression( Word.substr( at + 1 ), terms ); + } + case 'd': { + auto name { Word[ 0 ] == '-' ? Word.substr( 1 ) : Word }; + return is_name( name ); + } + case 'a': return Word == "+y" || Word == "-y" || Word == "+z" || Word == "-z"; + case 'i': { double value; return to_number( Word, value ) && value >= 1.0; } + default: return false; + } +} + +std::string const *step_signature( std::string const &Op ) { + auto const lookup { step_arguments.find( Op ) }; + return ( lookup != step_arguments.end() ? &lookup->second : nullptr ); +} + +bool valid_step( std::string const &Op, std::vector const &Words, std::string &Reason ) { + auto signature { *step_signature( Op ) }; + if( Op == "line" ) { + // line toward | dir | along + auto const &how { Words[ 2 ] }; + if( how == "toward" ) { signature = "npkp"; } + else if( how == "dir" ) { signature = "npkee"; } + else if( how == "along" ) { signature = "npkl"; } + else { Reason = "expected \"toward\", \"dir\" or \"along\" instead of \"" + how + "\""; return false; } + } + for( std::size_t index = 0; index < Words.size(); ++index ) { + if( signature[ index ] == 'k' ) { continue; } + if( false == valid_argument( signature[ index ], Words[ index ] ) ) { Reason = "bad argument \"" + Words[ index ] + "\""; return false; } + } + return true; +} + +vec2 rest_axis( std::string const &Axis ) { + if( Axis == "+y" ) { return toward_support; } + if( Axis == "-y" ) { return toward_track; } + if( Axis == "+z" ) { return upward; } + return downward; +} + +// prosta w płaszczyźnie wysięgnika: oś członu albo kierunek pomocniczy, z nazwanymi położeniami wzdłuż niej +struct line { + vec2 origin; + vec2 direction; + vec2 normal; // prostopadła skierowana w górę + std::map stations; + vec2 at( double const Distance ) const { return origin + direction * Distance; } +}; + +// stan składania jednego wysięgnika +class assembler { +public: + assembler( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_input const &Input, cantilever_registration const *Registration, cantilever_result &Result ) + : m_catalog( Catalog ), m_scheme( Scheme ), m_input( Input ), m_registration( Registration ), m_result( Result ), + m_label( std::to_string( Scheme.card ) + "-" + std::to_string( Scheme.execution ) ) { + m_points[ "wire" ] = { 0.0, 0.0 }; + m_points[ "messenger" ] = to_vec( Input.messenger ); + m_points[ "strut_mount" ] = to_vec( Input.strut_mount ); + m_points[ "stay_mount" ] = to_vec( Input.stay_mount ); + m_points[ "steady_mount" ] = to_vec( Input.steady_mount ); + if( Registration != nullptr ) { m_points[ "registration" ] = { Registration->clamp, Registration->tube }; } + for( auto const &point : Input.points ) { m_points[ point.first ] = to_vec( point.second ); } + } + + bool run( std::vector const &Steps ) { + return run( Steps, 0, Steps.size() ); + } + +private: + // błąd przerywający składanie + struct failure {}; + + bool run( std::vector const &Steps, std::size_t const Begin, std::size_t const End ) { + try { + for( auto index { Begin }; index < End; ) { + auto const &step { Steps[ index ] }; + if( step.op == "repeat" ) { + // powtarzanie do ustalenia się punktu; ostrzeżenia zostają tylko z ostatniego przebiegu + auto const passes { static_cast( number( step.words[ 0 ] ) ) }; + auto const warnings { m_result.warnings.size() }; + for( int pass = 0; pass < passes; ++pass ) { + m_result.warnings.resize( warnings ); + auto const before { point( step.words[ 1 ] ) }; + if( false == run( Steps, index + 1, step.end - 1 ) ) { return false; } + if( length( point( step.words[ 1 ] ) - before ) < 1e-6 ) { break; } + } + index = step.end; + continue; + } + execute( step ); + ++index; + } + } + catch( failure const & ) { return false; } + return true; + } + + [[noreturn]] void fail( std::string const &Reason ) { + m_result.warnings.push_back( "cantilever " + m_label + ": " + Reason ); + throw failure(); + } + void warn( std::string const &Text ) { m_result.warnings.push_back( "cantilever " + m_label + ": " + Text ); } + + // nazwa części przypisanej roli; pusta, gdy schemat jej nie ma + std::string part_name( std::string const &Role ) const { + auto const replacement { m_input.overrides.find( Role ) }; + if( replacement != m_input.overrides.end() ) { return replacement->second; } + auto const lookup { m_scheme.roles.find( Role ) }; + return ( lookup != m_scheme.roles.end() ? lookup->second : std::string() ); + } + bool has_role( std::string const &Role ) const { return false == part_name( Role ).empty(); } + cantilever_part const &part( std::string const &Role ) { + auto const name { part_name( Role ) }; + if( name.empty() ) { fail( "the scheme has no part for the role \"" + Role + "\"" ); } + auto const *found { m_catalog.part( name ) }; + if( found == nullptr ) { fail( "unknown part \"" + name + "\" for the role \"" + Role + "\"" ); } + return *found; + } + // rola z nazwą części do komunikatów + std::string described( std::string const &Role ) const { + auto const name { part_name( Role ) }; + return Role + ( name.empty() ? std::string() : " \"" + name + "\"" ); + } + + line &line_named( std::string const &Name ) { + auto const lookup { m_lines.find( Name ) }; + if( lookup == m_lines.end() ) { fail( "unknown line \"" + Name + "\"" ); } + return lookup->second; + } + double station( std::string const &Line, std::string const &Station ) { + auto const &found { line_named( Line ) }; + auto const lookup { found.stations.find( Station ) }; + if( lookup == found.stations.end() ) { fail( "line \"" + Line + "\" has no position \"" + Station + "\"" ); } + return lookup->second; + } + + // wartość jednego składnika wyrażenia + double value( std::string const &Text ) { + double result; + if( to_number( Text, result ) ) { return result; } + auto const dot { Text.find( '.' ) }; + if( dot != std::string::npos ) { + auto const head { Text.substr( 0, dot ) }; + auto const tail { Text.substr( dot + 1 ) }; + if( head == "rules" ) { + auto const &rules { m_catalog.rules }; + if( tail == "stay_gap_min" ) { return rules.stay_gap_min; } + if( tail == "stay_gap" ) { return rules.stay_gap; } + if( tail == "stay_gap_max" ) { return rules.stay_gap_max; } + if( tail == "clamp_margin" ) { return rules.clamp_margin; } + if( tail == "cord_clamp_inset" ) { return rules.cord_clamp_inset; } + fail( "unknown rule \"" + tail + "\"" ); + } + auto const lineat { m_lines.find( head ) }; + if( lineat != m_lines.end() ) { + auto const stationat { lineat->second.stations.find( tail ) }; + if( stationat != lineat->second.stations.end() ) { return stationat->second; } + } + if( is_part_field( tail ) ) { + part_field( part( head ), tail, result ); + return result; + } + // nazwany punkt modelu części: ..y albo .z + if( has_role( head ) ) { + auto const last { tail.rfind( '.' ) }; + if( last != std::string::npos ) { + auto const &named { part( head ).points }; + auto const found { named.find( tail.substr( 0, last ) ) }; + if( found != named.end() ) { + if( tail.substr( last + 1 ) == "y" ) { return found->second.first; } + if( tail.substr( last + 1 ) == "z" ) { return found->second.second; } + } + } + } + auto const valueat { m_result.values.find( Text ) }; + if( valueat != m_result.values.end() ) { return valueat->second; } + // współrzędna punktu: .y albo .z + auto const last { Text.rfind( '.' ) }; + auto const pointat { m_points.find( Text.substr( 0, last ) ) }; + if( pointat != m_points.end() ) { + if( Text.substr( last + 1 ) == "y" ) { return pointat->second.y; } + if( Text.substr( last + 1 ) == "z" ) { return pointat->second.z; } + } + } + fail( "unknown value \"" + Text + "\"" ); + } + double number( std::string const &Text ) { + std::vector terms; + if( false == split_expression( Text, terms ) ) { fail( "bad expression \"" + Text + "\"" ); } + double result { 0.0 }; + auto first { true }; + for( auto const &item : terms ) { + auto const part { item.constant ? item.number : value( item.text ) }; + // pierwszy składnik bez dodawania do zera, żeby wynik był co do bitu taki, jak przy zapisie wprost + if( first ) { result = ( item.sign < 0.0 ? -part : part ); first = false; } + else if( item.sign < 0.0 ) { result = result - part; } + else { result = result + part; } + } + return result; + } + vec2 point( std::string const &Text ) { + auto const at { Text.find( '@' ) }; + if( at != std::string::npos ) { + auto const &found { line_named( Text.substr( 0, at ) ) }; + return found.at( number( Text.substr( at + 1 ) ) ); + } + auto const lookup { m_points.find( Text ) }; + if( lookup == m_points.end() ) { fail( "unknown point \"" + Text + "\"" ); } + return lookup->second; + } + // kierunek: , -, .up, .down (prostopadła w górę / w dół) + vec2 direction( std::string const &Text ) { + auto const reverse { Text[ 0 ] == '-' }; + auto name { reverse ? Text.substr( 1 ) : Text }; + auto side { 0 }; + auto const suffix = [ & ]( std::string const &Ending ) { + return name.size() > Ending.size() && name.compare( name.size() - Ending.size(), Ending.size(), Ending ) == 0 && m_lines.count( name ) == 0; + }; + if( suffix( ".up" ) ) { side = 1; name.erase( name.size() - 3 ); } + else if( suffix( ".down" ) ) { side = -1; name.erase( name.size() - 5 ); } + auto const &found { line_named( name ) }; + auto const result { side == 0 ? found.direction : ( side > 0 ? found.normal : found.normal * -1.0 ) }; + return ( reverse ? result * -1.0 : result ); + } + line &make_line( std::string const &Name, vec2 const &Origin, vec2 const &Direction ) { + auto &made { m_lines[ Name ] }; + made.origin = Origin; + made.direction = Direction; + made.normal = upward_normal( Direction ); + made.stations.clear(); + return made; + } + // zestaw ramion stawiany zerem w punkcie przewodu "wire"; Flip: zestaw od słupa (obrót o 180 stopni wokół pionu). + // Zestaw ze składnikami (member) stawia składniki zamiast siebie + void put_arm_set( std::string const &setrole, cantilever_part const &set, bool const flip ) { + auto const side { flip ? -1.0 : 1.0 }; + auto const turn = [ & ]( cantilever_placement &Placement ) { + if( flip ) { for( int column : { 0, 1 } ) { for( int row = 0; row < 3; ++row ) { Placement.rotation[ row * 3 + column ] = -Placement.rotation[ row * 3 + column ]; } } } + }; + if( set.members.empty() ) { + auto placement { place( setrole, set.name, m_points[ "wire" ], toward_support, toward_support ) }; + turn( placement ); + put( placement ); + } + else { + // zestaw z osobnych modeli: każdy składnik w swoim położeniu i obrocie wokół pionu, cały zestaw obracany jak zwykły + auto const &wire { m_points[ "wire" ] }; + for( std::size_t index = 0; index < set.members.size(); ++index ) { + auto const &member { set.members[ index ] }; + cantilever_placement placement; + placement.role = setrole + "." + std::to_string( index + 1 ); + placement.part = member.part; + placement.position[ 0 ] = side * member.position[ 0 ]; + placement.position[ 1 ] = wire.y + side * member.position[ 1 ]; + placement.position[ 2 ] = wire.z + member.position[ 2 ]; + auto const angle { member.yaw * 3.14159265358979323846 / 180.0 }; + auto const cosine { std::cos( angle ) }; + auto const sine { std::sin( angle ) }; + double const rotation[ 9 ] { cosine, -sine, 0.0, sine, cosine, 0.0, 0.0, 0.0, 1.0 }; + std::copy( std::begin( rotation ), std::end( rotation ), std::begin( placement.rotation ) ); + turn( placement ); + put( placement ); + // zaczep przewodu każdego ramienia zestawu (zero modelu ramienia) - dane dla edytora + auto const *memberpart { m_catalog.part( member.part ) }; + if( memberpart != nullptr && memberpart->kind == "arms" ) { + m_result.values[ placement.role + ".wire.x" ] = placement.position[ 0 ]; + m_result.values[ placement.role + ".wire.y" ] = placement.position[ 1 ]; + m_result.values[ placement.role + ".wire.z" ] = placement.position[ 2 ]; + } + } + } + } + + // część ustawiona drugi raz (w kolejnym przebiegu "repeat") zastępuje poprzednie ustawienie + void put( cantilever_placement const &Input ) { + auto Placement { Input }; + // część z innym plikiem modelu (lina 9832-2 z modelu linki 4490-00) + if( Placement.model.empty() ) { + auto const *known { m_catalog.part( Placement.part ) }; + if( known != nullptr && false == known->model.empty() ) { Placement.model = m_catalog.models() + known->model; } + } + for( auto &placed : m_result.parts ) { + if( placed.role == Placement.role && Placement.role != "registration" ) { placed = Placement; return; } + } + m_result.parts.push_back( Placement ); + } + cantilever_placement *placed( std::string const &Role ) { + for( auto &placement : m_result.parts ) { if( placement.role == Role ) { return &placement; } } + return nullptr; + } + + // część w dowolnym kierunku w przestrzeni: oś spoczynkowa Axis (+y -y +z -z) wzdłuż Direction (wektor jednostkowy X Y Z), + // oś X części możliwie wzdłuż toru + cantilever_placement place_3d( std::string const &Role, std::string const &Part, vec2 const &Origin, double const ( &Direction )[ 3 ], std::string const &Axis ) { + cantilever_placement placement; + placement.role = Role; + placement.part = Part; + placement.position[ 1 ] = Origin.y; + placement.position[ 2 ] = Origin.z; + auto const sign { Axis[ 0 ] == '-' ? -1.0 : 1.0 }; + double const main[ 3 ] { Direction[ 0 ] * sign, Direction[ 1 ] * sign, Direction[ 2 ] * sign }; + // X części: oś toru bez składowej wzdłuż głównej osi + double x[ 3 ] { 1.0 - main[ 0 ] * main[ 0 ], -main[ 0 ] * main[ 1 ], -main[ 0 ] * main[ 2 ] }; + auto const size { std::sqrt( x[ 0 ] * x[ 0 ] + x[ 1 ] * x[ 1 ] + x[ 2 ] * x[ 2 ] ) }; + for( auto &value : x ) { value /= size; } + double y[ 3 ], z[ 3 ]; + if( Axis[ 1 ] == 'z' ) { + std::copy( std::begin( main ), std::end( main ), std::begin( z ) ); + y[ 0 ] = z[ 1 ] * x[ 2 ] - z[ 2 ] * x[ 1 ]; y[ 1 ] = z[ 2 ] * x[ 0 ] - z[ 0 ] * x[ 2 ]; y[ 2 ] = z[ 0 ] * x[ 1 ] - z[ 1 ] * x[ 0 ]; + } + else { + std::copy( std::begin( main ), std::end( main ), std::begin( y ) ); + z[ 0 ] = x[ 1 ] * y[ 2 ] - x[ 2 ] * y[ 1 ]; z[ 1 ] = x[ 2 ] * y[ 0 ] - x[ 0 ] * y[ 2 ]; z[ 2 ] = x[ 0 ] * y[ 1 ] - x[ 1 ] * y[ 0 ]; + } + for( int row = 0; row < 3; ++row ) { + placement.rotation[ row * 3 + 0 ] = x[ row ]; + placement.rotation[ row * 3 + 1 ] = y[ row ]; + placement.rotation[ row * 3 + 2 ] = z[ row ]; + } + return placement; + } + + void execute( cantilever_step const &Step ) { + auto const &op { Step.op }; + auto const &w { Step.words }; + + if( op == "point" ) { m_points[ w[ 0 ] ] = point( w[ 1 ] ); return; } + + if( op == "shift" ) { + auto const base { point( w[ 1 ] ) }; + m_points[ w[ 0 ] ] = base + vec2 { number( w[ 2 ] ), number( w[ 3 ] ) }; + return; + } + + if( op == "side" ) { + // punkt na członie, odsunięty od jego osi wzdłuż prostopadłej skierowanej w górę + auto const &found { line_named( w[ 1 ] ) }; + m_points[ w[ 0 ] ] = found.at( number( w[ 2 ] ) ) + found.normal * number( w[ 3 ] ); + return; + } + + if( op == "line" ) { + auto const origin { point( w[ 1 ] ) }; + auto const &how { w[ 2 ] }; + if( how == "toward" ) { make_line( w[ 0 ], origin, unit( point( w[ 3 ] ) - origin ) ); } + else if( how == "dir" ) { make_line( w[ 0 ], origin, unit( vec2 { number( w[ 3 ] ), number( w[ 4 ] ) } ) ); } + else { make_line( w[ 0 ], origin, line_named( w[ 3 ] ).direction ); } + return; + } + + if( op == "hang" ) { + // część wisząca w jednej linii z linką: zero w punkcie zawieszenia, prosta od punktu "end" do "hook" części + // idzie wzdłuż kierunku od punktu zaczepu linki do punktu zawieszenia + auto const &role { w[ 0 ] }; + auto const &hung { part( role ) }; + vec2 const hook { hung.hook[ 0 ], hung.hook[ 1 ] }; + vec2 const end { hung.end[ 0 ], hung.end[ 1 ] }; + if( length( hook - end ) < 1e-4 ) { fail( "part \"" + hung.name + "\" has no \"hook\" and \"end\" points" ); } + auto const axis { unit( hook - end ) }; + auto const origin { point( w[ 1 ] ) }; + auto const pull { unit( origin - point( w[ 2 ] ) ) }; + auto const turn { std::atan2( pull.z, pull.y ) - std::atan2( axis.z, axis.y ) }; + m_points[ role + ".hook" ] = origin + rotate( hook, turn ); + m_points[ role + ".end" ] = origin + rotate( end, turn ); + make_line( role, origin, pull ); + put( place( role, hung.name, origin, pull, axis ) ); + return; + } + + if( op == "hangat" ) { + // uchwyt liny wiszący pionowo hakiem w punkcie Hook (drugi uchwyt liny na ukośniku, 102x); zero (oś liny) trafia + // do punktu , haczyk i koniec do .hook, .end + auto const &role { w[ 0 ] }; + auto const &hung { part( role ) }; + vec2 const hook { hung.hook[ 0 ], hung.hook[ 1 ] }; + vec2 const end { hung.end[ 0 ], hung.end[ 1 ] }; + if( length( hook - end ) < 1e-4 ) { fail( "part \"" + hung.name + "\" has no \"hook\" and \"end\" points" ); } + auto const axis { unit( hook - end ) }; + auto const turn { std::atan2( 1.0, 0.0 ) - std::atan2( axis.z, axis.y ) }; + auto const origin { point( w[ 1 ] ) - rotate( hook, turn ) }; + m_points[ role ] = origin; + m_points[ role + ".hook" ] = origin + rotate( hook, turn ); + m_points[ role + ".end" ] = origin + rotate( end, turn ); + make_line( role, origin, upward ); + put( place( role, hung.name, origin, upward, axis ) ); + return; + } + + if( op == "hangto" ) { + // uchwyt liny z zerem w punkcie Rope, zawieszony hakiem na uchu uchwytu stawianego na członie Line (ucho odsunięte o Side + // wzdłuż prostopadłej): ucho tam, gdzie jego odległość od liny równa się odległości haka od zera uchwytu (wyżej z dwóch + // rozwiązań); położenie ucha na członie trafia do stacji Station. Drugi uchwyt liny na ukośniku (102x) + auto const &role { w[ 0 ] }; + auto const &hung { part( role ) }; + vec2 const hook { hung.hook[ 0 ], hung.hook[ 1 ] }; + vec2 const end { hung.end[ 0 ], hung.end[ 1 ] }; + if( length( hook - end ) < 1e-4 ) { fail( "part \"" + hung.name + "\" has no \"hook\" and \"end\" points" ); } + auto const rope { point( w[ 1 ] ) }; + auto &found { line_named( w[ 2 ] ) }; + auto const side { number( w[ 4 ] ) }; + auto const base { found.origin + found.normal * side - rope }; + auto const radius { length( hook ) }; + // |base + dir*s|^2 = r^2 + auto const b { dot( base, found.direction ) }; + auto const c { dot( base, base ) - radius * radius }; + auto const delta { b * b - c }; + if( delta < 0.0 ) { fail( "the " + role + " can't reach the " + w[ 2 ] + " from the rope" ); } + auto const s1 { -b - std::sqrt( delta ) }; + auto const s2 { -b + std::sqrt( delta ) }; + auto const e1 { found.at( s1 ) + found.normal * side }; + auto const e2 { found.at( s2 ) + found.normal * side }; + auto const pick { e1.z >= e2.z ? s1 : s2 }; + auto const eye { e1.z >= e2.z ? e1 : e2 }; + found.stations[ w[ 3 ] ] = pick; + auto const turn { std::atan2( ( eye - rope ).z, ( eye - rope ).y ) - std::atan2( hook.z, hook.y ) }; + auto const axis { unit( hook - end ) }; + auto const pull { rotate( axis, turn ) }; + m_points[ role ] = rope; + m_points[ role + ".hook" ] = eye; + m_points[ role + ".end" ] = rope + rotate( end, turn ); + make_line( role, rope, pull ); + put( place( role, hung.name, rope, pull, axis ) ); + return; + } + + if( op == "tangent" ) { + // człon z punktu, styczny do okręgu o promieniu Radius wokół punktu Center; okrąg leży pod członem. + // położenie Station to punkt styczności (rzut środka okręgu na oś członu) + auto const from { point( w[ 1 ] ) }; + auto const center { point( w[ 2 ] ) }; + auto const radius { number( w[ 3 ] ) }; + auto const toward { center - from }; + auto const distance { length( toward ) }; + if( distance <= radius + 1e-6 ) { fail( "line \"" + w[ 0 ] + "\" starts inside the circle it should touch" ); } + auto const along { std::sqrt( distance * distance - radius * radius ) }; + auto const base { std::atan2( toward.z, toward.y ) }; + auto const offset { std::asin( radius / distance ) }; + vec2 heading, normal; + for( auto const sign : { 1.0, -1.0 } ) { + auto const angle { base + sign * offset }; + heading = { std::cos( angle ), std::sin( angle ) }; + normal = upward_normal( heading ); + if( length( from + heading * along - normal * radius - center ) < 1e-6 ) { break; } + } + make_line( w[ 0 ], from, heading ).stations[ w[ 4 ] ] = along; + return; + } + + if( op == "level" ) { + // położenie na członie, w którym punkt odsunięty o Side wzdłuż prostopadłej wypada na wysokości punktu Target + auto &found { line_named( w[ 0 ] ) }; + if( std::abs( found.direction.z ) < 1e-6 ) { fail( "line \"" + w[ 0 ] + "\" is level, nothing on it can be set to a height" ); } + auto const side { number( w[ 3 ] ) }; + found.stations[ w[ 1 ] ] = ( point( w[ 2 ] ).z - found.origin.z - side * found.normal.z ) / found.direction.z; + return; + } + + if( op == "across" ) { + // położenie na członie, w którym punkt odsunięty o Side wzdłuż prostopadłej wypada w poziomie (Y) punktu Target + auto &found { line_named( w[ 0 ] ) }; + if( std::abs( found.direction.y ) < 1e-6 ) { fail( "line \"" + w[ 0 ] + "\" is vertical, nothing on it can be set across" ); } + auto const side { number( w[ 3 ] ) }; + found.stations[ w[ 1 ] ] = ( point( w[ 2 ] ).y - found.origin.y - side * found.normal.y ) / found.direction.y; + return; + } + + if( op == "project" ) { + // rzut punktu na człon, najdalej do End - Inset; wystawanie członu za tym położeniem trafia do .overhang, + // a brakujący kawałek do .short + auto &found { line_named( w[ 0 ] ) }; + auto const nominal { dot( point( w[ 2 ] ) - found.origin, found.direction ) }; + auto const end { number( w[ 3 ] ) }; + auto const inset { number( w[ 4 ] ) }; + auto const along { std::min( nominal, end - inset ) }; + found.stations[ w[ 1 ] ] = along; + m_result.values[ w[ 0 ] + ".overhang" ] = end - along; + m_result.values[ w[ 0 ] + ".short" ] = std::max( 0.0, nominal - ( end - inset ) ); + // gdy człon nie sięga położenia nominalnego, część staje przy jego końcu (z zapasem Inset); katalog nie podaje + // sztywnej odległości, więc to nie jest uwaga do logu (Jan, 2026-10-13: obejma 4210) + if( nominal < 0.0 ) { warn( described( w[ 0 ] ) + " starts beyond the " + w[ 1 ] ); } + return; + } + + if( op == "limit" ) { + // za położeniem Station musi zostać co najmniej Margin członu, który kończy się w End + auto const at { station( w[ 0 ], w[ 1 ] ) }; + auto const overhang { number( w[ 2 ] ) - at }; + auto const margin { number( w[ 3 ] ) }; + m_result.values[ w[ 0 ] + ".overhang" ] = overhang; + if( overhang < margin ) { warn( described( w[ 0 ] ) + " is " + to_text( margin - overhang ) + " m too short for the " + w[ 1 ] ); } + return; + } + + if( op == "export" ) { + // punkt do wyniku (values .y, .z) - dane dla edytora, np. zaczep drugiego przewodu na 4120 (117x/118x) + auto const at { point( w[ 0 ] ) }; + m_result.values[ w[ 0 ] + ".y" ] = at.y; + m_result.values[ w[ 0 ] + ".z" ] = at.z; + return; + } + + if( op == "atleast" ) { + // położenie na członie nie bliżej początku niż Min (uchwyt liny ramion za izolatorem ukośnika, 105x/125x) + auto &found { line_named( w[ 0 ] ) }; + auto const at { station( w[ 0 ], w[ 1 ] ) }; + found.stations[ w[ 1 ] ] = std::max( at, number( w[ 2 ] ) ); + return; + } + + if( op == "window" ) { + auto const at { station( w[ 0 ], w[ 1 ] ) }; + if( w[ 2 ] != "-" && at < number( w[ 2 ] ) ) { warn( "the " + w[ 1 ] + " falls too close to the start of the " + w[ 0 ] ); } + if( w[ 3 ] != "-" && at > number( w[ 3 ] ) ) { warn( "the " + w[ 1 ] + " falls too close to the end of the " + w[ 0 ] ); } + return; + } + + if( op == "anchor" ) { + // zaczep przesuwany w pionie tak, żeby łańcuch o długości Length + jeden z otworów części Holes sięgnął punktu + // Target; wybierany otwór z najmniejszym przesunięciem (zaczep odciągu na słupie jest regulowany, "~1,20 m") + auto const found { m_points.find( w[ 0 ] ) }; + if( found == m_points.end() ) { fail( "unknown point \"" + w[ 0 ] + "\"" ); } + auto const target { point( w[ 1 ] ) }; + auto const base { number( w[ 2 ] ) }; + auto const &holes { part( w[ 3 ] ) }; + if( holes.holes.empty() ) { fail( "part \"" + holes.name + "\" has no adjustment holes" ); } + auto const old { found->second }; + auto const dy { target.y - old.y }; + auto best { -1 }; + auto bestz { old.z }; + for( std::size_t hole = 0; hole < holes.holes.size(); ++hole ) { + auto const reach { base + holes.holes[ hole ] }; + if( reach <= std::abs( dy ) ) { continue; } + auto const rise { std::sqrt( reach * reach - dy * dy ) }; + for( auto const z : { target.z + rise, target.z - rise } ) { + if( best < 0 || std::abs( z - old.z ) < std::abs( bestz - old.z ) ) { best = static_cast( hole ); bestz = z; } + } + } + if( best < 0 ) { + // brak do 2 cm: odciąg w poziomie na ostatnim otworze (luz na sworzniu, na oko niewidoczne; 0738/0748 na granicy) + auto const longest { base + holes.holes.back() }; + if( std::abs( dy ) - longest > 0.02 ) { + warn( described( w[ 3 ] ) + " can't make the chain from the " + w[ 0 ] + " reach the " + w[ 1 ] + ", " + to_text( std::abs( dy ) - longest ) + " m short" ); + return; + } + best = static_cast( holes.holes.size() ) - 1; + bestz = target.z; + } + if( std::abs( bestz - old.z ) > 0.15 ) { + warn( "the " + w[ 0 ] + " had to move " + to_text( bestz - old.z ) + " m to reach the " + w[ 1 ] ); + } + found->second.z = bestz; + m_result.values[ w[ 0 ] + ".shift" ] = bestz - old.z; + m_result.values[ w[ 3 ] + ".hole" ] = best; + return; + } + + if( op == "chain" ) { + // łańcuch o długości skokowej (Length + jeden z otworów części Holes) od punktu Anchor do punktu odsuniętego + // o Side od osi członu, w położeniu Station dobieranym tak, żeby odstęp Reference - Station leżał + // w przedziale Min..Max możliwie blisko Nominal + auto &found { line_named( w[ 0 ] ) }; + auto const &newstation { w[ 1 ] }; + auto const anchor { point( w[ 2 ] ) }; + auto const side { number( w[ 3 ] ) }; + auto const base { number( w[ 4 ] ) }; + auto const &holes { part( w[ 5 ] ) }; + auto const reference { station( w[ 0 ], w[ 6 ] ) }; + auto const gapmin { number( w[ 7 ] ) }; + auto const gap { number( w[ 8 ] ) }; + auto const gapmax { number( w[ 9 ] ) }; + if( holes.holes.empty() ) { fail( "part \"" + holes.name + "\" has no adjustment holes" ); } + auto const pinbase { found.origin + found.normal * side - anchor }; + auto const along { dot( pinbase, found.direction ) }; + auto best { -1 }; + auto bestdistance { 0.0 }; + auto bestpenalty { 0.0 }; + auto bestfits { false }; + for( std::size_t hole = 0; hole < holes.holes.size(); ++hole ) { + auto const reach { base + holes.holes[ hole ] }; + auto const discriminant { along * along - dot( pinbase, pinbase ) + reach * reach }; + if( discriminant < 0.0 ) { continue; } + for( auto const sign : { -1.0, 1.0 } ) { + auto const distance { -along + sign * std::sqrt( discriminant ) }; + auto const offset { reference - distance }; + auto const fits { offset >= gapmin - 1e-9 && offset <= gapmax + 1e-9 }; + auto const penalty { std::abs( offset - gap ) }; + if( best < 0 || ( fits && false == bestfits ) || ( fits == bestfits && penalty < bestpenalty ) ) { + best = static_cast( hole ); + bestdistance = distance; + bestpenalty = penalty; + bestfits = fits; + } + } + } + auto chosen { reference - gap }; + if( best < 0 ) { + // łańcuch za krótki, żeby w ogóle sięgnąć członu: położenie zalecane, części ustawione w linii mimo luki + warn( described( w[ 5 ] ) + " can't make the chain from the " + w[ 2 ] + " reach the " + w[ 0 ] ); + best = static_cast( holes.holes.size() ) - 1; + } + else { + chosen = bestdistance; + if( false == bestfits ) { + warn( "no hole of \"" + holes.name + "\" puts the " + newstation + " " + to_text( gapmin, 2 ) + "-" + to_text( gapmax, 2 ) + " m from the " + w[ 6 ] + ", the nearest gives " + to_text( reference - chosen ) + " m" ); + } + } + found.stations[ newstation ] = chosen; + m_result.values[ newstation + ".hole" ] = best; + m_result.values[ newstation + ".gap" ] = reference - chosen; + return; + } + + if( op == "place" ) { + auto role { w[ 0 ] }; + if( role[ 0 ] == '?' ) { role.erase( 0, 1 ); if( false == has_role( role ) ) { return; } } + auto const &placedpart { part( role ) }; + put( place( role, placedpart.name, point( w[ 1 ] ), direction( w[ 2 ] ), rest_axis( w[ 3 ] ) ) ); + return; + } + + if( op == "roll" ) { + // ćwierć obrotu wokół własnej osi; z warunkiem: tylko gdy schemat ma podaną rolę + if( w[ 1 ] != "-" && false == has_role( w[ 1 ] ) ) { return; } + auto *target { placed( w[ 0 ] ) }; + if( target == nullptr ) { fail( "\"roll\" of \"" + w[ 0 ] + "\", which isn't placed yet" ); } + roll_quarter( *target ); + return; + } + + if( op == "flip" ) { + // pół obrotu wokół własnej osi podłużnej (lokalnej Y): lokalne X i Z na przeciwną stronę (6010: ucho odciągu u góry, obejma wysięgu u dołu) + auto *target { placed( w[ 0 ] ) }; + if( target == nullptr ) { fail( "\"flip\" of \"" + w[ 0 ] + "\", which isn't placed yet" ); } + for( auto const index : { 0, 3, 6, 2, 5, 8 } ) { target->rotation[ index ] = -target->rotation[ index ]; } + return; + } + + if( op == "snap" ) { + // położenie na członie przesunięte do najbliższego środka między sąsiednimi punktami _1, _2, ... części roli + // (punkty leżą wzdłuż -Y części, zero części na początku członu): uchwyt 6710 jak U-śruba przez dwa otwory 6010 + auto &found { line_named( w[ 0 ] ) }; + auto const station { found.stations.find( w[ 1 ] ) }; + if( station == found.stations.end() ) { fail( "line \"" + w[ 0 ] + "\" has no station \"" + w[ 1 ] + "\"" ); } + auto const &holder { part( w[ 2 ] ) }; + std::vector marks; + for( int index = 1; ; ++index ) { + auto const mark { holder.points.find( w[ 3 ] + "_" + std::to_string( index ) ) }; + if( mark == holder.points.end() ) { break; } + marks.push_back( -mark->second.first ); + } + if( marks.size() < 2 ) { fail( "part \"" + holder.name + "\" has fewer than two \"" + w[ 3 ] + "_N\" points" ); } + std::sort( marks.begin(), marks.end() ); + auto best { 0.5 * ( marks[ 0 ] + marks[ 1 ] ) }; + auto pair { 1 }; + for( std::size_t index = 1; index + 1 < marks.size(); ++index ) { + auto const middle { 0.5 * ( marks[ index ] + marks[ index + 1 ] ) }; + if( std::abs( middle - station->second ) < std::abs( best - station->second ) ) { best = middle; pair = static_cast( index ) + 1; } + } + auto const shift { best - station->second }; + if( std::abs( shift ) > 0.03 ) { + warn( "the " + w[ 1 ] + " on the " + w[ 0 ] + " is " + to_text( shift ) + " m from the nearest pair of " + w[ 3 ] + " points of \"" + holder.name + "\"" ); + } + m_result.values[ w[ 1 ] + ".pair" ] = pair; + m_result.values[ w[ 1 ] + ".snap" ] = shift; + station->second = best; + return; + } + + if( op == "platform" ) { + // ukośnik peronowy 6010 (rola strut) wsparty odciągiem bez izolatora: oko ukośnika na zaczepie StrutMount przesuwanym + // w pionie, odciąg (stay + otwór stay_end) od zaczepu StayMount do stałego ucha "eye" ukośnika, kolumna od osi blachy + // 6710 (plate) do liny Messenger odchyla się swobodnie (Jan). Dla każdej pary otworów pod U-śrubę (hole_N, rola + // hanger_mount) i każdego otworu końcówki odciągu szukane jest takie obniżenie zaczepu ukośnika, przy którym kolumna + // dokładnie sięga liny; wybierane jest najmniejsze obniżenie, przy równych - kolumna najbliżej pionu. + auto const mount { point( w[ 0 ] ) }; + auto const staymount { point( w[ 1 ] ) }; + auto const messenger { point( w[ 2 ] ) }; + auto const &strutpart { part( "strut" ) }; + auto const &endpart { part( "stay_end" ) }; + auto const pointof = [&]( cantilever_part const &Part, std::string const &Name ) { + auto const found { Part.points.find( Name ) }; + if( found == Part.points.end() ) { fail( "part \"" + Part.name + "\" has no point \"" + Name + "\"" ); } + return vec2 { found->second.first, found->second.second }; + }; + auto const lug { pointof( strutpart, "eye" ) }; // (Y, Z) w układzie rury: Y wzdłuż -rury + auto const lugstation { -lug.y }; + auto const lugside { lug.z }; // ucho odciągu nad osią rury (modele 6010 od 2026-10-16) + auto const pivotside { -pointof( part( "hanger_mount" ), "eye" ).z }; // oś blachy pod rurą + auto const &plate { part( "plate" ) }; + auto const &hanger { part( "hanger" ) }; + auto const &clamp { part( "messenger_clamp" ) }; + auto const column { ( pointof( plate, "pivot" ).z - pointof( plate, "eye" ).z ) + part( "hanger_insulator" ).length - pointof( hanger, "eye" ).z + clamp.hook[ 1 ] }; + std::vector pairs; + { + std::vector marks; + for( int index = 1; ; ++index ) { + auto const found { strutpart.points.find( "hole_" + std::to_string( index ) ) }; + if( found == strutpart.points.end() ) { break; } + marks.push_back( -found->second.first ); + } + std::sort( marks.begin(), marks.end() ); + for( std::size_t index = 0; index + 1 < marks.size(); ++index ) { pairs.push_back( 0.5 * ( marks[ index ] + marks[ index + 1 ] ) ); } + } + if( pairs.empty() ) { fail( "part \"" + strutpart.name + "\" has no pairs of \"hole_N\" points" ); } + auto const lugradius { std::sqrt( lugstation * lugstation + lugside * lugside ) }; + auto const lugangle { std::atan2( lugside, lugstation ) }; // kąt ucha nad osią rury, widziany z oka + struct pose { bool ok { false }; vec2 origin, direction, pivot; }; + auto const solve = [&]( double Drop, double Station, double Stay ) { + pose result; + vec2 const origin { mount.y, mount.z - Drop }; + auto const between { staymount - origin }; + auto const distance { length( between ) }; + if( distance > lugradius + Stay || distance < std::abs( lugradius - Stay ) ) { return result; } + auto const along { ( lugradius * lugradius - Stay * Stay + distance * distance ) / ( 2.0 * distance ) }; + auto const across { std::sqrt( std::max( 0.0, lugradius * lugradius - along * along ) ) }; + auto const axis { unit( between ) }; + vec2 const normal { -axis.z, axis.y }; + // dwa punkty przecięcia; ucho jest po stronie toru (mniejsze Y) + auto const first { origin + axis * along + normal * across }; + auto const second { origin + axis * along - normal * across }; + auto const lugpoint { ( first.y < second.y ? first : second ) }; + auto const toward { unit( lugpoint - origin ) }; + auto const angle { std::atan2( toward.z, toward.y ) + lugangle }; + // rura idzie w stronę toru (Y maleje); ucho leży nad nią + vec2 const direction { std::cos( angle ), std::sin( angle ) }; + if( direction.y > -0.05 || direction.z < 0.0 ) { return result; } + auto const up { upward_normal( direction ) }; + result.ok = true; + result.origin = origin; + result.direction = direction; + result.pivot = origin + direction * Station - up * pivotside; + return result; + }; + auto bestscore { 1e9 }; + pose best; + double bestdrop { 0.0 }, beststation { 0.0 }; + int besthole { -1 }; + for( auto const station : pairs ) { + for( std::size_t hole = 0; hole < endpart.holes.size(); ++hole ) { + auto const stay { number( "stay.length" ) + endpart.holes[ hole ] }; + // f(drop) = odległość osi blachy od liny - długość kolumny; szukane miejsce zerowe w -1,0 .. 2,5 m + auto f = [&]( double Drop ) { auto const p { solve( Drop, station, stay ) }; return p.ok ? length( p.pivot - messenger ) - column : 1e9; }; + double previous { -1.0 }, fprevious { f( previous ) }; + for( double drop = -1.0 + 0.02; drop <= 2.5; drop += 0.02 ) { + auto const fnow { f( drop ) }; + if( fprevious < 1e8 && fnow < 1e8 && ( fprevious <= 0.0 ) != ( fnow <= 0.0 ) ) { + double low { previous }, high { drop }, flow { fprevious }; + for( int step = 0; step < 50; ++step ) { + auto const middle { 0.5 * ( low + high ) }; + auto const fmiddle { f( middle ) }; + if( ( fmiddle <= 0.0 ) == ( flow <= 0.0 ) ) { low = middle; flow = fmiddle; } else { high = middle; } + } + auto const p { solve( low, station, stay ) }; + auto const hanging { unit( messenger - p.pivot ) }; + auto const tilt { std::abs( std::atan2( hanging.y, -hanging.z ) ) }; + auto const score { std::abs( low ) + 0.2 * tilt }; + if( p.ok && hanging.z < 0.0 && score < bestscore ) { bestscore = score; best = p; bestdrop = low; beststation = station; besthole = static_cast( hole ); } + } + previous = drop; fprevious = fnow; + } + } + } + if( besthole < 0 ) { fail( "no position of the platform strut \"" + strutpart.name + "\" lets the stay and the hanging column reach the messenger" ); } + auto const hanging { unit( messenger - best.pivot ) }; + make_line( "strut", best.origin, best.direction ); + auto &strut { line_named( "strut" ) }; + strut.stations[ "top" ] = beststation; + strut.stations[ "lug" ] = lugstation; + m_points[ "strut_pin" ] = best.origin; + m_points[ "stay_pin" ] = best.origin + best.direction * lugstation + upward_normal( best.direction ) * lugside; + m_points[ "plate_pivot" ] = best.pivot; + make_line( "column", best.pivot, hanging ); + m_result.values[ "strut.drop" ] = bestdrop; + m_result.values[ "stay_end.hole" ] = besthole; + m_result.values[ "column.tilt" ] = std::atan2( hanging.y, -hanging.z ) * 180.0 / 3.14159265358979323846; + // obniżenie zaczepu nie jest ostrzeżeniem: rura peronowa jest z polimeru, mocowanie może zejść nawet metr nad ziemię (Jan) + return; + } + + if( op == "tee" ) { + // wysięgnik z teownika (stary katalog Kolprojekt, 01xx, 09xx): izolator 7110 (rola strut_insulator) na zaczepie + // StrutMount i ukośnik teowy 6101 (rola strut) to jedna sztywna bryła obracająca się wokół zaczepu; pręt górny + // (rola stay, długość do punktu "eye") z izolatorem 7120 (rola stay_insulator) od zaczepu StayMount leży poziomo i łączy + // się z punktem "eye" ukośnika; lina nośna leży w siodle (rola messenger_clamp, punkt "rope") na poziomym ramieniu + // ukośnika, w zakresie slide_horizontal_min..max. Pozycja bryły: kąt z warunku poziomego pręta przy danym położeniu + // zaczepu górnego w poziomie, wysokość zaczepu dolnego z wysokości liny (wartość strut.drop), wysokość zaczepu górnego + // z wysokości ucha ukośnika (stay_mount.shift). Człony: strut_insulator, tee (oś poziomego ramienia, obrót bryły), + // stay (pręt), leg (linia slide_min..slide_max skośnego ramienia, położenie "end" na jej końcu). + auto const mount { point( w[ 0 ] ) }; + auto const upper { point( w[ 1 ] ) }; + auto const rope { point( w[ 2 ] ) }; + auto const pointof = [&]( cantilever_part const &Part, std::string const &Name ) { + auto const found { Part.points.find( Name ) }; + if( found == Part.points.end() ) { fail( "part \"" + Part.name + "\" has no point \"" + Name + "\"" ); } + return vec2 { found->second.first, found->second.second }; + }; + auto const &strutpart { part( "strut" ) }; + auto const slidemin { pointof( strutpart, "slide_min" ) }; + auto const slidemax { pointof( strutpart, "slide_max" ) }; + auto const flatmin { pointof( strutpart, "slide_horizontal_min" ) }; + auto const flatmax { pointof( strutpart, "slide_horizontal_max" ) }; + auto const eye { pointof( strutpart, "eye" ) }; + auto const ropez { pointof( part( "messenger_clamp" ), "rope" ).z }; + auto const legdir { unit( slidemax - slidemin ) }; // skośne ramię w układzie części (w -Y i w górę) + auto const insulator { part( "strut_insulator" ).length }; + auto const topinsulator { part( "stay_insulator" ).length }; + auto const base { legdir * insulator }; // od zaczepu do zera ukośnika, bryła w spoczynku + auto const body { base + eye }; // od zaczepu do ucha ukośnika + // pręt poziomy łączy się z uchem ukośnika swoim punktem "eye" (przechylne, 4310) albo jednym z otworów hole_N + // (nieprzechylne, 4320; otwór z najmniejszym obrotem bryły, bez preferencji - Jan 2026-10-18); punkt pręta leży + // o -z pod jego osią. Obrót bryły wokół zaczepu dolnego tak, żeby ucho wypadło o długość pręta przed zaczepem górnym + auto const &barpart { part( "stay" ) }; + std::vector> joints; + if( barpart.points.count( "eye" ) ) { joints.push_back( { "eye", pointof( barpart, "eye" ) } ); } + for( int index = 1; index < 20; ++index ) { + auto const name { "hole_" + std::to_string( index ) }; + if( barpart.points.count( name ) == 0 ) { break; } + joints.push_back( { name, pointof( barpart, name ) } ); + } + if( joints.empty() ) { joints.push_back( { "length", vec2 { -barpart.length, 0.0 } } ); } + auto const reach { length( body ) }; + auto const alpha { std::atan2( body.z, body.y ) }; + auto const normalized = []( double Angle ) { + auto const pi { 3.14159265358979323846 }; + while( Angle > pi ) { Angle -= 2.0 * pi; } + while( Angle < -pi ) { Angle += 2.0 * pi; } + return Angle; + }; + auto tilt { 0.0 }; + auto best { -1 }; + vec2 joint; + for( std::size_t index = 0; index < joints.size(); ++index ) { + auto const need { upper.y - ( topinsulator - joints[ index ].second.y ) - mount.y }; + if( std::abs( need ) > reach ) { continue; } + auto const turn0 { std::acos( need / reach ) }; + auto const first { normalized( turn0 - alpha ) }; + auto const second { normalized( -turn0 - alpha ) }; + auto const candidate { std::abs( first ) < std::abs( second ) ? first : second }; + if( best < 0 || std::abs( candidate ) < std::abs( tilt ) ) { tilt = candidate; best = static_cast( index ); joint = joints[ index ].second; } + } + if( best < 0 ) { fail( "the strut \"" + strutpart.name + "\" can't reach the top bar \"" + barpart.name + "\" from its mount" ); } + m_result.values[ "stay.joint" ] = joints[ best ].first.compare( 0, 5, "hole_" ) == 0 ? std::stod( joints[ best ].first.substr( 5 ) ) : 0.0; + if( std::abs( tilt ) > 5.0 * 3.14159265358979323846 / 180.0 ) { + warn( "the strut \"" + strutpart.name + "\" is tilted by " + to_text( tilt * 180.0 / 3.14159265358979323846 ) + " degrees to meet the top bar" ); + } + // lina w siodle: położenie siodła na ramieniu i wysokość zaczepu dolnego + auto const along { rotate( toward_track, tilt ) }; + auto const offset { rotate( base + flatmin, tilt ) + rotate( vec2 { 0.0, ropez }, tilt ) }; + if( std::abs( along.y ) < 1e-6 ) { fail( "the strut \"" + strutpart.name + "\" stands vertical" ); } + auto const slide { ( rope.y - mount.y - offset.y ) / along.y }; + vec2 const strutpin { mount.y, rope.z - offset.z - along.z * slide }; + auto const flatlength { length( flatmax - flatmin ) }; + if( slide < -1e-3 ) { warn( "the messenger is " + to_text( -slide ) + " m short of the saddle slide on the strut \"" + strutpart.name + "\"" ); } + if( slide > flatlength + 1e-3 ) { warn( "the messenger is " + to_text( slide - flatlength ) + " m beyond the saddle slide on the strut \"" + strutpart.name + "\"" ); } + auto const lug { strutpin + rotate( body, tilt ) }; + vec2 const stay { upper.y, lug.z - joint.z }; + auto const barline { unit( vec2 { lug.y, stay.z } - stay ) }; + m_points[ "strut_pin" ] = strutpin; + m_points[ "strut_start" ] = strutpin + rotate( base, tilt ); + m_points[ "stay_pin" ] = stay; + m_points[ "stay_start" ] = stay + barline * topinsulator; + m_points[ "strut_eye" ] = lug; + m_points[ "saddle" ] = strutpin + rotate( base + flatmin, tilt ) + along * slide; + // wszystkie nazwane punkty ukośnika w układzie wysięgnika: strut. (np. strut.hole_2 - uchwyt linki 4230) + for( auto const &named : strutpart.points ) { + m_points[ "strut." + named.first ] = strutpin + rotate( base + vec2 { named.second.first, named.second.second }, tilt ); + } + make_line( "strut_insulator", strutpin, rotate( legdir, tilt ) ); + make_line( "tee", strutpin + rotate( base, tilt ), along ); + make_line( "stay", stay, barline ); + make_line( "leg", strutpin + rotate( base + slidemin, tilt ), rotate( legdir, tilt ) ).stations[ "end" ] = length( slidemax - slidemin ); + m_result.values[ "strut.drop" ] = mount.z - strutpin.z; + m_result.values[ "stay_mount.shift" ] = stay.z - upper.z; + m_result.values[ "strut.tilt" ] = tilt * 180.0 / 3.14159265358979323846; + m_result.values[ "messenger_clamp.slide" ] = slide; + return; + } + + if( op == "tee_bar" ) { + // teownik nieprzechylny z siodłem na pręcie górnym (0109 wyk. 1 i 5): pręt (rola stay) z izolatorem (stay_insulator) + // poziomo od zaczepu StayMount, siodło (messenger_clamp, punkt "rope") na pręcie w zakresie slide_horizontal_min..max, + // ukośnik (strut) z izolatorem (strut_insulator) jako sztywna bryła z zaczepu StrutMount, punkt ukośnika Point w punkcie + // "eye" pręta. Wysokość pręta z liny (stay_mount.shift), obrót bryły z odległości w poziomie, wysokość zaczepu dolnego + // z wysokości ucha pręta (strut.drop). Oś izolatora: slide_min..slide_max ukośnika albo prosta od zera do Point. + auto const mount { point( w[ 0 ] ) }; + auto const upper { point( w[ 1 ] ) }; + auto const rope { point( w[ 2 ] ) }; + auto const pointof = [&]( cantilever_part const &Part, std::string const &Name ) { + auto const found { Part.points.find( Name ) }; + if( found == Part.points.end() ) { fail( "part \"" + Part.name + "\" has no point \"" + Name + "\"" ); } + return vec2 { found->second.first, found->second.second }; + }; + auto const &strutpart { part( "strut" ) }; + auto const &barpart { part( "stay" ) }; + auto const joint { pointof( strutpart, w[ 3 ] ) }; + auto const axis { ( strutpart.points.count( "slide_min" ) && strutpart.points.count( "slide_max" ) ) + ? unit( pointof( strutpart, "slide_max" ) - pointof( strutpart, "slide_min" ) ) : unit( joint ) }; + auto const bareye { pointof( barpart, "eye" ) }; + auto const flatmin { pointof( barpart, "slide_horizontal_min" ) }; + auto const flatmax { pointof( barpart, "slide_horizontal_max" ) }; + auto const ropez { pointof( part( "messenger_clamp" ), "rope" ).z }; + auto const insulator { part( "strut_insulator" ).length }; + auto const topinsulator { part( "stay_insulator" ).length }; + // pręt: oś na wysokości liny minus siodło + auto const barz { rope.z - ropez - flatmin.z }; + vec2 const stay { upper.y, barz }; + vec2 const barzero { upper.y - topinsulator, barz }; + auto const eye { barzero + bareye }; + // ukośnik: obrót bryły tak, żeby punkt Point wypadł w poziomie ucha pręta + auto const base { axis * insulator }; + auto const body { base + joint }; + auto const reach { length( body ) }; + auto const dy { eye.y - mount.y }; + if( std::abs( dy ) > reach ) { fail( "the strut \"" + strutpart.name + "\" can't reach the eye of the top bar \"" + barpart.name + "\"" ); } + auto const angle { std::acos( dy / reach ) }; // kierunek bryły nad poziomem, w górę + auto const tilt { std::remainder( angle - std::atan2( body.z, body.y ), 2.0 * 3.14159265358979323846 ) }; + vec2 const strutpin { mount.y, eye.z - reach * std::sin( angle ) }; + auto const slide { barzero.y + flatmin.y - rope.y }; + auto const flatlength { length( flatmax - flatmin ) }; + if( slide < -1e-3 ) { warn( "the messenger is " + to_text( -slide ) + " m short of the saddle slide on the bar \"" + barpart.name + "\"" ); } + if( slide > flatlength + 1e-3 ) { warn( "the messenger is " + to_text( slide - flatlength ) + " m beyond the saddle slide on the bar \"" + barpart.name + "\"" ); } + m_points[ "strut_pin" ] = strutpin; + m_points[ "strut_start" ] = strutpin + rotate( base, tilt ); + m_points[ "stay_pin" ] = stay; + m_points[ "stay_start" ] = barzero; + m_points[ "saddle" ] = vec2 { rope.y, barz + flatmin.z }; + for( auto const &named : strutpart.points ) { + m_points[ "strut." + named.first ] = strutpin + rotate( base + vec2 { named.second.first, named.second.second }, tilt ); + } + // nazwane punkty pręta w układzie wysięgnika: stay. (np. stay.hole_1 - uchwyt linki 4230 na końcu pręta) + for( auto const &named : barpart.points ) { + m_points[ "stay." + named.first ] = barzero + vec2 { named.second.first, named.second.second }; + } + make_line( "strut_insulator", strutpin, rotate( axis, tilt ) ); + make_line( "tee", strutpin + rotate( base, tilt ), rotate( toward_track, tilt ) ); + make_line( "stay", stay, toward_track ); + // zakres przesuwu na ukośniku (węzeł 4360 na 6103): człon "leg" od slide_min do slide_max, położenie "end" + if( strutpart.points.count( "slide_min" ) && strutpart.points.count( "slide_max" ) ) { + auto const slidemin { pointof( strutpart, "slide_min" ) }; + auto const slidemax { pointof( strutpart, "slide_max" ) }; + make_line( "leg", strutpin + rotate( base + slidemin, tilt ), rotate( unit( slidemax - slidemin ), tilt ) ).stations[ "end" ] = length( slidemax - slidemin ); + } + m_result.values[ "strut.drop" ] = mount.z - strutpin.z; + m_result.values[ "stay_mount.shift" ] = barz - upper.z; + m_result.values[ "strut.tilt" ] = tilt * 180.0 / 3.14159265358979323846; + m_result.values[ "messenger_clamp.slide" ] = slide; + return; + } + + if( op == "stretch" ) { + // część rozciągnięta między dwoma punktami, skalowana wzdłuż swojej osi spoczynkowej + auto role { w[ 0 ] }; + auto const optional { role[ 0 ] == '?' }; + if( optional ) { role.erase( 0, 1 ); } + auto const from { point( w[ 1 ] ) }; + auto const to { point( w[ 2 ] ) }; + auto const span { length( to - from ) }; + m_result.values[ role + ".span" ] = span; + if( optional && false == has_role( role ) ) { return; } + auto const &stretched { part( role ) }; + auto placement { place( role, stretched.name, from, unit( to - from ), rest_axis( w[ 3 ] ) ) }; + auto const axis { ( w[ 3 ][ 1 ] == 'y' ? 1 : 2 ) }; + if( stretched.length > 1e-6 ) { placement.scale[ axis ] = span / stretched.length; } + for( int across = 0; across < 3; ++across ) { if( across != axis ) { placement.scale[ across ] *= stretched.girth; } } + put( placement ); + return; + } + + if( op == "vee" ) { + // dwie jednakowe części rozciągnięte od punktu From do dwóch punktów po obu stronach punktu To, rozsuniętych + // wzdłuż toru (X) o +-Spread: linka w kształcie V. Druga część dostaje rolę _2 + auto role { w[ 0 ] }; + auto const optional { role[ 0 ] == '?' }; + if( optional ) { role.erase( 0, 1 ); } + auto const from { point( w[ 1 ] ) }; + auto const to { point( w[ 2 ] ) }; + auto const spread { number( w[ 3 ] ) }; + auto const legs { std::sqrt( spread * spread + dot( to - from, to - from ) ) }; + m_result.values[ role + ".span" ] = legs; + if( optional && false == has_role( role ) ) { return; } + auto const &stretched { part( role ) }; + for( auto const side : { -1.0, 1.0 } ) { + double const direction[ 3 ] { side * spread / legs, ( to.y - from.y ) / legs, ( to.z - from.z ) / legs }; + auto placement { place_3d( side < 0.0 ? role : role + "_2", stretched.name, from, direction, w[ 4 ] ) }; + auto const axis { ( w[ 4 ][ 1 ] == 'y' ? 1 : 2 ) }; + if( stretched.length > 1e-6 ) { placement.scale[ axis ] = legs / stretched.length; } + for( int across = 0; across < 3; ++across ) { if( across != axis ) { placement.scale[ across ] *= stretched.girth; } } + put( placement ); + } + return; + } + + if( op == "armropes" ) { + // liny ramion (105x, 125x, 127x): od punktu From do ucha (holder) każdego postawionego ramienia (rola zaczynająca się od arms, + // część z punktem holder) - przy parze dwie osobne liny. Linka obrócona o 90 stopni wokół własnej osi względem stretch + // (płaska strona pionowo). Role: Rope, Rope.2, ... + auto rope { w[ 0 ] }; + if( rope[ 0 ] == '?' ) { rope.erase( 0, 1 ); } + if( false == has_role( rope ) ) { return; } + auto const &cord { part( rope ) }; + auto const from2 { point( w[ 1 ] ) }; + double const from[ 3 ] { 0.0, from2.y, from2.z }; + std::vector arms; + for( auto const &placed : m_result.parts ) { + if( placed.role.rfind( "arms", 0 ) != 0 ) { continue; } + auto const *armpart { m_catalog.part( placed.part ) }; + if( armpart == nullptr || armpart->kind != "arms" || armpart->points.find( "holder" ) == armpart->points.end() ) { continue; } + arms.push_back( placed ); + } + for( std::size_t index = 0; index < arms.size(); ++index ) { + auto const &arm { arms[ index ] }; + auto const *armpart { m_catalog.part( arm.part ) }; + auto const &hold { armpart->points.at( "holder" ) }; + auto const foundx { armpart->points_x.find( "holder" ) }; + auto const holdx { foundx != armpart->points_x.end() ? foundx->second : 0.0 }; + double to[ 3 ]; + for( int row = 0; row < 3; ++row ) { + to[ row ] = arm.position[ row ] + arm.rotation[ row * 3 + 0 ] * holdx + arm.rotation[ row * 3 + 1 ] * hold.first + arm.rotation[ row * 3 + 2 ] * hold.second; + } + double d[ 3 ] { to[ 0 ] - from[ 0 ], to[ 1 ] - from[ 1 ], to[ 2 ] - from[ 2 ] }; + auto const span { std::sqrt( d[ 0 ] * d[ 0 ] + d[ 1 ] * d[ 1 ] + d[ 2 ] * d[ 2 ] ) }; + if( span < 1e-6 ) { continue; } + for( auto &value : d ) { value /= span; } + double x[ 3 ] { 1.0 - d[ 0 ] * d[ 0 ], -d[ 0 ] * d[ 1 ], -d[ 0 ] * d[ 2 ] }; + auto const size { std::sqrt( x[ 0 ] * x[ 0 ] + x[ 1 ] * x[ 1 ] + x[ 2 ] * x[ 2 ] ) }; + for( auto &value : x ) { value /= size; } + double const y[ 3 ] { d[ 1 ] * x[ 2 ] - d[ 2 ] * x[ 1 ], d[ 2 ] * x[ 0 ] - d[ 0 ] * x[ 2 ], d[ 0 ] * x[ 1 ] - d[ 1 ] * x[ 0 ] }; + cantilever_placement placement; + placement.role = ( index == 0 ? rope : rope + "." + std::to_string( index + 1 ) ); + placement.part = cord.name; + for( int row = 0; row < 3; ++row ) { + placement.position[ row ] = from[ row ]; + // obrót o 90 stopni wokół osi linki: X części = dawne Y, Y części = -dawne X + placement.rotation[ row * 3 + 0 ] = y[ row ]; + placement.rotation[ row * 3 + 1 ] = -x[ row ]; + placement.rotation[ row * 3 + 2 ] = d[ row ]; + } + if( cord.length > 1e-6 ) { placement.scale[ 2 ] = span / cord.length; } + placement.scale[ 0 ] *= cord.girth; placement.scale[ 1 ] *= cord.girth; + m_result.values[ placement.role + ".span" ] = span; + put( placement ); + } + return; + } + + if( op == "armcords" ) { + // linki ramion odciągowych do liny nośnej (106x, 1054): na każdym postawionym ramieniu (rola zaczynająca się od arms) z punktem + // "cord_nom" obejma Clamp w tym punkcie (obrócona jak ramię, rura wzdłuż Y ramienia), z jej ucha linka Cord do punktu To + // (lina nośna). Jedno ramię: linka w V do dwóch punktów rozsuniętych wzdłuż toru o +-Spread; kilka ramion: każde + // pionowo do punktu To przesuniętego wzdłuż toru o +-Spread po stronie ramienia. Bez ramion z punktem cord_nom nic nie robi. + auto cordrole { w[ 0 ] }; + if( cordrole[ 0 ] == '?' ) { cordrole.erase( 0, 1 ); } + auto clamprole { w[ 1 ] }; + if( clamprole[ 0 ] == '?' ) { clamprole.erase( 0, 1 ); } + if( false == has_role( cordrole ) || false == has_role( clamprole ) ) { return; } + auto const &cord { part( cordrole ) }; + auto const &clamp { part( clamprole ) }; + auto const to { point( w[ 2 ] ) }; + auto const spread { number( w[ 3 ] ) }; + std::vector arms; + for( auto const &placed : m_result.parts ) { + if( placed.role.rfind( "arms", 0 ) != 0 ) { continue; } + auto const *armpart { m_catalog.part( placed.part ) }; + if( armpart == nullptr || armpart->points.find( "cord_nom" ) == armpart->points.end() ) { continue; } + arms.push_back( placed ); + } + if( arms.empty() ) { return; } + auto const transform = []( cantilever_placement const &Placement, double const X, double const Y, double const Z, double ( &Out )[ 3 ] ) { + for( int row = 0; row < 3; ++row ) { + Out[ row ] = Placement.position[ row ] + Placement.rotation[ row * 3 + 0 ] * X + Placement.rotation[ row * 3 + 1 ] * Y + Placement.rotation[ row * 3 + 2 ] * Z; + } + }; + auto const stretch = [ & ]( std::string const &Role, double const ( &From )[ 3 ], double const ( &To )[ 3 ] ) { + double d[ 3 ] { To[ 0 ] - From[ 0 ], To[ 1 ] - From[ 1 ], To[ 2 ] - From[ 2 ] }; + auto const span { std::sqrt( d[ 0 ] * d[ 0 ] + d[ 1 ] * d[ 1 ] + d[ 2 ] * d[ 2 ] ) }; + if( span < 1e-6 ) { return; } + for( auto &value : d ) { value /= span; } + // Z części wzdłuż linki, X części: oś toru bez składowej wzdłuż linki + double x[ 3 ] { 1.0 - d[ 0 ] * d[ 0 ], -d[ 0 ] * d[ 1 ], -d[ 0 ] * d[ 2 ] }; + auto const size { std::sqrt( x[ 0 ] * x[ 0 ] + x[ 1 ] * x[ 1 ] + x[ 2 ] * x[ 2 ] ) }; + for( auto &value : x ) { value /= size; } + double const y[ 3 ] { d[ 1 ] * x[ 2 ] - d[ 2 ] * x[ 1 ], d[ 2 ] * x[ 0 ] - d[ 0 ] * x[ 2 ], d[ 0 ] * x[ 1 ] - d[ 1 ] * x[ 0 ] }; + cantilever_placement placement; + placement.role = Role; + placement.part = cord.name; + for( int row = 0; row < 3; ++row ) { + placement.position[ row ] = From[ row ]; + placement.rotation[ row * 3 + 0 ] = x[ row ]; + placement.rotation[ row * 3 + 1 ] = y[ row ]; + placement.rotation[ row * 3 + 2 ] = d[ row ]; + } + if( cord.length > 1e-6 ) { placement.scale[ 2 ] = span / cord.length; } + placement.scale[ 0 ] *= cord.girth; placement.scale[ 1 ] *= cord.girth; + m_result.values[ Role + ".span" ] = span; + put( placement ); + }; + for( std::size_t index = 0; index < arms.size(); ++index ) { + auto const &arm { arms[ index ] }; + auto const *armpart { m_catalog.part( arm.part ) }; + auto const &nominal { armpart->points.at( "cord_nom" ) }; + auto const foundx { armpart->points_x.find( "cord_nom" ) }; + double at[ 3 ]; + transform( arm, ( foundx != armpart->points_x.end() ? foundx->second : 0.0 ), nominal.first, nominal.second, at ); + auto clampplacement { arm }; + clampplacement.role = clamprole + "." + std::to_string( index + 1 ); + clampplacement.part = clamp.name; + clampplacement.model.clear(); + std::fill( std::begin( clampplacement.scale ), std::end( clampplacement.scale ), 1.0 ); + std::copy( std::begin( at ), std::end( at ), std::begin( clampplacement.position ) ); + put( clampplacement ); + double eye[ 3 ]; + transform( clampplacement, 0.0, 0.0, clamp.eye, eye ); + auto const suffix { "." + std::to_string( index + 1 ) }; + if( arms.size() == 1 ) { + for( auto const side : { -1.0, 1.0 } ) { + double const target[ 3 ] { side * spread, to.y, to.z }; + stretch( cordrole + suffix + ( side < 0.0 ? "" : "_2" ), eye, target ); + } + } + else { + double const target[ 3 ] { ( eye[ 0 ] < 0.0 ? -spread : spread ), to.y, to.z }; + stretch( cordrole + suffix, eye, target ); + } + } + return; + } + + if( op == "setarms" ) { + // zestaw ramion bez wspornika na rurze (ramiona na linie, 105x, 125x): zestaw zerem w punkcie przewodu, Side jak w hangarms; + // punkt zawieszenia (holder) zestawu trafia do punktu _holder. Bez roli zestawu nic nie robi + auto setrole { w[ 0 ] }; + if( setrole[ 0 ] == '?' ) { setrole.erase( 0, 1 ); } + if( false == has_role( setrole ) ) { return; } + auto const &set { part( setrole ) }; + auto const flip { point( w[ 1 ] ).y < 0.0 }; + auto const found { set.points.find( "holder" ) }; + if( found == set.points.end() ) { fail( "part \"" + set.name + "\" has no \"holder\" point" ); } + put_arm_set( setrole, set, flip ); + m_points[ setrole + "_holder" ] = m_points[ "wire" ] + vec2 { ( flip ? -1.0 : 1.0 ) * found->second.first, found->second.second }; + return; + } + + if( op == "hangarms" ) { + // zestaw ramion odciągowych: gotowy model z zerem w osi przewodu jezdnego i punktem "holder" (zawieszenie na wsporniku); + // wspornik na rurze członu Line: zero na spodzie rury, wprost nad punktem zawieszenia. Wspornik z punktami + // slide_min/slide_max (4240) ma zwis regulowany płynnie i głowicę (rola Head, punkt "eye" w głowicy); wspornik z punktem + // "eye" (4250, 4260) jest stały. Zestaw po stronie toru przeciwnej słupowi (punkt Side z Y < 0) jest obrócony o 180 stopni. + // Bez roli zestawu w schemacie klocek nic nie robi. + auto setrole { w[ 0 ] }; + if( setrole[ 0 ] == '?' ) { setrole.erase( 0, 1 ); } + if( false == has_role( setrole ) ) { return; } + auto const &set { part( setrole ) }; + auto const &bracket { part( w[ 1 ] ) }; + auto headrole { w[ 2 ] }; + if( headrole[ 0 ] == '?' ) { headrole.erase( 0, 1 ); } + auto const *head { has_role( headrole ) ? &part( headrole ) : nullptr }; + auto const &tube { line_named( w[ 3 ] ) }; + auto const flip { point( w[ 4 ] ).y < 0.0 }; + auto const side { flip ? -1.0 : 1.0 }; + auto const lookup = [ & ]( cantilever_part const &Part, char const *Name ) -> std::pair const * { + auto const found { Part.points.find( Name ) }; + return ( found != Part.points.end() ? &found->second : nullptr ); + }; + auto const *hold { lookup( set, "holder" ) }; + if( hold == nullptr ) { fail( "part \"" + set.name + "\" has no \"holder\" point" ); } + // punkt zawieszenia względem przewodu, przy którym stoi zestaw (dwie sieci: przewód sieci B przestawiony na "wire") + vec2 const hang { m_points[ "wire" ].y + side * hold->first, m_points[ "wire" ].z + hold->second }; + // zero wspornika: przesunięte od punktu zawieszenia o ucho głowicy (w głowicy ucho w -Y, po obróceniu w +Y) + auto offset { 0.0 }; + if( head != nullptr ) { + auto const *eye { lookup( *head, "eye" ) }; + if( eye != nullptr ) { offset = -side * eye->first; } + } + auto const bracket_y { hang.y + offset }; + // punkt na spodzie rury nad tym miejscem + if( std::abs( tube.direction.y ) < 1e-6 ) { fail( "line \"" + w[ 3 ] + "\" is vertical, the arm holder can't be placed" ); } + auto const radius { has_role( w[ 3 ] ) ? part( w[ 3 ] ).radius : 0.019 }; + // wspornik siedzi na rurze i przechyla się razem z nią (pochylony wysięg 6115 w 0194, Jan 2026-10-18): oś Y wspornika wzdłuż + // rury w stronę słupa, Z prostopadle w górę; wspornik ze stałym uchem (bez zwisu i otworów) stawiany tak, żeby ucho wypadło + // w poziomie punktu zawieszenia. Przy rurze poziomej wszystko jak dawniej + vec2 const sup { tube.direction.y < 0.0 ? vec2 { -tube.direction.y, -tube.direction.z } : tube.direction }; + vec2 const up { -sup.z, sup.y }; + auto const *fixedeye { ( lookup( bracket, "slide_min" ) == nullptr && lookup( bracket, "hole_1" ) == nullptr ) ? lookup( bracket, "eye" ) : nullptr }; + auto along { ( bracket_y - tube.origin.y ) / tube.direction.y }; + if( fixedeye != nullptr && std::abs( up.y ) > 1e-9 ) { + along = ( bracket_y - tube.origin.y + up.y * radius - sup.y * side * fixedeye->first - up.y * fixedeye->second ) / tube.direction.y; + } + auto const axis { tube.at( along ) }; + vec2 const zero { axis.y - up.y * radius, axis.z - up.z * radius }; + auto const turn = [ & ]( cantilever_placement &Placement ) { + if( flip ) { for( int column : { 0, 1 } ) { for( int row = 0; row < 3; ++row ) { Placement.rotation[ row * 3 + column ] = -Placement.rotation[ row * 3 + column ]; } } } + }; + put_arm_set( setrole, set, flip ); + auto const *slide_min { lookup( bracket, "slide_min" ) }; + auto const *slide_max { lookup( bracket, "slide_max" ) }; + auto drop { zero.z - hang.z }; + // ucho wspornika ze stałym uchem na pochylonej rurze: wysokość ucha liczona po przechyle + if( fixedeye != nullptr ) { drop = zero.z + up.z * fixedeye->second - fixedeye->second - hang.z; } + if( slide_min != nullptr && slide_max != nullptr ) { + auto const low { -slide_min->second }; + auto const high { -slide_max->second }; + if( drop < low - 1e-3 || drop > high + 1e-3 ) { + warn( "arm holder \"" + bracket.name + "\" would need " + to_text( drop ) + " m between the tube and the arms, its range is " + to_text( low ) + "-" + to_text( high ) + " m" ); + } + m_result.values[ setrole + ".slide" ] = drop; + if( head != nullptr ) { + auto headplacement { place( headrole, head->name, vec2 { bracket_y, hang.z }, toward_support, toward_support ) }; + turn( headplacement ); + put( headplacement ); + } + } + else if( lookup( bracket, "hole_1" ) != nullptr ) { + // wspornik z otworami (4290): głowica w otworze najbliższym punktowi zawieszenia + std::pair const *best { nullptr }; + std::string bestname; + for( int hole = 1; hole < 20; ++hole ) { + auto const name { "hole_" + std::to_string( hole ) }; + auto const *found { lookup( bracket, name.c_str() ) }; + if( found == nullptr ) { break; } + if( best == nullptr || std::abs( drop + found->second ) < std::abs( drop + best->second ) ) { best = found; bestname = name; } + } + if( std::abs( drop + best->second ) > 0.01 ) { + warn( "no hole of arm holder \"" + bracket.name + "\" fits the arms, " + bestname + " is " + to_text( drop + best->second ) + " m off their hanging point" ); + } + m_result.values[ setrole + ".hole" ] = std::stod( bestname.substr( 5 ) ); + if( head != nullptr ) { + auto headplacement { place( headrole, head->name, vec2 { bracket_y, zero.z + best->second }, toward_support, toward_support ) }; + turn( headplacement ); + put( headplacement ); + } + } + else { + auto const *eye { lookup( bracket, "eye" ) }; + if( eye != nullptr && std::abs( drop + eye->second ) > 0.01 ) { + warn( "arm holder \"" + bracket.name + "\" holds the arms " + to_text( drop + eye->second ) + " m off their hanging point" ); + } + } + auto bracketplacement { place( w[ 1 ], bracket.name, zero, sup, toward_support ) }; + turn( bracketplacement ); + put( bracketplacement ); + // wspornik musi siedzieć na rurze + auto const *tubepart { has_role( w[ 3 ] ) ? &part( w[ 3 ] ) : nullptr }; + if( tubepart != nullptr && ( along < 0.0 || along > tubepart->length ) ) { + warn( "arm holder \"" + bracket.name + "\" falls " + to_text( along < 0.0 ? -along : along - tubepart->length ) + " m off the " + w[ 3 ] + " tube" ); + } + return; + } + + if( op == "registration" ) { + // ramiona odciągowe: stałe położenie względem przewodów jezdnych + if( m_registration == nullptr ) { fail( "unknown registration \"" + m_scheme.registration + "\"" ); } + for( auto const &item : m_registration->items ) { + cantilever_placement placement; + placement.role = "registration"; + placement.part = item.part; + std::copy( std::begin( item.position ), std::end( item.position ), std::begin( placement.position ) ); + std::copy( std::begin( item.rotation ), std::end( item.rotation ), std::begin( placement.rotation ) ); + m_result.parts.push_back( placement ); + } + return; + } + + fail( "unknown assembly step \"" + op + "\"" ); + } + + cantilever_catalog const &m_catalog; + cantilever_scheme const &m_scheme; + cantilever_input const &m_input; + cantilever_registration const *m_registration; + cantilever_result &m_result; + std::string m_label; + std::map m_points; + std::map m_lines; + +public: + // kierunek członu, gdy istnieje (do wyników zgodnych z dawnym solverem) + bool line_direction( std::string const &Name, vec2 &Direction ) const { + auto const lookup { m_lines.find( Name ) }; + if( lookup == m_lines.end() ) { return false; } + Direction = lookup->second.direction; + return true; + } +}; + +} // namespace + +namespace { +bool solve_cantilever_once( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_input const &Input, cantilever_result &Result ) { + + Result = {}; + auto const label { std::to_string( Scheme.card ) + "-" + std::to_string( Scheme.execution ) }; + auto const *assembly { Catalog.assembly( Scheme.assembly ) }; + if( assembly == nullptr ) { + Result.warnings.push_back( "cantilever " + label + ": unknown assembly \"" + Scheme.assembly + "\"" ); + return false; + } + auto const *registration { Scheme.registration.empty() ? nullptr : Catalog.registration( Scheme.registration ) }; + if( false == Scheme.registration.empty() && registration == nullptr ) { + Result.warnings.push_back( "cantilever " + label + ": unknown registration \"" + Scheme.registration + "\"" ); + return false; + } + assembler builder { Catalog, Scheme, Input, registration, Result }; + if( false == builder.run( assembly->steps ) ) { return false; } + + // wartości z nazwami używanymi przez wysięgniki przechylne + vec2 strut; + if( builder.line_direction( "strut", strut ) ) { Result.strut_angle = std::atan2( strut.z, -strut.y ); } + auto const value = [ & ]( char const *Name, double const Default ) { + auto const lookup { Result.values.find( Name ) }; + return ( lookup != Result.values.end() ? lookup->second : Default ); + }; + Result.strut_overhang = value( "strut.overhang", 0.0 ); + Result.steady_overhang = value( "steady.overhang", 0.0 ); + Result.stay_hole = static_cast( value( "stay_clamp.hole", -1.0 ) ); + Result.stay_gap = value( "stay_clamp.gap", 0.0 ); + Result.cord_length = value( "cord.span", 0.0 ); + return true; +} + +// podniesienia rury wysięgu do wypróbowania; zero, gdy schemat nie ma regulowanego uchwytu ramion +std::vector tube_raises( cantilever_scheme const &Scheme ) { + if( Scheme.tube_steps.empty() ) { return { 0.0 }; } + auto raises { Scheme.tube_steps }; + // od najbliższego preferowanemu; przy równej odległości niższe + std::stable_sort( raises.begin(), raises.end(), [ & ]( double const Left, double const Right ) { + return std::abs( Left - Scheme.tube_prefer ) < std::abs( Right - Scheme.tube_prefer ) - 1e-9; } ); + return raises; +} + +// wybór z kolejnych prób: najmniej ostrzeżeń, przy równej liczbie wcześniejsza (bliższa preferowanej) próba +template +bool solve_with_raises( cantilever_scheme const &Scheme, cantilever_result &Result, Attempt const &Solve ) { + auto const raises { tube_raises( Scheme ) }; + // wypadnięcie wspornika ramion poza rurę nie steruje wysokością rury: zmiana wysokości go nie usuwa, a przesuwa resztę + auto const scored = []( cantilever_result const &Tried ) { + std::size_t count { 0 }; + for( auto const &warning : Tried.warnings ) { if( warning.find( ": arm holder" ) == std::string::npos || warning.find( " tube" ) == std::string::npos || warning.find( " falls " ) == std::string::npos ) { ++count; } } + return count; }; + auto found { false }; + for( auto const raise : raises ) { + cantilever_result attempt; + auto const solved { Solve( raise, attempt ) }; + if( false == solved ) { if( false == found ) { Result = attempt; } continue; } + attempt.values[ "tube.raise" ] = raise; + if( false == found || scored( attempt ) < scored( Result ) ) { Result = attempt; found = true; } + if( scored( Result ) == 0 ) { break; } + } + return found; +} +} // namespace + +bool solve_cantilever( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_input const &Input, cantilever_result &Result ) { + + if( Scheme.tube_steps.empty() ) { return solve_cantilever_once( Catalog, Scheme, Input, Result ); } + auto const *registration { Catalog.registration( Scheme.registration ) }; + return solve_with_raises( Scheme, Result, [ & ]( double const Raise, cantilever_result &Attempt ) { + auto input { Input }; + if( registration != nullptr ) { input.points[ "registration" ] = { registration->clamp, registration->tube + Raise }; } + // zaczepy w schemacie odpowiadają podniesieniu preferowanemu + input.strut_mount.z += Raise - Scheme.tube_prefer; + return solve_cantilever_once( Catalog, Scheme, input, Attempt ); } ); +} + +cantilever_scene_part cantilever_to_scene( cantilever_placement const &Placement, double const Heading ) { + + cantilever_scene_part part; + // układ wysięgnika (X wzdłuż toru, Y do konstrukcji, Z w górę) a lokalny układ scenerii: x = -X, y = Z, z = Y. + // tak samo symulator przestawia osie wierzchołków modelu t3d, więc macierz obrotu części R przechodzi w C R C^T + auto const &r { Placement.rotation }; + double const rotation[ 3 ][ 3 ] { + { r[ 0 ], -r[ 2 ], -r[ 1 ] }, + { -r[ 6 ], r[ 8 ], r[ 7 ] }, + { -r[ 3 ], r[ 5 ], r[ 4 ] } }; + // rozkład na obroty Y, X, Z w kolejności, w jakiej składa je symulator: R = Ry Rx Rz + auto const pitch { -std::asin( std::clamp( rotation[ 1 ][ 2 ], -1.0, 1.0 ) ) }; + double yaw, roll; + if( std::abs( std::cos( pitch ) ) > 1e-6 ) { + yaw = std::atan2( rotation[ 0 ][ 2 ], rotation[ 2 ][ 2 ] ); + roll = std::atan2( rotation[ 1 ][ 0 ], rotation[ 1 ][ 1 ] ); + } + else { + // część ustawiona pionowo: obroty Y i Z się zlewają, cały idzie na Y + yaw = std::atan2( -rotation[ 2 ][ 0 ], rotation[ 0 ][ 0 ] ); + roll = 0.0; + } + auto const degrees { 180.0 / 3.14159265358979323846 }; + part.angles[ 0 ] = pitch * degrees; + part.angles[ 1 ] = ( yaw + Heading ) * degrees; + part.angles[ 2 ] = roll * degrees; + part.scale[ 0 ] = Placement.scale[ 0 ]; + part.scale[ 1 ] = Placement.scale[ 2 ]; + part.scale[ 2 ] = Placement.scale[ 1 ]; + // położenie: zamiana osi, potem obrót wokół Y o kierunek do konstrukcji + auto const x { -Placement.position[ 0 ] }; + auto const z { Placement.position[ 1 ] }; + auto const cosine { std::cos( Heading ) }; + auto const sine { std::sin( Heading ) }; + part.offset[ 0 ] = x * cosine + z * sine; + part.offset[ 1 ] = Placement.position[ 2 ]; + part.offset[ 2 ] = -x * sine + z * cosine; + return part; +} + +namespace { +bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_pole const &Pole, + double const Distance, double const RailHead, cantilever_point const &Messenger, std::map const &Overrides, + std::map const &Points, cantilever_umbrella const &Umbrella, cantilever_result &Result ); +} + +bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_pole const &Pole, + double const Distance, double const RailHead, cantilever_point const &Messenger, std::map const &Overrides, cantilever_result &Result ) { + return solve_cantilever_on_pole( Catalog, Scheme, Pole, Distance, RailHead, Messenger, Overrides, cantilever_umbrella(), Result ); +} + +void umbrella_mounts( cantilever_catalog const &Catalog, cantilever_pole const &Pole, cantilever_result const &First, cantilever_result const &Second, + cantilever_umbrella &FirstSide, cantilever_umbrella &SecondSide ) { + + FirstSide.shared.clear(); FirstSide.nuts.clear(); + SecondSide.shared.clear(); SecondSide.nuts.clear(); + std::map const roles { { "stay", &Pole.stay }, { "strut", &Pole.strut }, { "steady", &Pole.steady } }; + auto const height = [ & ]( cantilever_result const &Result, std::string const &Role, double &Height ) { + auto const *mount { Catalog.mount( *roles.at( Role ) ) }; + if( mount == nullptr || mount->twin.empty() ) { return false; } + auto const found { Result.values.find( "mount." + Role ) }; + if( found == Result.values.end() ) { return false; } + Height = found->second; + return true; + }; + struct candidate { double gap; std::string first; std::string second; int nutside; }; // nutside: 0 - First, 1 - Second + std::vector candidates; + auto const consider = [ & ]( std::string const &A, std::string const &B, int const Nutside ) { + double a, b; + if( false == height( First, A, a ) || false == height( Second, B, b ) ) { return; } + auto const gap { std::abs( a - b ) }; + if( gap > Catalog.rules.umbrella_spread + 1e-9 ) { return; } + candidates.push_back( { gap, A, B, Nutside } ); + }; + for( auto const *role : { "stay", "strut", "steady" } ) { consider( role, role, 1 ); } + consider( "strut", "stay", 1 ); // 4670: śruby po stronie ukośnika + consider( "stay", "strut", 0 ); + std::stable_sort( candidates.begin(), candidates.end(), []( candidate const &L, candidate const &R ) { return L.gap < R.gap; } ); + for( auto const &pair : candidates ) { + if( FirstSide.shared.count( pair.first ) || SecondSide.shared.count( pair.second ) ) { continue; } + double a, b; + height( First, pair.first, a ); + height( Second, pair.second, b ); + // średnia nie wychodzi ponad najwyższe położenie mocowania na słupie (odciąg ścięty do mount_max po jednej stronie) + auto shared { 0.5 * ( a + b ) }; + if( Pole.mount_max != 0.0 ) { shared = std::min( shared, std::max( Pole.mount_max, std::min( a, b ) ) ); } + FirstSide.shared[ pair.first ] = SecondSide.shared[ pair.second ] = shared; + if( pair.nutside == 0 ) { FirstSide.nuts.insert( pair.first ); } + else { SecondSide.nuts.insert( pair.second ); } + } +} + +bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_pole const &Pole, + double const Distance, double const RailHead, cantilever_point const &Messenger, std::map const &Overrides, + cantilever_umbrella const &Umbrella, cantilever_result &Result ) { + + auto const *registration { Catalog.registration( Scheme.registration ) }; + return solve_with_raises( Scheme, Result, [ & ]( double const Raise, cantilever_result &Attempt ) { + // schemat bez regulacji, z dolnym zaczepem ukośnika podniesionym razem z rurą wysięgu + auto scheme { Scheme }; + scheme.tube_steps.clear(); + scheme.tube_prefer = 0.0; + scheme.mount_strut += Raise - Scheme.tube_prefer; + std::map points; + if( registration != nullptr && Raise != 0.0 ) { points[ "registration" ] = { registration->clamp, registration->tube + Raise }; } + return solve_on_pole_once( Catalog, scheme, Pole, Distance, RailHead, Messenger, Overrides, points, Umbrella, Attempt ); } ); +} + +namespace { +bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_pole const &Pole, + double const Distance, double const RailHead, cantilever_point const &Messenger, std::map const &Overrides, + std::map const &Points, cantilever_umbrella const &Umbrella, cantilever_result &Result ) { + + Result = cantilever_result(); + auto const label { std::to_string( Scheme.card ) + "-" + std::to_string( Scheme.execution ) + " on pole \"" + Pole.name + "\"" }; + auto const fail = [ & ]( std::string const &Reason ) { Result.warnings.push_back( "cantilever " + label + ": " + Reason ); return false; }; + auto const mountpart = [ & ]( std::string const &Name, char const *Use ) -> cantilever_mount const * { + if( Name.empty() ) { fail( std::string( "the pole has no " ) + Use + " mount" ); return nullptr; } + auto const *mount { Catalog.mount( Name ) }; + if( mount == nullptr ) { fail( std::string( Use ) + " mount \"" + Name + "\" isn't in the catalogue" ); } + return mount; + }; + auto const *strut { mountpart( Pole.strut, "strut" ) }; + auto const *stay { mountpart( Pole.stay, "stay" ) }; + auto const *steady { Scheme.steady_mounted ? mountpart( Pole.steady, "steady arm" ) : nullptr }; + if( strut == nullptr || stay == nullptr || ( Scheme.steady_mounted && steady == nullptr ) ) { return false; } + + std::vector poleparts; + auto const add = [ & ]( std::string const &Role, std::string const &Part, std::string const &Model, double const Y, double const Z ) { + cantilever_placement placement; + placement.role = Role; + placement.part = Part; + placement.model = Model; + placement.position[ 1 ] = Y; + placement.position[ 2 ] = Z; + poleparts.push_back( placement ); + }; + std::vector notes; + // mocowanie z osią śrub na wysokości Bolts; zwraca punkt zaczepu izolatora + std::map heights; + auto const fix = [ & ]( std::string const &Role, cantilever_mount const &Mount, double Bolts ) { + // parasol: mocowanie dwustronne na wspólnej wysokości, bez tyłu; po stronie z nakrętkami jego połówka "twin" + auto const shared { Umbrella.shared.find( Role ) }; + auto const twin { shared != Umbrella.shared.end() && false == Mount.twin.empty() }; + if( twin ) { Bolts = RailHead + shared->second; } + auto const height { Bolts - RailHead }; + heights[ Role ] = height; + if( height > Pole.top ) { notes.push_back( Role + " mount " + to_text( height - Pole.top ) + " m above the top of the pole" ); } + auto const depth { Pole.depth( height ) }; + auto const face { Distance - 0.5 * depth }; + add( Role + "_mount", ( twin && Umbrella.nuts.count( Role ) ) ? Mount.twin : Mount.name, "", face, Bolts ); + if( false == twin && false == Mount.back.empty() ) { add( Role + "_back", Mount.back, "", face + depth, Bolts ); } + if( false == Mount.hinge.empty() ) { add( Role + "_hinge", Mount.hinge, "", face + Mount.point[ 0 ], Bolts + Mount.point[ 1 ] ); } + if( false == Mount.rod.empty() ) { add( Role + "_rod", Mount.rod, "", face + Mount.point[ 0 ], Bolts + Mount.point[ 1 ] ); } + return cantilever_point { face + Mount.attach_y(), Bolts + Mount.attach_z() }; + }; + + // słup bramki stawia bramka + if( Umbrella.pole && false == Pole.gantry ) { + add( "pole", Pole.name, Pole.model, Distance, RailHead ); + if( Pole.turn != 0.0 ) { + // model obrócony wokół pionu (zwis na bramce: profil szerszym bokiem wzdłuż toru) + auto const angle { Pole.turn * 3.14159265358979323846 / 180.0 }; + auto const c { std::cos( angle ) }, s { std::sin( angle ) }; + double const turned[ 9 ] { c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0 }; + std::copy( std::begin( turned ), std::end( turned ), poleparts.back().rotation ); + } + // fundament ma to samo zero co słup (oś słupa na wysokości główki szyny) + if( false == Pole.foundation.empty() ) { add( "foundation", Pole.name + "_foundation", Pole.foundation, Distance, RailHead ); } + } + auto const raise { Messenger.z - Scheme.height }; + auto staybolts { Scheme.mount_stay + raise - stay->attach_z() }; + if( Pole.mount_max != 0.0 && staybolts - RailHead > Pole.mount_max ) { + notes.push_back( "the stay mount was lowered to the highest mount position of the pole (" + to_text( Pole.mount_max ) + " m above the rail head)" ); + staybolts = RailHead + Pole.mount_max; + } + // odciąg z góry (zaczep odciągu nad zaczepem ukośnika w schemacie), który po ścięciu do mount_max wypadłby na wysokości + // dolnego zaczepu albo niżej: konstrukcja wyszłaby odwrócona (101x na słupie 1701 - Jan 2026-10-15), więc nie składamy + { + auto const strutbolts { Scheme.mount_strut + ( Scheme.steady_mounted ? raise : 0.0 ) - strut->attach_z() }; + auto const designgap { ( Scheme.mount_stay + raise - stay->attach_z() ) - strutbolts }; + if( designgap > 0.3 && staybolts - strutbolts < 0.5 * designgap ) { + for( auto const ¬e : notes ) { Result.warnings.push_back( "cantilever " + label + ": " + note ); } + return fail( "the pole is too short: the stay mount would have to be " + to_text( Scheme.mount_stay + raise - stay->attach_z() - RailHead ) + " m above the rail head, the pole allows " + + to_text( Pole.mount_max ) + " m, which puts the stay at or below the strut; the cantilever isn't assembled" ); + } + } + cantilever_input input; + input.messenger = Messenger; + input.overrides = Overrides; + input.points = Points; + auto stayeye { fix( "stay", *stay, staybolts ) }; + input.strut_mount = fix( "strut", *strut, Scheme.mount_strut + ( Scheme.steady_mounted ? raise : 0.0 ) - strut->attach_z() ); + if( steady != nullptr ) { input.steady_mount = fix( "steady", *steady, Scheme.mount_steady - steady->attach_z() ); } + // zwis na bramce: mocowania nie mogą zejść poniżej jego końca + if( Pole.has_mount_min ) { + for( auto const &height : heights ) { + if( height.second < Pole.mount_min - 1e-6 ) { notes.push_back( height.first + " mount " + to_text( Pole.mount_min - height.second ) + " m below the lowest mount position of the pole" ); } + } + } + + // szekla odciągu wisi na uchu mocowania, a izolator na szekli: zaczep izolatora leży o długość szekli dalej wzdłuż odciągu, + // którego kierunek zależy od złożenia, więc dwa przebiegi + auto const shacklename { Overrides.count( "stay_shackle" ) ? Overrides.at( "stay_shackle" ) : ( Scheme.roles.count( "stay_shackle" ) ? Scheme.roles.at( "stay_shackle" ) : std::string() ) }; + auto const *shackle { shacklename.empty() ? nullptr : Catalog.part( shacklename ) }; + auto const shackle_length { shackle != nullptr ? shackle->length : 0.0 }; + input.stay_mount = { stayeye.y - shackle_length, stayeye.z }; + auto solved { solve_cantilever( Catalog, Scheme, input, Result ) }; + // zaczep odciągu przesunięty przez sposób składania (krok anchor): mocowanie na słupie idzie za nim + auto const shifted { Result.values.find( "stay_mount.shift" ) }; + if( solved && shifted != Result.values.end() && std::abs( shifted->second ) > 1e-4 ) { + auto const shift { shifted->second }; + poleparts.erase( std::remove_if( poleparts.begin(), poleparts.end(), []( cantilever_placement const &Placement ) { return Placement.role.compare( 0, 5, "stay_" ) == 0; } ), poleparts.end() ); + staybolts += shift; + if( Pole.mount_max != 0.0 && staybolts - RailHead > Pole.mount_max + 1e-6 ) { + notes.push_back( "the stay mount needs to be " + to_text( staybolts - RailHead - Pole.mount_max ) + " m above the highest mount position of the pole" ); + } + stayeye = fix( "stay", *stay, staybolts ); + input.stay_mount = { stayeye.y - shackle_length, stayeye.z }; + solved = solve_cantilever( Catalog, Scheme, input, Result ); + } + // zaczep ukośnika obniżony przez sposób składania (krok platform): mocowanie na słupie idzie za nim + auto const dropped { Result.values.find( "strut.drop" ) }; + if( solved && dropped != Result.values.end() && std::abs( dropped->second ) > 1e-4 ) { + auto const drop { dropped->second }; + poleparts.erase( std::remove_if( poleparts.begin(), poleparts.end(), []( cantilever_placement const &Placement ) { return Placement.role.compare( 0, 6, "strut_" ) == 0; } ), poleparts.end() ); + input.strut_mount = fix( "strut", *strut, Scheme.mount_strut + ( Scheme.steady_mounted ? raise : 0.0 ) - strut->attach_z() - drop ); + solved = solve_cantilever( Catalog, Scheme, input, Result ); + } + if( solved && shackle_length > 0.0 ) { + auto const *insulator { Result.find( "stay_insulator" ) }; + if( insulator == nullptr ) { insulator = Result.find( "stay" ); } // odciąg bez izolatora (peronowe): cięgno wprost na szekli + if( insulator != nullptr ) { + // kierunek odciągu do toru to -Y części + input.stay_mount = { stayeye.y - insulator->rotation[ 4 ] * shackle_length, stayeye.z - insulator->rotation[ 7 ] * shackle_length }; + solved = solve_cantilever( Catalog, Scheme, input, Result ); + } + } + for( auto const ¬e : notes ) { Result.warnings.push_back( "cantilever " + label + ": " + note ); } + for( auto const &height : heights ) { Result.values[ "mount." + height.first ] = height.second; } + if( false == solved ) { return false; } + // wysięgnik pomocniczy bez ukośnika i odciągu (0167, 0169 - tylko zaczep na wysokości przewodu): bez ich mocowań na słupie + if( Scheme.roles.count( "strut" ) == 0 && Scheme.roles.count( "stay" ) == 0 ) { + poleparts.erase( std::remove_if( poleparts.begin(), poleparts.end(), []( cantilever_placement const &Placement ) { + return Placement.role.compare( 0, 6, "strut_" ) == 0 || Placement.role.compare( 0, 5, "stay_" ) == 0; } ), poleparts.end() ); + Result.values.erase( "mount.stay" ); + Result.values.erase( "mount.strut" ); + } + Result.parts.insert( Result.parts.end(), poleparts.begin(), poleparts.end() ); + return true; +} +} // namespace + +} // namespace catenary diff --git a/world/catenary_cantilever.h b/world/catenary_cantilever.h new file mode 100644 index 00000000..a9b540ae --- /dev/null +++ b/world/catenary_cantilever.h @@ -0,0 +1,321 @@ +// Sieć trakcyjna: wysięgnik składany z części katalogowych. +// Katalog (części, zestawy ramion odciągowych, schematy kart, sposoby składania) jest czytany z pliku danych. +// Solver nie zna żadnego konkretnego wysięgnika: składa go z klocków połączeń (człon z punktu w kierunku, styczna do +// ucha, uchwyt na danej wysokości, dobór skokowy długości, część wisząca w linii z linką...) w kolejności zapisanej +// w bloku "assembly" pliku danych. Opis klocków jest w komentarzu nad blokami w pliku danych. +// Kod niezależny od reszty symulatora (C++17, tylko biblioteka standardowa). +// +// Układ wysięgnika: zero w osi przewodów jezdnych przy podwieszeniu, Y poziomo w stronę konstrukcji wsporczej, +// Z w górę, X wzdłuż toru. Wszystkie wymiary w metrach. +// +// Układ części liczonych przez solver: model części jest ułożony tak, jak część siedzi w wysięgniku o poziomych rurach +// (Y w stronę konstrukcji, Z w górę, sworznie wzdłuż X), z punktem zerowym w głównym zaczepie. Solver obraca część +// wokół osi X tylko o tyle, o ile jej rzeczywisty kierunek różni się od spoczynkowego: +// izolatory 7xxx zero w zaczepie przy konstrukcji, biegną w -Y (do toru) +// izolator odciągu wiszący na szekli 4700 solver dodatkowo obraca o 90 stopni wokół osi Y +// ukośnik 6001 zero w osi otworu sworznia przy izolatorze, biegnie w -Y +// odciąg 6007 zero w osi ucha przy izolatorze, biegnie w -Y +// końcówka 4400 zero w osi ucha przy uchwycie 4070, biegnie w +Y (rura z otworami w stronę konstrukcji) +// szekla 4700 zero od strony mocowania, biegnie w -Y do punktu "eye" (sworzeń w uchu izolatora) +// wysięg pomocniczy 6011: zero w osi haka (na uchu leży punkt "hook" z danych), 6111: w osi pierwszego otworu; biegnie w -Y +// uchwyt 4050 zero w osi rury, ucho w -Z (w dół); "eye" to krawędź ucha, na której leży hak +// uchwyt 4070 zero w osi rury, sworzeń w +Z (w górę) +// obejma linki 4210 zero w osi rury, śruba linki w +Z +// uchwyt liny 4140 zero w osi liny nośnej, punkt "hook" (ucho uchwytu 4050) nad nim, "end" (linka) pod nim +// linka 4490 zero w dolnym zaczepie (przy obejmie 4210), biegnie w +Z; skalowana wzdłuż Z +// Części zestawu ramion odciągowych mają położenie i obrót przeniesione wprost z pliku danych. +#pragma once +#include +#include +#include +#include + +namespace catenary { + +// składnik zestawu ramion złożonego z osobnych modeli (np. para 6207-1 + 6207-2 z dwoma 4110): położenie zera modelu +// względem zera zestawu (osi przewodu odniesienia) i obrót wokół pionu, jak w złożeniu z Blendera +struct cantilever_member { + std::string part; + double position[ 3 ] { 0.0, 0.0, 0.0 }; // X wzdłuż toru, Y w stronę konstrukcji, Z w górę + double yaw { 0.0 }; // stopnie, wokół Z, dodatni przeciwnie do wskazówek zegara patrząc z góry +}; + +// wymiary części potrzebne mechanice +struct cantilever_part { + std::string name; // numer katalogowy, zarazem nazwa modelu + std::string kind; // strut, stay, stay_end, steady, clamp, messenger_clamp, insulator, shackle, cord + double length { 0.0 }; // od głównego zaczepu do końca albo do osi drugiego połączenia + double clearance { 0.0 }; // izolator: dodatkowy odstęp za "length", którego nie może zająć uchwyt na rurze (gruby koniec izolatora 25 kV) + double pin { 0.0 }; // rura: odległość osi otworu sworznia od końca rury + double radius { 0.019 }; // rura: promień (6011, 6111: 38 mm; 6001, 6002, 6003: 60 mm - Jan) + double eye { 0.0 }; // uchwyt: odległość ucha od osi rury + double hook[ 2 ] { 0.0, 0.0 }; // punkt, którym część wisi na uchu uchwytu 4050 (Y, Z w układzie części): uchwyt liny, hak wysięgu 6011 + double end[ 2 ] { 0.0, 0.0 }; // uchwyt liny: punkt mocowania linki (Y, Z w układzie części) + std::vector holes; // końcówka odciągu: odległości otworów od osi ucha + std::map> points; // nazwane punkty modelu (Y, Z w układzie części), np. holder, eye, slide_min + std::vector members; // zestaw ramion z osobnych modeli: składniki stawiane zamiast samego zestawu + std::map points_x; // składowa X (wzdłuż toru) nazwanych punktów, gdy model ją ma (ramiona 6207/6208: obejma 4210, ucho) + std::string model; // inny plik modelu niż numer katalogowy (lina 9832-2 = model linki 4490-00), pusty - jak name + double girth { 1.0 }; // część rozciągana (linka): skala w poprzek osi (grubsza lina z modelu cieńszej linki) +}; + +// część o stałym położeniu względem przewodów jezdnych +struct cantilever_item { + std::string part; + double position[ 3 ] { 0.0, 0.0, 0.0 }; + double rotation[ 9 ] { 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 }; // wierszami, kolumny to osie części +}; + +// zestaw ramion odciągowych jednego wykonania +struct cantilever_registration { + std::string name; + double tube { 0.22 }; // wysokość osi wysięgu pomocniczego nad przewodem jezdnym + double clamp { 0.17 }; // nominalne położenie Y obejmy linki na wysięgu pomocniczym + std::vector items; +}; + +// schemat: jedna karta katalogowa w jednym wykonaniu +struct cantilever_scheme { + int card { 0 }; + int execution { 0 }; + double reach_min { 0.0 }; // pasmo A z karty: od osi przewodów do czoła konstrukcji, razem z mocowaniem + double reach_max { 0.0 }; + double height { 1.70 }; // wysokość konstrukcyjna + double mount_stay { 0.0 }; // nominalne wysokości zaczepów izolatorów nad przewodem jezdnym + double mount_strut { 0.0 }; + double mount_steady { 0.0 }; + bool steady_mounted { false };// wysięg pomocniczy ma własny izolator przy konstrukcji (karty z trzema zaczepami) + std::string registration; // nazwa zestawu ramion odciągowych + std::string assembly; // sposób składania (blok "assembly") + std::vector tube_steps; // regulowany uchwyt ramion (np. 4290): możliwe podniesienia rury wysięgu względem zestawu + // ramion; dolny zaczep ukośnika idzie w górę razem z rurą. Solver bierze najniższe, przy którym + // wysięgnik składa się bez ostrzeżeń (albo z najmniejszą ich liczbą), próbując od najbliższego + double tube_prefer { 0.0 }; // podniesieniu preferowanemu (np. położeniu ze złożeń) + std::map roles; // rola -> część +}; + +// ustawienia montażowe, wspólne dla wszystkich schematów +struct cantilever_rules { + double stay_gap_min { 0.10 }; // odstęp uchwytu odciągu od uchwytu głównego wzdłuż ukośnika: najmniejszy, + double stay_gap { 0.15 }; // zalecany + double stay_gap_max { 0.30 }; // i największy (z kart: "max 0,30 m") + double clamp_margin { 0.03 }; // ile rury musi zostać za osią uchwytu + double cord_clamp_inset { 0.0 }; // najmniejsza odległość osi obejmy linki od końca wysięgu pomocniczego + double umbrella_spread { 0.10 }; // parasol: największa różnica wysokości mocowań obu wysięgników, przy której idą na wspólne mocowanie +}; + +// słup: wymiary potrzebne do położenia czoła na dowolnej wysokości i mocowania, które się na nim stosuje. +// wysokości liczone od główki szyny; zero modelu słupa w jego osi na wysokości główki szyny +struct cantilever_pole { + std::string name; + std::string model; // ścieżka modelu względem katalogu modeli symulatora, bez rozszerzenia + std::string foundation; // model fundamentu (ścieżka jak wyżej) stawiany w tym samym zerze co słup; puste = bez fundamentu + double top { 0.0 }; // góra słupa nad główką szyny + double length { 0.0 }; // długość, na której biegnie zbieżność, liczona od góry + double depth_top { 0.0 }; // wymiar od toru w głąb (prostopadle do toru) u góry + double depth_base { 0.0 }; // to samo na końcu długości zbieżności + double mount_max { 0.0 }; // najwyżej położona oś śrub mocowania; 0 = bez ograniczenia (zwis na bramce: ujemna, pod dźwigarem) + double mount_min { 0.0 }; // najniżej położona oś śrub mocowania (zwis: nad jego końcem); tylko gdy has_mount_min + bool has_mount_min { false }; + double turn { 0.0 }; // obrót modelu wokół pionu względem płaszczyzny wysięgnika [stopnie] (zwis: 90) + std::string drop; // zwis na bramce: rodzina zwisów (np. 4850), do której pasuje dźwigar; pusta = zwykły słup + std::string strut; // mocowanie ukośnika (część z punktem axis i zawiasem) + std::string stay; // mocowanie izolatora odciągu + std::string steady; // mocowanie izolatora wysięgu pomocniczego (schematy z trzema zaczepami) + // słup bramki z wysięgnikiem (zrobiony z wpisu "mast" z mocowaniami): model stawia bramka, nie wysięgnik; mount_max pod + // dźwigarem bramki liczy sceneria wg jej poziomu (mast_low/mast_high minus below_beam), tu - pod dolnym mocowaniem + bool gantry { false }; + double mast_low { 0.0 }; + double mast_high { 0.0 }; + double below_beam { 0.15 }; + // głębokość słupa na wysokości Height nad główką szyny + double depth( double Height ) const; +}; + +// mocowanie do słupa: przód z punktem zaczepu, opcjonalnie tył po drugiej stronie słupa, zawias albo pręt z uchem. +// układ jak w pozostałych częściach: zero na płaszczyźnie przylegania do słupa w połowie między śrubami, słup w +Y +struct cantilever_mount { + std::string name; + std::string back; // plik tyłu (płaskownik z nakrętkami), stawiany na tylnej ścianie słupa + std::string hinge; // zawias obrotowy stawiany w punkcie axis (wspornik ukośnika) + std::string rod; // pręt z uchem stawiany w punkcie eye (uchwyt odciągu) + std::string twin; // połówka z nakrętkami mocowania dwustronnego (parasol); pusta = mocowanie bez regulacji, parasol z dwóch niezależnych + double point[ 2 ] { 0.0, 0.0 }; // Y, Z punktu axis (gdy jest zawias) albo eye + double hinge_eye[ 2 ] { 0.0, 0.0 }; // Y, Z ucha zawiasu względem jego zera + // punkt zaczepu izolatora względem zera mocowania + double attach_y() const { return point[ 0 ] + hinge_eye[ 0 ]; } + double attach_z() const { return point[ 1 ] + hinge_eye[ 1 ]; } +}; + +// jeden klocek sposobu składania: słowo kluczowe i jego argumenty, jak w pliku danych +struct cantilever_step { + std::string op; + std::vector words; + std::size_t end { 0 }; // "repeat": indeks kroku za pasującym "endrepeat" +}; + +// sposób składania wysięgnika: klocki w kolejności wykonywania +struct cantilever_assembly { + std::string name; + std::vector steps; +}; + +// katalog wysięgników wczytany z pliku danych +// słup bramki: model z zerem w osi słupa na wysokości główki szyny, Y modelu wzdłuż dźwigara, X wzdłuż toru +struct gantry_mast { + std::string name; + std::string model; + double width { 0.0 }; // wzdłuż toru + double length { 0.0 }; // wzdłuż dźwigara + double height { 0.0 }; + double mount_low { 0.0 }; // prześwit (spód dźwigara nad główką szyny) w dolnym i górnym mocowaniu + double mount_high { 0.0 }; + double offset { 0.0 }; // przesunięcie dźwigara wzdłuż toru względem osi słupa + bool through { false }; // przelotowy (dźwigary z obu stron); false = końcowy + // wysięgnik wprost na słupie bramki: mocowania (karta 53-0501 wyk. 04 - słupy bramek o rozstawie 400x600) i odstęp + // najwyższej osi śrub od spodu dźwigara; bez mocowań na słupie bramki nie da się postawić wysięgnika + std::string strut; + std::string stay; + std::string steady; + double below_beam { 0.15 }; +}; +// dźwigar bramki: model z zerem w osi pierwszego słupa na spodzie dźwigara, X modelu wzdłuż dźwigara +struct gantry_beam { + std::string name; + std::string model; + double span { 0.0 }; // rozstaw osi słupów; pojedynczy element: długość od ściany słupa + std::string drop; // rodzina pasujących zwisów + bool single { false }; // pojedynczy element na jednym słupie przelotowym (3017-3025, 1910): z jednej strony + // mocowanie do słupa, z drugiej do konsoli +}; + +// rozstaw osi słupów bramki: między ścianami słupów o 0,20 m mniej niż numer dźwigara (E101: 9,9 m), do osi dochodzą offsety słupów +inline double gantry_spacing( gantry_beam const &Beam, gantry_mast const &First, gantry_mast const &Second ) { + return Beam.span - 0.20 + First.offset + Second.offset; +} + +class cantilever_catalog { +public: + // rozbiera plik podany jako kolejne słowa (już bez komentarzy); błędy trafiają do Errors + void parse( std::vector const &Tokens, std::string const &Source, std::vector &Errors ); + // wczytuje plik; zwraca false, gdy pliku nie da się otworzyć + bool load( std::string const &Filename, std::vector &Errors ); + cantilever_part const *part( std::string const &Name ) const; + cantilever_registration const *registration( std::string const &Name ) const; + cantilever_scheme const *scheme( int Card, int Execution ) const; + cantilever_pole const *pole( std::string const &Name ) const; + cantilever_mount const *mount( std::string const &Name ) const; + cantilever_assembly const *assembly( std::string const &Name ) const; + // karta z tej samej dziesiątki co Card, której pasmo A obejmuje podaną odległość; nullptr gdy żadna + cantilever_scheme const *scheme_for_reach( int Card, int Execution, double Reach ) const; + gantry_mast const *mast( std::string const &Name ) const; + gantry_beam const *beam( std::string const &Name ) const; + std::map const &masts() const { return m_masts; } + std::map const &beams() const { return m_beams; } + // katalog modeli części względem katalogu modeli symulatora, z ukośnikiem na końcu (wpis "models" w pliku danych) + std::string const &models() const { return m_models; } + std::size_t part_count() const { return m_parts.size(); } + std::size_t scheme_count() const { return m_schemes.size(); } + std::size_t registration_count() const { return m_registrations.size(); } + std::map, cantilever_scheme> const &schemes() const { return m_schemes; } + std::map const &parts() const { return m_parts; } + std::map const &poles() const { return m_poles; } + cantilever_rules rules; + +private: + std::string m_models { "siec_trakcyjna/components/" }; + std::map m_parts; + std::map m_registrations; + std::map, cantilever_scheme> m_schemes; + std::map m_poles; + std::map m_mounts; + std::map m_assemblies; + std::map m_masts; + std::map m_beams; +}; + +// punkt w płaszczyźnie wysięgnika +struct cantilever_point { double y { 0.0 }; double z { 0.0 }; }; + +// dane jednego podwieszenia +struct cantilever_input { + cantilever_point stay_mount; // zaczep izolatora odciągu przy konstrukcji (po odjęciu mocowania i szekli) + cantilever_point strut_mount; // zaczep izolatora ukośnika + cantilever_point steady_mount; // zaczep izolatora wysięgu pomocniczego; tylko dla schematów z trzema zaczepami + cantilever_point messenger; // oś liny nośnej + std::map overrides; // podmiany części: rola -> część + std::map points; // dodatkowe punkty wejściowe dla sposobu składania (nazwa -> punkt) +}; + +// nominalne dane podwieszenia: zaczepy na wysokościach ze schematu, w odległości Reach od osi przewodów, +// lina nośna nad przewodami na wysokości konstrukcyjnej schematu +cantilever_input cantilever_nominal_input( cantilever_scheme const &Scheme, double Reach ); + +// jedna część złożonego wysięgnika +struct cantilever_placement { + std::string role; + std::string part; + std::string model; // ścieżka modelu względem katalogu modeli symulatora, gdy część leży poza katalogiem części (słup) + double position[ 3 ] { 0.0, 0.0, 0.0 }; + double rotation[ 9 ] { 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 }; // wierszami, kolumny to osie części + double scale[ 3 ] { 1.0, 1.0, 1.0 }; +}; + +struct cantilever_result { + std::vector parts; + std::vector warnings; // czego nie dało się złożyć zgodnie z zasadami; części są mimo to ustawione + double strut_angle { 0.0 }; // rad, nachylenie ukośnika do poziomu + double strut_overhang { 0.0 }; // ile ukośnika wystaje za osią uchwytu głównego + int stay_hole { -1 }; // wybrany otwór końcówki odciągu, licząc od ucha + double stay_gap { 0.0 }; // odstęp uchwytu odciągu od uchwytu głównego wzdłuż ukośnika + double steady_overhang { 0.0 }; // ile wysięgu pomocniczego wystaje za osią obejmy linki + double cord_length { 0.0 }; // długość linki między zaczepami + std::map values; // wartości nazwane przez klocki (np. "strut.overhang", "stay_clamp.hole") + cantilever_placement const *find( std::string const &Role ) const; +}; + +// składa wysięgnik. zwraca false, gdy brakuje danych (części albo zestawu ramion) albo geometria jest niemożliwa; +// przyczyna trafia wtedy do Result.warnings +bool solve_cantilever( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_input const &Input, cantilever_result &Result ); + +// wysięgnik na słupie. Układ jak w solve_cantilever (zero w osi przewodów jezdnych, Y poziomo w stronę słupa, Z w górę); +// oś słupa leży w Y = Distance, główka szyny na Z = RailHead (zwykle ujemnym), lina nośna w Messenger. +// Zaczepy izolatorów biorą się z mocowań słupa: wysokości ze schematu (zaczep odciągu idzie za liną nośną, gdy ta wisi +// wyżej niż w schemacie), odległość od osi z głębokości słupa na wysokości mocowania i z punktów mocowania. Oś śrub +// mocowania odciągu nie wychodzi ponad mount_max słupa. Do Result.parts trafia też słup, mocowania, tyły, zawias i pręt. +bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_pole const &Pole, + double Distance, double RailHead, cantilever_point const &Messenger, std::map const &Overrides, cantilever_result &Result ); + +// parasol: drugi wysięgnik na tym samym słupie, po przeciwnej stronie. Wysokości mocowań (stay, strut, steady) liczone od główki +// szyny; mocowanie z listy Shared idzie na podaną wysokość jako połówka mocowania dwustronnego (bez tyłu; role z Nuts dostają +// połówkę z nakrętkami, czyli "twin" mocowania). Wysokości wyliczone przez solver trafiają do Result.values jako "mount.". +struct cantilever_umbrella { + std::map shared; // rola mocowania -> wspólna wysokość osi śrub nad główką szyny + std::set nuts; // role mocowań, które po tej stronie są połówką z nakrętkami + bool pole { true }; // czy stawiać słup (stawia go tylko jedna strona) +}; +bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_scheme const &Scheme, cantilever_pole const &Pole, + double Distance, double RailHead, cantilever_point const &Messenger, std::map const &Overrides, + cantilever_umbrella const &Umbrella, cantilever_result &Result ); +// wspólne mocowania parasola z wysokości wyliczonych osobno dla obu stron (wartości "mount." z wyników): pary mocowań +// z połówką z nakrętkami, których wysokości różnią się najwyżej o rules.umbrella_spread, idą na średnią wysokość. Ta sama rola +// po obu stronach (4640/4650/4660): nakrętki po stronie Second. Ukośnik jednej strony z odciągiem drugiej (4670, Jan +// 2026-10-17): śruby po stronie ukośnika (4610-NN), nakrętki po stronie odciągu (4650-NN). Pary brane od najmniejszej różnicy, +// każde mocowanie najwyżej raz. Średnia nie wychodzi ponad mount_max słupa (chyba że obie strony są wyżej - wtedy niższa). +// Wypełnia shared i nuts obu stron. +void umbrella_mounts( cantilever_catalog const &Catalog, cantilever_pole const &Pole, cantilever_result const &First, cantilever_result const &Second, + cantilever_umbrella &FirstSide, cantilever_umbrella &SecondSide ); + +// część ustawiona w układzie scenerii symulatora (X, Y w górę, Z), względem punktu osi przewodów jezdnych +struct cantilever_scene_part { + double offset[ 3 ] { 0.0, 0.0, 0.0 }; // przesunięcie od punktu osi przewodów jezdnych + double angles[ 3 ] { 0.0, 0.0, 0.0 }; // kąty jak w dopisku "angles" wpisu node model [stopnie]: obroty Y, potem X, potem Z + double scale[ 3 ] { 1.0, 1.0, 1.0 }; // skala jak w dopisku "scale" wpisu node model +}; + +// przelicza część z układu wysięgnika na układ scenerii. Heading to kierunek poziomy od osi przewodów do konstrukcji +// wsporczej, jako kąt wokół osi Y scenerii [rad]: atan2( x, z ) tego kierunku. +// uwzględnia zamianę osi, którą symulator robi przy wczytaniu modelu t3d (odwrócenie X, zamiana Y z Z) +cantilever_scene_part cantilever_to_scene( cantilever_placement const &Placement, double Heading ); + +} // namespace catenary diff --git a/world/catenary_catalog.cpp b/world/catenary_catalog.cpp new file mode 100644 index 00000000..51bf7cc5 --- /dev/null +++ b/world/catenary_catalog.cpp @@ -0,0 +1,255 @@ +/* +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 "world/catenary_catalog.h" +#include "world/catenary_cantilever.h" + +#include +#include +#include +#include +#include +#include + +namespace catenary { + +bool catalog_family::fits( bool const Curve, bool const Toward, int const Card ) const { + if( curve >= 0 && ( curve == 1 ) != Curve ) { return false; } + if( toward == 0 || toward == 1 ) { return ( toward == 1 ) == Toward; } + if( toward == 2 ) { return ( Card % 2 == 1 ) == Toward; } + return true; +} + +bool structure_catalog::load( std::string const &Filename, std::vector &Errors ) { + std::ifstream file { Filename }; + if( false == file.is_open() ) { return false; } + std::vector lines; + std::string line; + while( std::getline( file, line ) ) { + if( false == line.empty() && line.back() == '\r' ) { line.pop_back(); } + lines.push_back( line ); + } + parse( lines, Filename, Errors ); + return true; +} + +namespace { + +std::string trimmed( std::string const &Text ) { + auto const first { Text.find_first_not_of( " \t" ) }; + if( first == std::string::npos ) { return {}; } + auto const last { Text.find_last_not_of( " \t" ) }; + return Text.substr( first, last - first + 1 ); +} + +// karty: "1110-1119" (zakres) albo "0111" +bool cards_of( std::string const &Word, std::vector &Cards ) { + auto const dash { Word.find( '-' ) }; + try { + if( dash == std::string::npos ) { Cards.push_back( std::stoi( Word ) ); return true; } + auto const from { std::stoi( Word.substr( 0, dash ) ) }, to { std::stoi( Word.substr( dash + 1 ) ) }; + if( to < from || to - from > 100 ) { return false; } + for( int card = from; card <= to; ++card ) { Cards.push_back( card ); } + return true; + } + catch( ... ) { return false; } +} + +} // namespace + +void structure_catalog::parse( std::vector const &Lines, std::string const &Source, std::vector &Errors ) { + // do którego wpisu dopisuje się "text" + std::string *text { nullptr }; + catalog_group *lastgroup { nullptr }; + for( std::size_t number = 0; number < Lines.size(); ++number ) { + auto line { Lines[ number ] }; + if( auto const comment { line.find( "//" ) }; comment != std::string::npos ) { line.erase( comment ); } + line = trimmed( line ); + if( line.empty() ) { continue; } + auto const report = [ & ]( std::string const &Reason ) { + Errors.push_back( "Catenary: file \"" + Source + "\" line " + std::to_string( number + 1 ) + ": " + Reason ); + }; + // słowa przed " : " i tytuł za nim + std::string head { line }, title; + if( auto const colon { line.find( " : " ) }; colon != std::string::npos ) { + head = line.substr( 0, colon ); + title = trimmed( line.substr( colon + 3 ) ); + } + std::istringstream stream { head }; + std::vector words; + for( std::string word; stream >> word; ) { words.push_back( word ); } + auto const &keyword { words.front() }; + if( keyword == "text" ) { + auto const body { trimmed( line.substr( 4 ) ) }; + if( text == nullptr ) { report( "\"text\" before any entry" ); continue; } + *text += ( text->empty() ? "" : " " ) + body; + continue; + } + if( keyword == "exec" ) { + if( lastgroup == nullptr || words.size() < 2 ) { report( "\"exec\" needs a number and a group before it" ); continue; } + try { lastgroup->executions.emplace_back( std::stoi( words[ 1 ] ), title ); } + catch( ... ) { report( "bad execution number" ); } + continue; + } + if( words.size() < 2 ) { report( "\"" + keyword + "\" without an id" ); continue; } + auto const &id { words[ 1 ] }; + if( keyword == "category" ) { + m_categories.push_back( { id, title, {} } ); + text = &m_categories.back().text; + lastgroup = nullptr; + continue; + } + if( keyword == "group" ) { + if( words.size() < 3 ) { report( "group \"" + id + "\" needs its category" ); continue; } + catalog_group group; + group.id = id; + group.category = words[ 2 ]; + group.title = title; + for( std::size_t k = 3; k + 1 < words.size(); k += 2 ) { + if( words[ k ] == "execution" ) { group.execution_user = ( words[ k + 1 ] != "reach" ); } + else { report( "group \"" + id + "\": unknown keyword \"" + words[ k ] + "\"" ); } + } + if( std::none_of( m_categories.begin(), m_categories.end(), [ & ]( catalog_category const &C ) { return C.id == group.category; } ) ) { + report( "group \"" + id + "\" is in category \"" + group.category + "\" which isn't defined before it" ); + } + m_groups.push_back( group ); + lastgroup = &m_groups.back(); + text = &lastgroup->text; + continue; + } + if( keyword == "family" ) { + if( words.size() < 3 ) { report( "family \"" + id + "\" needs its group" ); continue; } + catalog_family family; + family.id = id; + family.group = words[ 2 ]; + family.title = title; + auto good { true }; + for( std::size_t k = 3; k < words.size() && good; ) { + auto const &key { words[ k++ ] }; + if( key == "cards" ) { + while( k < words.size() && false == words[ k ].empty() && std::isdigit( static_cast( words[ k ][ 0 ] ) ) ) { + if( false == cards_of( words[ k++ ], family.cards ) ) { report( "family \"" + id + "\": bad card \"" + words[ k - 1 ] + "\"" ); good = false; } + } + } + else if( key == "curve" && k < words.size() ) { family.curve = ( words[ k++ ] == "yes" ? 1 : 0 ); } + else if( key == "toward" && k < words.size() ) { auto const &value { words[ k++ ] }; family.toward = ( value == "yes" ? 1 : value == "no" ? 0 : 2 ); } + else if( key == "image" && k < words.size() ) { family.image = words[ k++ ]; } + else { report( "family \"" + id + "\": unknown keyword \"" + key + "\"" ); good = false; } + } + if( false == good ) { text = nullptr; continue; } + if( family.cards.empty() ) { report( "family \"" + id + "\" has no cards" ); text = nullptr; continue; } + if( group( family.group ) == nullptr ) { report( "family \"" + id + "\" is in group \"" + family.group + "\" which isn't defined before it" ); } + m_families.push_back( family ); + text = &m_families.back().text; + continue; + } + if( keyword == "polegroup" ) { + catalog_polegroup polegroup; + polegroup.id = id; + polegroup.title = title; + for( std::size_t k = 2; k < words.size(); ) { + auto const &key { words[ k++ ] }; + if( key == "foundation" && k < words.size() ) { polegroup.foundation = words[ k++ ]; } + else if( key == "site" && k < words.size() ) { polegroup.site = words[ k++ ]; } + else if( key == "no_switch" ) { polegroup.no_switch = true; } + else if( key == "poles" ) { while( k < words.size() ) { polegroup.poles.push_back( words[ k++ ] ); } } + else { report( "polegroup \"" + id + "\": unknown keyword \"" + key + "\"" ); break; } + } + m_polegroups.push_back( polegroup ); + text = &m_polegroups.back().text; + lastgroup = nullptr; + continue; + } + report( "unknown keyword \"" + keyword + "\"" ); + text = nullptr; + } +} + +catalog_group const *structure_catalog::group( std::string const &Id ) const { + for( auto const &group : m_groups ) { if( group.id == Id ) { return &group; } } + return nullptr; +} + +catalog_family const *structure_catalog::family( std::string const &Id ) const { + for( auto const &family : m_families ) { if( family.id == Id ) { return &family; } } + return nullptr; +} + +catalog_family const *structure_catalog::family_of( int const Card ) const { + for( auto const &family : m_families ) { + if( std::find( family.cards.begin(), family.cards.end(), Card ) != family.cards.end() ) { return &family; } + } + return nullptr; +} + +catalog_polegroup const *structure_catalog::polegroup_of( std::string const &Pole ) const { + for( auto const &polegroup : m_polegroups ) { + if( std::find( polegroup.poles.begin(), polegroup.poles.end(), Pole ) != polegroup.poles.end() ) { return &polegroup; } + } + return nullptr; +} + +catalog_pick pick_card( structure_catalog const &Catalog, cantilever_catalog const &Cantilevers, std::string const &Group, std::string const &Family, + int const Card, bool const Curve, bool const Toward, int const Execution, double const Reach ) { + + catalog_pick result; + auto const *group { Catalog.group( Group ) }; + if( group == nullptr ) { result.problem = "no group \"" + Group + "\" in the catalogue"; return result; } + // pasmo A: od dolnej granicy włącznie do górnej wyłącznie (jak w kartach: 1,45-1,75 i 1,75-2,05 nie zachodzą na siebie) + auto const in_band = [ & ]( cantilever_scheme const &Scheme ) { + // karta z jednym A (peronowe: 4,70 / 5,00 / 5,30) - A w odległości do 0,15 m + if( Scheme.reach_max - Scheme.reach_min < 1e-6 ) { return std::abs( Reach - Scheme.reach_min ) <= 0.15; } + return Reach >= Scheme.reach_min && Reach < Scheme.reach_max; + }; + auto const within = [ & ]( catalog_family const &Candidate, catalog_pick &Pick ) { + for( auto const card : Candidate.cards ) { + if( Card > 0 && card != Card ) { continue; } + if( Family.empty() && Card <= 0 && false == Candidate.fits( Curve, Toward, card ) ) { continue; } + cantilever_scheme const *other { nullptr }; + for( auto const &entry : Cantilevers.schemes() ) { + if( entry.first.first != card || false == in_band( entry.second ) ) { continue; } + if( false == group->execution_user || entry.second.execution == Execution ) { + Pick.family = &Candidate; + Pick.scheme = &entry.second; + return true; + } + if( other == nullptr ) { other = &entry.second; } + } + if( other != nullptr ) { + Pick.family = &Candidate; + Pick.scheme = other; + Pick.execution_changed = true; + return true; + } + } + return false; + }; + // rodziny grupy: najpierw te, które dają wybrane wykonanie + catalog_pick fallback; + auto anyfamily { false }; + for( auto const &family : Catalog.families() ) { + if( family.group != Group ) { continue; } + if( false == Family.empty() && family.id != Family ) { continue; } + anyfamily = true; + catalog_pick pick; + if( false == within( family, pick ) ) { continue; } + if( false == pick.execution_changed ) { return pick; } + if( fallback.scheme == nullptr ) { fallback = pick; } + } + if( fallback.scheme != nullptr ) { return fallback; } + if( false == anyfamily ) { result.problem = "no family of the group fits"; } + else { + char text[ 32 ]; + std::snprintf( text, sizeof( text ), "%.2f", Reach ); + result.problem = std::string( "no card of the group covers A = " ) + text + " m" + ( Family.empty() ? std::string( Curve ? " on a curve" : " on a straight" ) + ( Toward ? ", wires towards the pole" : ", wires away from the pole" ) : std::string() ); + } + return result; +} + +} // namespace catenary diff --git a/world/catenary_catalog.h b/world/catenary_catalog.h new file mode 100644 index 00000000..21673a8b --- /dev/null +++ b/world/catenary_catalog.h @@ -0,0 +1,103 @@ +/* +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/. +*/ + +// Sieć trakcyjna: opisany katalog konstrukcji dla edytora (data/traction_catalog.txt, plik pisany ręcznie). +// Karty wysięgników z traction_cantilevers.txt i słupy z traction_supports.txt pogrupowane w kategorie, grupy i rodziny +// z opisami po ludzku, żeby wstawiający wiedział, co wstawia. Z rodziny i odległości od słupa (pasmo A) wybierana jest karta. +// Kod niezależny od reszty symulatora (C++17, tylko biblioteka standardowa). +#pragma once +#include +#include + +namespace catenary { + +class cantilever_catalog; +struct cantilever_scheme; + +struct catalog_category { + std::string id; + std::string title; + std::string text; // opis (linie "text") +}; + +// grupa: to, co wybiera użytkownik (np. "rurowe przechylne, tom 3"); rodzina w grupie dobiera się sama z położenia +struct catalog_group { + std::string id; + std::string category; + std::string title; + std::string text; + bool execution_user { true }; // wykonanie wybiera użytkownik (false: z pasma A, jak w teownikach) + std::vector> executions; // opisy wykonań (exec : ) +}; + +// rodzina: karty o jednym przeznaczeniu (np. prosta, odsuw do słupa) +struct catalog_family { + std::string id; + std::string group; + std::string title; + std::string text; + std::string image; // obrazek (ścieżka tekstury względem katalogu symulatora); pusty = brak + std::vector cards; // karty w kolejności doboru + int curve { -1 }; // 1 łuk, 0 prosta, -1 bez znaczenia + int toward { -1 }; // 1 odsuw do słupa, 0 od słupa, -1 bez znaczenia, 2 z numeru karty (nieparzysta = do słupa) + bool fits( bool Curve, bool Toward, int Card ) const; +}; + +// grupa słupów (np. "kratowe 161x na fundamentach palowych") +struct catalog_polegroup { + std::string id; + std::string title; + std::string text; + std::string foundation; // piles, cast; puste = nieokreślone + std::string site; // line (szlak), station (równia stacyjna), platform (peron); puste = nieokreślone + bool no_switch { false }; // słupy tej grupy nie stoją przy rozjazdach (edytor zgłasza błąd) + std::vector poles; +}; + +class structure_catalog { +public: + // wczytuje plik; zwraca false, gdy pliku nie da się otworzyć. błędy w treści trafiają do Errors + bool load( std::string const &Filename, std::vector &Errors ); + // rozbiera treść podaną jako linie (bez znaków końca linii) + void parse( std::vector const &Lines, std::string const &Source, std::vector &Errors ); + + std::vector const &categories() const { return m_categories; } + std::vector const &groups() const { return m_groups; } + std::vector const &families() const { return m_families; } + std::vector const &polegroups() const { return m_polegroups; } + catalog_group const *group( std::string const &Id ) const; + catalog_family const *family( std::string const &Id ) const; + // rodzina, do której należy karta; nullptr gdy żadna + catalog_family const *family_of( int Card ) const; + // grupa słupów, do której należy słup; nullptr gdy żadna + catalog_polegroup const *polegroup_of( std::string const &Pole ) const; + bool empty() const { return m_groups.empty(); } + +private: + std::vector m_categories; + std::vector m_groups; + std::vector m_families; + std::vector m_polegroups; +}; + +// wynik doboru karty +struct catalog_pick { + catalog_family const *family { nullptr }; + cantilever_scheme const *scheme { nullptr }; + bool execution_changed { false }; // wybranego wykonania nie ma dla tego A - wzięte inne + std::string problem; // dlaczego nie ma karty; puste gdy jest +}; + +// dobór karty z grupy: rodzina pasująca do łuku i odsuwu (albo Family, gdy niepusta), w niej pierwsza karta, której pasmo A +// obejmuje Reach - wykonanie Execution, gdy wykonanie wybiera użytkownik (a gdy takiego nie ma, inne z tym pasmem), +// w przeciwnym razie dowolne z tym pasmem. Card > 0: tylko ta karta +catalog_pick pick_card( structure_catalog const &Catalog, cantilever_catalog const &Cantilevers, std::string const &Group, std::string const &Family, + int Card, bool Curve, bool Toward, int Execution, double Reach ); + +} // namespace catenary diff --git a/world/catenary_layout.cpp b/world/catenary_layout.cpp new file mode 100644 index 00000000..d3602baa --- /dev/null +++ b/world/catenary_layout.cpp @@ -0,0 +1,121 @@ +#include "world/catenary_layout.h" + +#include +#include +#include +#include +#include + +namespace catenary { + +namespace { + +bool to_number( std::string const &Text, double &Value ) { + std::istringstream stream { Text }; + stream.imbue( std::locale::classic() ); + stream >> Value; + return false == stream.fail() && stream.eof(); +} + +} // namespace + +bool layout_tables::load( std::string const &Filename, std::vector &Errors ) { + + std::ifstream file { Filename }; + if( false == file.is_open() ) { return false; } + std::vector tokens; + std::string line; + while( std::getline( file, line ) ) { + auto const comment { line.find( "//" ) }; + if( comment != std::string::npos ) { line.erase( comment ); } + std::istringstream words { line }; + std::string word; + while( words >> word ) { tokens.push_back( word ); } + } + parse( tokens, Filename, Errors ); + return true; +} + +void layout_tables::parse( std::vector const &Tokens, std::string const &Source, std::vector &Errors ) { + + std::size_t index { 0 }; + auto const count { Tokens.size() }; + while( index < count ) { + auto const &key { Tokens[ index++ ] }; + if( key == "span" ) { + // span + double first { 0.0 }, second { 0.0 }; + if( index + 3 <= count && to_number( Tokens[ index + 1 ], first ) && to_number( Tokens[ index + 2 ], second ) ) { + m_spans[ Tokens[ index ] ] = { first, second }; + index += 3; + } + else { + Errors.push_back( "Bad layout file \"" + Source + "\": \"span\" needs a network symbol and two lengths" ); + index = std::min( index + 3, count ); + } + } + else if( key == "curve_stagger" ) { + double value { 0.0 }; + if( index < count && to_number( Tokens[ index ], value ) && value > 0.0 ) { m_curvestagger = value; } + else { Errors.push_back( "Bad layout file \"" + Source + "\": \"curve_stagger\" needs a positive number" ); } + ++index; + } + else { + Errors.push_back( "Bad layout file \"" + Source + "\": unexpected token \"" + key + "\"" ); + } + } +} + +double layout_tables::nominal_span( std::string const &Symbol, int const Zone ) const { + + auto const lookup { m_spans.find( Symbol ) }; + if( lookup == m_spans.end() ) { return 0.0; } + return ( Zone == 2 ? lookup->second.second : lookup->second.first ); +} + +std::vector layout_tables::symbols() const { + + std::vector result; + for( auto const &span : m_spans ) { result.push_back( span.first ); } + return result; +} + +double curve_span( double const Radius, double const Stagger ) { + + if( Radius <= 0.0 || Stagger <= 0.0 ) { return 0.0; } + return 2.0 * std::sqrt( 2.0 * Radius * Stagger ); +} + +double span_limit( layout_tables const &Tables, std::string const &Symbol, int const Zone, double const Radius, double const Stagger ) { + + auto const nominal { Tables.nominal_span( Symbol, Zone ) }; + if( nominal <= 0.0 ) { return 0.0; } + if( Radius <= 0.0 ) { return nominal; } + return std::min( nominal, curve_span( Radius, Stagger ) ); +} + +double plan_radius( double const A[ 2 ], double const B[ 2 ], double const C[ 2 ] ) { + + auto const ab { std::hypot( B[ 0 ] - A[ 0 ], B[ 1 ] - A[ 1 ] ) }; + auto const bc { std::hypot( C[ 0 ] - B[ 0 ], C[ 1 ] - B[ 1 ] ) }; + auto const ca { std::hypot( A[ 0 ] - C[ 0 ], A[ 1 ] - C[ 1 ] ) }; + // podwójne pole trójkąta + auto const area2 { std::abs( ( B[ 0 ] - A[ 0 ] ) * ( C[ 1 ] - A[ 1 ] ) - ( B[ 1 ] - A[ 1 ] ) * ( C[ 0 ] - A[ 0 ] ) ) }; + // prawie prosta: promień ponad 20 km traktowany jak prosta + if( area2 < 1e-9 ) { return 0.0; } + auto const radius { ab * bc * ca / ( 2.0 * area2 ) }; + return ( radius > 20000.0 ? 0.0 : radius ); +} + +int reach_digit( double const Reach ) { + + if( Reach < 1.45 || Reach >= 4.45 ) { return -1; } + return std::clamp( static_cast( std::floor( ( Reach - 1.45 ) / 0.30 ) ), 0, 9 ); +} + +int family_decade( suspension_family const &Family ) { + + return ( Family.swinging ? 1100 : 1200 ) + ( Family.curve ? 30 : 10 ) + ( Family.toward_pole ? 0 : 10 ); +} + +} // namespace catenary diff --git a/world/catenary_layout.h b/world/catenary_layout.h new file mode 100644 index 00000000..15cad91b --- /dev/null +++ b/world/catenary_layout.h @@ -0,0 +1,53 @@ +// Sieć trakcyjna: zasady rozstawiania konstrukcji wsporczych i doboru podwieszeń z katalogu (Kolprojekt, karty 0002, 0007, 0008, +// stary katalog 13-0002, 13-0005, 13-0008). Kod niezależny od reszty symulatora (C++17, tylko biblioteka standardowa). +// Edytor używa tego do sprawdzania tego, co postawił użytkownik (rozpiętości przęseł, pasmo A) i do doboru karty - +// niczego sam nie rozstawia. +#pragma once +#include +#include +#include + +namespace catenary { + +// tablice z pliku data/traction_layout.txt +class layout_tables { +public: + // wczytuje plik; zwraca false, gdy pliku nie da się otworzyć. błędy w treści trafiają do Errors + bool load( std::string const &Filename, std::vector &Errors ); + // rozbiera treść podaną jako kolejne słowa (już bez komentarzy) + void parse( std::vector const &Tokens, std::string const &Source, std::vector &Errors ); + // rozpiętość nominalna przęsła sieci o podanym symbolu w strefie wiatrowej Zone (1 albo 2); 0 gdy sieci nie ma w tablicy + double nominal_span( std::string const &Symbol, int Zone ) const; + // odsuw sieci na łuku przyjmowany do rozstawienia konstrukcji (karta 0008), m + double curve_stagger() const { return m_curvestagger; } + // symbole sieci z tablicy, w kolejności alfabetycznej + std::vector symbols() const; + +private: + std::map> m_spans; + double m_curvestagger { 0.40 }; +}; + +// największa rozpiętość przęsła na łuku o promieniu Radius przy odsuwie Stagger: a = 2·√(2·R·z) (karta 0008) +double curve_span( double Radius, double Stagger ); +// największa rozpiętość przęsła: nominalna sieci, a na łuku nie więcej niż z wykresu 0008. Radius <= 0 = prosta. +// zwraca 0, gdy sieci nie ma w tablicy +double span_limit( layout_tables const &Tables, std::string const &Symbol, int Zone, double Radius, double Stagger ); + +// promień okręgu przez trzy punkty w planie (x, z); 0 gdy leżą na prostej (albo prawie) +double plan_radius( double const A[ 2 ], double const B[ 2 ], double const C[ 2 ] ); + +// pasmo A w kartach podwieszeń: ostatnia cyfra karty = (A − 1,45) / 0,30, czyli 0 dla 1,45–1,75 m … 9 dla 4,15–4,45 m. +// zwraca -1 poza zakresem +int reach_digit( double Reach ); + +// rodzina kart podwieszeń przelotowych (dziesiątka katalogu): rodzaj wysięgnika, prosta czy łuk, odsuw do słupa czy od słupa +struct suspension_family { + bool swinging { true }; // przechylny (11xx); false = nieprzechylny (12xx) + bool curve { false }; // łuk (113x/114x, 123x/124x); false = prosta + bool toward_pole { true }; // odsuw do słupa; false = od słupa +}; +// numer dziesiątki (np. 1110, 1120, 1230) dla rodziny wysięgników rurowych z tomu 3 nowego katalogu +int family_decade( suspension_family const &Family ); + +} // namespace catenary diff --git a/world/catenary_network.cpp b/world/catenary_network.cpp new file mode 100644 index 00000000..2be44403 --- /dev/null +++ b/world/catenary_network.cpp @@ -0,0 +1,561 @@ +#include "world/catenary_network.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace catenary { + +namespace { + +constexpr double dan { 10.0 }; // naciągi w plikach są w daN, jak w katalogu + +std::string trim( std::string const &Text ) { + auto const first { Text.find_first_not_of( " \t\r\n" ) }; + if( first == std::string::npos ) { return {}; } + auto const last { Text.find_last_not_of( " \t\r\n" ) }; + return Text.substr( first, last - first + 1 ); +} + +std::string strip_comment( std::string const &Line ) { + auto const comment { Line.find( "//" ) }; + return comment == std::string::npos ? Line : Line.substr( 0, comment ); +} + +bool to_number( std::string const &Text, double &Value ) { + std::istringstream stream { Text }; + stream.imbue( std::locale::classic() ); + stream >> Value; + return false == stream.fail(); +} + +std::vector to_numbers( std::string const &Text ) { + std::vector values; + std::istringstream stream { Text }; + stream.imbue( std::locale::classic() ); + double value; + while( stream >> value ) { values.push_back( value ); } + return values; +} + +// odstępy z reguły: odstęp skrajny przy podwieszeniach, środek podzielony równo na odcinki nie dłuższe od Maxgap +std::vector rule_gaps( double const Length, double Endgap, double Maxgap ) { + Endgap = std::max( Endgap, 0.5 ); + Maxgap = std::max( Maxgap, 1.0 ); + if( Length <= 2.0 * Endgap + 0.5 ) { return { 0.5 * Length, 0.5 * Length }; } // jeden wieszak w środku + auto const inner { Length - 2.0 * Endgap }; + auto const count { std::max( 1, static_cast( std::ceil( inner / Maxgap - 1e-9 ) ) ) }; + std::vector gaps; + gaps.push_back( Endgap ); + gaps.insert( gaps.end(), static_cast( count ), inner / count ); + gaps.push_back( Endgap ); + return gaps; +} + +} // namespace + +type_registry &types() { + static type_registry registry; + return registry; +} + +void type_registry::directory( std::string const &Path ) { + std::lock_guard lock { m_mutex }; + m_directory = Path; + if( false == m_directory.empty() && m_directory.back() != '/' && m_directory.back() != '\\' ) { m_directory += '/'; } +} + +void type_registry::report( std::string const &Message ) { + if( m_reported[ Message ] ) { return; } + m_reported[ Message ] = true; + if( on_error ) { on_error( Message ); } +} + +void type_registry::load_tables() { + if( m_tablesloaded ) { return; } + m_tablesloaded = true; + auto const filename { m_directory + "traction_tables.txt" }; + std::ifstream file { filename }; + if( !file ) { + report( "Catenary: can't open file \"" + filename + "\", hanger spacing and wire data unavailable" ); + return; + } + std::string line; + int linenumber { 0 }; + while( std::getline( file, line ) ) { + ++linenumber; + line = trim( strip_comment( line ) ); + if( line.empty() || line[ 0 ] == '#' ) { continue; } + std::istringstream stream { line }; + stream.imbue( std::locale::classic() ); + std::string kind, key; + stream >> kind >> key; + if( kind == "wire" ) { + double mass { 0.0 }, diameter { 0.0 }; + stream >> mass >> diameter; + if( stream.fail() || mass <= 0.0 ) { report( "Catenary: bad wire entry in \"" + filename + "\" (line " + std::to_string( linenumber ) + ")" ); continue; } + m_wires[ key ] = { mass, diameter * 0.001 }; + } + else if( kind == "spacing" ) { + int span { 0 }; + stream >> span; + std::vector gaps; + double gap; + while( stream >> gap ) { gaps.push_back( gap ); } + auto const sum { std::accumulate( gaps.begin(), gaps.end(), 0.0 ) }; + if( span <= 0 || gaps.size() < 2 || std::abs( sum - span ) > 0.01 ) { + report( "Catenary: bad spacing entry in \"" + filename + "\" (line " + std::to_string( linenumber ) + "), gaps don't add up to the span length" ); + continue; + } + m_spacing[ key ][ span ] = gaps; + } + // pozostałe wiersze (np. "length" z tablic wzorcowych) nie są potrzebne w symulatorze + } +} + +std::unique_ptr type_registry::load_type( std::string const &Symbol ) { + auto const filename { m_directory + "traction_" + Symbol + ".txt" }; + std::ifstream file { filename }; + if( !file ) { + report( "Catenary: unknown network type \"" + Symbol + "\", can't open file \"" + filename + "\"" ); + return nullptr; + } + std::map values; + std::string line; + while( std::getline( file, line ) ) { + line = strip_comment( line ); + auto const colon { line.find( ':' ) }; + if( colon == std::string::npos ) { continue; } // nawiasy, puste wiersze + values[ trim( line.substr( 0, colon ) ) ] = trim( line.substr( colon + 1 ) ); + } + bool good { true }; + auto const fail = [ & ]( std::string const &Reason ) { + report( "Catenary: network type \"" + Symbol + "\" (file \"" + filename + "\"): " + Reason ); + good = false; + }; + auto const number = [ & ]( char const *Key, double &Value, bool const Required ) { + auto const lookup { values.find( Key ) }; + if( lookup == values.end() || lookup->second.empty() ) { + if( Required ) { fail( std::string( "missing entry \"" ) + Key + "\"" ); } + return false; + } + if( false == to_number( lookup->second, Value ) ) { fail( std::string( "entry \"" ) + Key + "\" is not a number" ); return false; } + return true; + }; + auto const wire = [ & ]( char const *Key, wire_data &Wire, bool const Required ) { + auto const lookup { values.find( Key ) }; + if( lookup == values.end() || lookup->second.empty() ) { + if( Required ) { fail( std::string( "missing entry \"" ) + Key + "\"" ); } + return false; + } + auto const data { m_wires.find( lookup->second ) }; + if( data == m_wires.end() ) { fail( "wire \"" + lookup->second + "\" isn't listed in traction_tables.txt" ); return false; } + Wire = data->second; + return true; + }; + + auto definition { std::make_unique() }; + auto &type { definition->type }; + type.symbol = Symbol; + double value { 0.0 }; + if( number( "traction_code", value, false ) ) { type.code = static_cast( value ); } + if( number( "contact_wire_num", value, true ) ) { type.contact_count = static_cast( value ); } + if( number( "catenary_wire_num", value, true ) ) { type.messenger_count = static_cast( value ); } + if( wire( "contact_wire", definition->contact_wire, true ) ) { type.contact_mass = definition->contact_wire.mass; } + if( wire( "catenary_wire", definition->messenger_wire, true ) ) { type.messenger_mass = definition->messenger_wire.mass; } + if( number( "catenary_tension", value, true ) ) { type.messenger_tension = value * dan; } + number( "contact_wire_spacing", definition->contact_spacing, false ); + number( "catenary_stack", type.messenger_stack, false ); + number( "hanger_mass", type.hanger_mass, false ); + number( "height", type.structure_height, true ); + number( "y_hanger_correction", type.y_hanger_drop, false ); + number( "y_junction_drop", type.y_junction_drop, false ); + number( "y_plan_offset", definition->y_plan_offset, false ); + if( number( "contact_tension", value, false ) ) { type.contact_tension = value * dan; } + { + // linka wieszaków: domyślnie L10, jak na kartach wieszaków + auto const lookup { values.find( "hanger_wire" ) }; + auto const data { m_wires.find( lookup != values.end() && false == lookup->second.empty() ? lookup->second : "L10" ) }; + if( data != m_wires.end() ) { definition->hanger_wire = data->second; } + } + if( number( "y_tension", value, false ) && value > 0.0 ) { + type.y_tension = value * dan; + if( wire( "y_wire", definition->y_wire, true ) ) { type.y_mass = definition->y_wire.mass; } + // pary: najmniejsza rozpiętość, długość Y; zapisane malejąco wg rozpiętości + auto const lookup { values.find( "y_length" ) }; + auto const pairs { lookup != values.end() ? to_numbers( lookup->second ) : std::vector{} }; + if( pairs.empty() || pairs.size() % 2 != 0 ) { fail( "entry \"y_length\" needs pairs of numbers: minimum span and Y length" ); } + for( std::size_t index = 0; index + 1 < pairs.size(); index += 2 ) { type.y_length_by_span.emplace_back( pairs[ index ], pairs[ index + 1 ] ); } + std::sort( type.y_length_by_span.begin(), type.y_length_by_span.end(), []( auto const &Left, auto const &Right ) { return Left.first > Right.first; } ); + if( type.y_tension >= type.messenger_tension ) { fail( "\"y_tension\" has to be lower than \"catenary_tension\"" ); } + } + { + auto const lookup { values.find( "hanger_table" ) }; + if( lookup == values.end() || lookup->second.empty() ) { fail( "missing entry \"hanger_table\"" ); } + else { + definition->hanger_table = lookup->second; + if( m_spacing.find( definition->hanger_table ) == m_spacing.end() ) { fail( "hanger table \"" + definition->hanger_table + "\" isn't listed in traction_tables.txt" ); } + } + } + if( good ) { + if( type.contact_count < 1 || type.contact_count > 2 ) { fail( "\"contact_wire_num\" has to be 1 or 2" ); } + if( type.messenger_count < 1 || type.messenger_count > 2 ) { fail( "\"catenary_wire_num\" has to be 1 or 2" ); } + if( type.messenger_tension <= 0.0 ) { fail( "\"catenary_tension\" has to be greater than zero" ); } + } + if( false == good ) { return nullptr; } + return definition; +} + +network_definition const *type_registry::find( std::string const &Symbol ) { + std::lock_guard lock { m_mutex }; + auto const lookup { m_types.find( Symbol ) }; + if( lookup != m_types.end() ) { return lookup->second.get(); } + load_tables(); + auto &slot { m_types[ Symbol ] }; + slot = load_type( Symbol ); + return slot.get(); +} + +std::vector type_registry::hanger_gaps( std::string const &Table, double const Length ) { + std::lock_guard lock { m_mutex }; + load_tables(); + auto const table { m_spacing.find( Table ) }; + if( table == m_spacing.end() || table->second.empty() ) { return rule_gaps( Length, 2.0, 4.0 ); } + auto const &rows { table->second }; + auto const span { static_cast( std::lround( Length ) ) }; + auto const row { rows.find( span ) }; + if( row == rows.end() ) { + // rozpiętość spoza tablicy: reguła oparta na najbliższym wierszu + auto nearest { rows.lower_bound( span ) }; + if( nearest == rows.end() ) { --nearest; } + else if( nearest != rows.begin() ) { + auto const previous { std::prev( nearest ) }; + if( span - previous->first < nearest->first - span ) { nearest = previous; } + } + auto const &gaps { nearest->second }; + return rule_gaps( Length, gaps.front(), *std::max_element( gaps.begin(), gaps.end() ) ); + } + auto gaps { row->second }; + // różnicę do rozpiętości całkowitej przejmują po równo największe (środkowe) odstępy + auto const difference { Length - std::accumulate( gaps.begin(), gaps.end(), 0.0 ) }; + auto const largest { *std::max_element( gaps.begin(), gaps.end() ) }; + auto const count { std::count_if( gaps.begin(), gaps.end(), [ & ]( double const Gap ) { return std::abs( Gap - largest ) < 1e-6; } ) }; + for( auto &gap : gaps ) { + if( std::abs( gap - largest ) < 1e-6 ) { gap += difference / static_cast( count ); } + } + return gaps; +} + +bool solve_span_points( network_definition const &Definition, std::vector const &Hangergaps, span_points const &Points, + bool const Ystart, bool const Yend, span_solution &Solution, std::string &Error ) { + + auto const &type { Definition.type }; + auto const &c0 { Points.contact_start }, &c1 { Points.contact_end }, &m0 { Points.messenger_start }, &m1 { Points.messenger_end }; + auto const length { std::hypot( c1.x - c0.x, c1.z - c0.z ) }; + if( length < 1.0 ) { Error = "span shorter than 1 m"; return false; } + + span_input input; + input.length = length; + input.hanger_gaps = Hangergaps; + input.height_start = std::max( 0.0, m0.y - c0.y ); + input.height_end = std::max( 0.0, m1.y - c1.y ); + input.y_at_start = Ystart; + input.y_at_end = Yend; + span_result result; + try { + // bez rozstawu wieszaków: odcinek do kotwienia, same liny + result = ( Hangergaps.empty() ? + solve_free_span( type, length, input.height_start, input.height_end ) : + solve_span( type, input, 4.0 ) ); + } + catch( std::exception const &exception ) { + Error = exception.what(); + return false; + } + // lina poniżej przewodu jezdnego: przęsło za długie albo za mała wysokość konstrukcyjna dla tego typu sieci + for( auto const *line : { &result.messenger, &result.y_start, &result.y_end } ) { + for( auto const &point : *line ) { + if( point.y - ( type.messenger_count > 1 ? type.messenger_stack : 0.0 ) < -0.01 ) { + Error = "messenger wire would hang below the contact wire (span too long or suspension too low for this network type)"; + return false; + } + } + } + Solution.length = length; + Solution.result = std::move( result ); + return true; +} + +void join_y( network_definition const &Definition, span_solution &Solution, bool const Atend, y_half const &Neighbour ) { + join_y( Definition.type, Solution.result, Solution.length, Atend, Neighbour ); +} + +bool build_span_lines( network_definition const &Definition, std::vector const &Hangergaps, span_points const &Points, + double const Fallbackoffset, std::vector &Output, std::string &Error ) { + span_solution solution; + if( false == solve_span_points( Definition, Hangergaps, Points, true, true, solution, Error ) ) { return false; } + build_lines( Definition, solution, Points, Fallbackoffset, Output ); + return true; +} + +y_side make_y_side( span_solution const &Solution, span_points const &Points, bool const Atend ) { + + y_side side; + auto const &half { Atend ? Solution.result.y_half_end : Solution.result.y_half_start }; + if( half.reach <= 0.0 ) { return side; } + auto const &m0 { Points.messenger_start }, &m1 { Points.messenger_end }; + auto const t { ( Atend ? Solution.length - half.reach : half.reach ) / Solution.length }; + side.valid = true; + side.reach = half.reach; + side.axis = ( Atend ? m1 : m0 ); + side.junction = { m0.x + ( m1.x - m0.x ) * t, m0.y + ( m1.y - m0.y ) * t, m0.z + ( m1.z - m0.z ) * t }; + return side; +} + +vec3 y_plan::point( double const Distance ) const { + // łuk drugiego stopnia; parametr wg odległości wzdłuż liny nośnej + auto const t { std::clamp( ( reach - Distance ) / total, 0.0, 1.0 ) }; + auto const a { ( 1.0 - t ) * ( 1.0 - t ) }, b { 2.0 * t * ( 1.0 - t ) }, c { t * t }; + return { a * start.x + b * control.x + c * end.x, 0.0, a * start.z + b * control.z + c * end.z }; +} + +y_plan solve_y_plan( network_definition const &Definition, y_side const &Mine, y_side const &Other ) { + + y_plan plan; + if( false == Mine.valid || false == Other.valid ) { return plan; } + plan.reach = Mine.reach; + plan.total = Mine.reach + Other.reach; + plan.start = Mine.junction; + plan.end = Other.junction; + // załom liny nośnej w osi podwieszenia i środek cięciwy zacisk–zacisk + vec3 const axis { 0.5 * ( Mine.axis.x + Other.axis.x ), 0.0, 0.5 * ( Mine.axis.z + Other.axis.z ) }; + vec3 const middle { 0.5 * ( plan.start.x + plan.end.x ), 0.0, 0.5 * ( plan.start.z + plan.end.z ) }; + // środek łuku leży w połowie drogi między punktem kierującym a środkiem cięciwy, + // więc punkt kierujący jest odsunięty od środka cięciwy o 2 ( 1 - y_plan_offset ) odległości do załomu + auto const weight { 2.0 * ( 1.0 - std::clamp( Definition.y_plan_offset, 0.0, 1.0 ) ) }; + plan.control = { middle.x + ( axis.x - middle.x ) * weight, 0.0, middle.z + ( axis.z - middle.z ) * weight }; + plan.valid = true; + return plan; +} + +double wire_diameter( wire_data const &Wire ) { + if( Wire.diameter > 0.0 ) { return Wire.diameter; } + if( Wire.mass <= 0.0 ) { return 0.004; } + // linka skręcana z miedzi: przekrój z masy, wypełnienie obrysu ok. 0,76 + constexpr double pi { 3.14159265358979323846 }; + return std::sqrt( 4.0 * ( Wire.mass / 8900.0 ) / ( pi * 0.76 ) ); +} + +void build_geometry( network_definition const &Definition, span_solution const &Solution, span_points const &Points, + double const Fallbackoffset, span_geometry &Output, y_plan const &Ystart, y_plan const &Yend ) { + + auto const &type { Definition.type }; + auto const &c0 { Points.contact_start }, &c1 { Points.contact_end }, &m0 { Points.messenger_start }, &m1 { Points.messenger_end }; + auto const length { Solution.length }; + auto const &result { Solution.result }; + + // kierunek przęsła w poziomie i wektor poziomy w bok od osi przęsła + vec3 const along { ( c1.x - c0.x ) / length, 0.0, ( c1.z - c0.z ) / length }; + vec3 const side { along.z, 0.0, -along.x }; + auto const offset { type.contact_count > 1 ? ( Definition.contact_spacing > 0.0 ? 0.5 * Definition.contact_spacing : Fallbackoffset ) : 0.0 }; + // punkt przewodu jezdnego w odległości X od początku, przesunięty w bok o Shift + auto const contact = [ & ]( double const X, double const Shift ) { + auto const t { X / length }; + return vec3 { c0.x + ( c1.x - c0.x ) * t + side.x * Shift, c0.y + ( c1.y - c0.y ) * t, c0.z + ( c1.z - c0.z ) * t + side.z * Shift }; + }; + // punkt liny: w poziomie na cięciwie liny nośnej, w pionie Height nad przewodem jezdnym + auto const rope = [ & ]( double const X, double const Height ) { + auto const t { X / length }; + return vec3 { m0.x + ( m1.x - m0.x ) * t, c0.y + ( c1.y - c0.y ) * t + Height, m0.z + ( m1.z - m0.z ) * t }; + }; + // punkt linki Y: w planie wg policzonego przebiegu, a gdy go nie ma (albo w zacisku) na cięciwie liny nośnej + auto const yrope = [ & ]( double const X, double const Height, y_plan const &Plan, bool const Atend ) { + auto point { rope( X, Height ) }; + if( false == Plan.valid ) { return point; } + auto const plan { Plan.point( Atend ? length - X : X ) }; + point.x = plan.x; + point.z = plan.z; + return point; + }; + // linka Y przy jednym podwieszeniu; na łuku dzielona na krótsze odcinki, żeby przebieg w planie był gładki + auto const yline = [ & ]( std::vector const &Line, y_plan const &Plan, bool const Atend ) { + std::vector points; + for( std::size_t index = 0; index < Line.size(); ++index ) { + if( index > 0 && Plan.valid ) { + auto const &from { Line[ index - 1 ] }, &to { Line[ index ] }; + auto const pieces { static_cast( std::ceil( ( to.x - from.x ) / 1.0 - 1e-9 ) ) }; + for( int piece = 1; piece < pieces; ++piece ) { + auto const t { static_cast( piece ) / pieces }; + points.push_back( yrope( from.x + ( to.x - from.x ) * t, from.y + ( to.y - from.y ) * t, Plan, Atend ) ); + } + } + points.push_back( yrope( Line[ index ].x, Line[ index ].y, Plan, Atend ) ); + } + return points; + }; + auto const polyline = [ & ]( std::vector const &Line, element_kind const Kind ) { + for( std::size_t index = 0; index + 1 < Line.size(); ++index ) { + Output.elements.push_back( { Line[ index ], Line[ index + 1 ], Kind } ); + } + }; + auto const reachstart { result.y_half_start.reach }, reachend { result.y_half_end.reach }; + + // przewody jezdne; w przęśle bez wieszaków ze zwisem, o ile typ podaje ich naciąg + for( int wire = 0; wire < type.contact_count; ++wire ) { + auto const shift { type.contact_count > 1 ? ( wire == 0 ? -offset : offset ) : 0.0 }; + std::vector line; + if( result.contact.size() > 1 ) { + for( auto const &point : result.contact ) { + auto position { contact( point.x, shift ) }; + position.y += point.y; + line.push_back( position ); + } + } + else { + line = { contact( 0.0, shift ), contact( length, shift ) }; + } + polyline( line, element_kind::contact ); + } + // górna lina nośna + { + std::vector line; + for( auto const &point : result.messenger ) { line.push_back( rope( point.x, point.y ) ); } + polyline( line, element_kind::messenger ); + } + // linki Y, a przy dwóch linach dolna lina nośna: przy podwieszeniu przebieg jak linka Y, dalej pod górną liną + { + auto const start { yline( result.y_start, Ystart, false ) }; + auto const end { yline( result.y_end, Yend, true ) }; + polyline( start, element_kind::y_rope ); + if( type.messenger_count > 1 ) { + std::vector middle; + if( false == start.empty() ) { middle.push_back( start.back() ); } + for( auto const &point : result.messenger ) { + auto const instart { reachstart > 0.0 && point.x < reachstart + 1e-6 }; + auto const inend { reachend > 0.0 && point.x > length - reachend - 1e-6 }; + if( false == instart && false == inend ) { middle.push_back( rope( point.x, point.y - type.messenger_stack ) ); } + } + if( false == end.empty() ) { middle.push_back( end.front() ); } + polyline( middle, element_kind::messenger ); + } + polyline( end, element_kind::y_rope ); + // zaciski linki Y na linie nośnej + if( false == start.empty() ) { Output.fittings.push_back( { start.back(), along, fitting_kind::y_clamp } ); } + if( false == end.empty() ) { Output.fittings.push_back( { end.front(), along, fitting_kind::y_clamp } ); } + } + // wieszaki, przy dwóch przewodach jezdnych na przemian + for( std::size_t index = 0; index < result.hangers.size(); ++index ) { + auto const &hanger { result.hangers[ index ] }; + auto const top { hanger.length + ( hanger.on_y ? type.y_hanger_drop : 0.0 ) }; + if( top <= 0.01 ) { continue; } // lina przy samym przewodzie, wieszaka nie ma + auto const shift { type.contact_count > 1 ? ( index % 2 == 0 ? -offset : offset ) : 0.0 }; + auto const atstart { hanger.x < 0.5 * length }; + auto const upper { hanger.on_y ? yrope( hanger.x, top, atstart ? Ystart : Yend, false == atstart ) : rope( hanger.x, top ) }; + auto const lower { contact( hanger.x, shift ) }; + Output.elements.push_back( { upper, lower, element_kind::hanger } ); + Output.fittings.push_back( { upper, along, fitting_kind::rope_clamp } ); + Output.fittings.push_back( { lower, along, fitting_kind::wire_clamp } ); + // przy dwóch linach nośnych: łącznik między liną dolną a górną w każdym punkcie wieszaka, + // poza strefą przy podwieszeniu, gdzie dolna lina odchodzi od górnej i sama niesie wieszaki + if( type.messenger_count > 1 && false == hanger.on_y && type.messenger_stack > 0.0 ) { + auto const above { rope( hanger.x, top + type.messenger_stack ) }; + Output.elements.push_back( { upper, above, element_kind::link } ); + Output.fittings.push_back( { { upper.x, 0.5 * ( upper.y + above.y ), upper.z }, along, fitting_kind::link_clamp } ); + } + } +} + +void build_lines( network_definition const &Definition, span_solution const &Solution, span_points const &Points, + double const Fallbackoffset, std::vector &Output, y_plan const &Ystart, y_plan const &Yend ) { + span_geometry geometry; + build_geometry( Definition, Solution, Points, Fallbackoffset, geometry, Ystart, Yend ); + Output.reserve( Output.size() + 2 * geometry.elements.size() ); + for( auto const &element : geometry.elements ) { + Output.push_back( element.from ); + Output.push_back( element.to ); + } +} + +namespace { + +vec3 operator+( vec3 const &Left, vec3 const &Right ) { return { Left.x + Right.x, Left.y + Right.y, Left.z + Right.z }; } +vec3 operator-( vec3 const &Left, vec3 const &Right ) { return { Left.x - Right.x, Left.y - Right.y, Left.z - Right.z }; } +vec3 operator*( vec3 const &Left, double const Right ) { return { Left.x * Right, Left.y * Right, Left.z * Right }; } +vec3 cross( vec3 const &Left, vec3 const &Right ) { return { Left.y * Right.z - Left.z * Right.y, Left.z * Right.x - Left.x * Right.z, Left.x * Right.y - Left.y * Right.x }; } +double norm( vec3 const &Vector ) { return std::sqrt( Vector.x * Vector.x + Vector.y * Vector.y + Vector.z * Vector.z ); } + +// ściany boczne graniastosłupa wokół odcinka; trójkąty zwrócone przodem na zewnątrz (kolejność przeciwna do ruchu wskazówek zegara), +// normalne promieniowe, żeby graniastosłup cieniował się jak walec +void add_prism( std::vector &Output, vec3 const &From, vec3 const &To, double const Radius, int const Sides ) { + constexpr double pi { 3.14159265358979323846 }; + auto axis { To - From }; + auto const length { norm( axis ) }; + if( length < 1e-6 ) { return; } + axis = axis * ( 1.0 / length ); + auto const reference { std::abs( axis.y ) < 0.9 ? vec3 { 0.0, 1.0, 0.0 } : vec3 { 1.0, 0.0, 0.0 } }; + auto u { cross( axis, reference ) }; + u = u * ( 1.0 / norm( u ) ); + auto const v { cross( axis, u ) }; + for( int side = 0; side < Sides; ++side ) { + auto const a0 { 2.0 * pi * side / Sides }, a1 { 2.0 * pi * ( side + 1 ) / Sides }; + auto const n0 { u * std::cos( a0 ) + v * std::sin( a0 ) }, n1 { u * std::cos( a1 ) + v * std::sin( a1 ) }; + solid_vertex const f0 { From + n0 * Radius, n0 }, f1 { From + n1 * Radius, n1 }, t0 { To + n0 * Radius, n0 }, t1 { To + n1 * Radius, n1 }; + Output.insert( Output.end(), { f0, f1, t0, f1, t1, t0 } ); + } +} + +// prostopadłościan o środku Center; Length wzdłuż Along, Width w bok, Height w pionie +void add_box( std::vector &Output, vec3 const &Center, vec3 const &Along, double const Length, double const Width, double const Height ) { + vec3 const side { Along.z, 0.0, -Along.x }, up { 0.0, 1.0, 0.0 }; + vec3 const a { Along * ( 0.5 * Length ) }, s { side * ( 0.5 * Width ) }, h { up * ( 0.5 * Height ) }; + // narożniki: bit 0 wzdłuż, bit 1 w bok, bit 2 w górę + vec3 corner[ 8 ]; + for( int index = 0; index < 8; ++index ) { + corner[ index ] = Center + a * ( index & 1 ? 1.0 : -1.0 ) + s * ( index & 2 ? 1.0 : -1.0 ) + h * ( index & 4 ? 1.0 : -1.0 ); + } + // ściany jako czwórki narożników obchodzonych przeciwnie do ruchu wskazówek zegara, patrząc z zewnątrz, i ich normalne + int const faces[ 6 ][ 4 ] { { 1, 3, 7, 5 }, { 0, 4, 6, 2 }, { 2, 6, 7, 3 }, { 0, 1, 5, 4 }, { 4, 5, 7, 6 }, { 0, 2, 3, 1 } }; + vec3 const normals[ 6 ] { Along, Along * -1.0, side, side * -1.0, up, up * -1.0 }; + for( int face = 0; face < 6; ++face ) { + auto const &f { faces[ face ] }; + auto const &n { normals[ face ] }; + Output.insert( Output.end(), { { corner[ f[ 0 ] ], n }, { corner[ f[ 1 ] ], n }, { corner[ f[ 2 ] ], n }, { corner[ f[ 0 ] ], n }, { corner[ f[ 2 ] ], n }, { corner[ f[ 3 ] ], n } } ); + } +} + +} // namespace + +void build_solid( network_definition const &Definition, span_geometry const &Geometry, std::vector &Wires, std::vector &Fittings ) { + + auto const contactradius { 0.5 * wire_diameter( Definition.contact_wire ) }; + auto const messengerradius { 0.5 * wire_diameter( Definition.messenger_wire ) }; + auto const yradius { 0.5 * wire_diameter( Definition.y_wire ) }; + auto const hangerradius { 0.5 * wire_diameter( Definition.hanger_wire ) }; + for( auto const &element : Geometry.elements ) { + switch( element.kind ) { + case element_kind::contact: { add_prism( Wires, element.from, element.to, contactradius, 6 ); break; } + case element_kind::messenger: { add_prism( Wires, element.from, element.to, messengerradius, 6 ); break; } + case element_kind::y_rope: { add_prism( Wires, element.from, element.to, yradius, 5 ); break; } + case element_kind::hanger: { add_prism( Wires, element.from, element.to, hangerradius, 4 ); break; } + case element_kind::link: { add_prism( Wires, element.from, element.to, hangerradius, 4 ); break; } + } + } + // bryły zastępcze w miejscach połączeń; wymiary przybliżone, do zastąpienia modelami uchwytów z katalogu + auto const stack { Definition.type.messenger_stack }; + for( auto const &fitting : Geometry.fittings ) { + auto center { fitting.position }; + switch( fitting.kind ) { + case fitting_kind::rope_clamp: { center.y -= 0.015; add_box( Fittings, center, fitting.along, 0.05, 0.03, 0.06 ); break; } // uchwyt wieszaka na linie (5440) + case fitting_kind::wire_clamp: { center.y += 0.015; add_box( Fittings, center, fitting.along, 0.04, 0.025, 0.05 ); break; } // uchwyt wieszaka na przewodzie (5420) + case fitting_kind::y_clamp: { add_box( Fittings, center, fitting.along, 0.08, 0.035, 0.05 ); break; } // zacisk linki Y na linie nośnej + case fitting_kind::link_clamp: { add_box( Fittings, center, fitting.along, 0.05, 0.03, stack + 0.04 ); break; } // zacisk łączący dwie liny (5430) + } + } +} + +} // namespace catenary diff --git a/world/catenary_network.h b/world/catenary_network.h new file mode 100644 index 00000000..e0806838 --- /dev/null +++ b/world/catenary_network.h @@ -0,0 +1,164 @@ +// Sieć trakcyjna: katalog typów sieci czytany z plików danych oraz geometria przęsła liczona solverem. +// Kod niezależny od reszty symulatora (C++17, tylko biblioteka standardowa), żeby dało się go testować osobno. +#pragma once +#include +#include +#include +#include +#include +#include + +#include "world/catenary_solver.h" + +namespace catenary { + +// przewód albo lina z tabeli przewodów +struct wire_data { + double mass { 0.0 }; // kg/m + double diameter { 0.0 }; // m +}; + +// pełny opis typu sieci: dane dla solvera i dane potrzebne tylko do rysowania +struct network_definition { + network_type type; + std::string hanger_table; // nazwa tablicy rozstawu wieszaków + double contact_spacing { 0.0 }; // m, rozstaw dwóch przewodów jezdnych; 0 = wziąć ze scenerii + wire_data contact_wire; + wire_data messenger_wire; + wire_data y_wire; + wire_data hanger_wire; // linka wieszaków + double y_plan_offset { 0.5 }; // przebieg linki Y w planie na łuku: 0 = przez oś podwieszenia, 1 = prosto od zacisku do zacisku +}; + +// katalog typów sieci. Pliki są czytane przy pierwszym użyciu; każdy błąd zgłaszany jest tylko raz +class type_registry { +public: + // katalog z plikami traction_.txt i traction_tables.txt + void directory( std::string const &Path ); + // odbiorca komunikatów o błędach w danych + std::function on_error; + // zwraca opis typu albo nullptr, gdy pliku nie ma lub jest niekompletny. Wskaźnik pozostaje ważny do końca programu + network_definition const *find( std::string const &Symbol ); + // odstępy wieszaków dla przęsła o podanej rozpiętości: oś podwieszenia–1, 1–2, …, ostatni–oś podwieszenia. + // rozpiętość niecałkowita: wiersz najbliższej całkowitej z rozciągniętymi środkowymi odstępami; + // rozpiętość spoza tablicy: odstępy wyznaczone regułą z odstępem skrajnym i największym odstępem z najbliższego wiersza + std::vector hanger_gaps( std::string const &Table, double Length ); + +private: + void load_tables(); + std::unique_ptr load_type( std::string const &Symbol ); + void report( std::string const &Message ); + + std::mutex m_mutex; + std::string m_directory { "data/" }; + bool m_tablesloaded { false }; + std::map m_wires; + std::map>> m_spacing; + std::map> m_types; // nullptr = typ błędny, już zgłoszony + std::map m_reported; +}; + +// wspólny katalog typów +type_registry &types(); + +struct vec3 { double x, y, z; }; + +// położenie przęsła w scenerii: końce przewodu jezdnego (osi sieci) i końce liny nośnej +struct span_points { + vec3 contact_start, contact_end; + vec3 messenger_start, messenger_end; +}; + +// policzone przęsło +struct span_solution { + double length { 0.0 }; // m, rozpiętość w poziomie + span_result result; +}; + +// liczy przęsło. Ystart, Yend: czy przy danym podwieszeniu jest linka Y (o ile typ ją przewiduje dla tej rozpiętości). +// pusty Hangergaps oznacza odcinek bez wieszaków (do kotwienia albo urządzenia naprężającego): same liny pod własnym ciężarem +// zwraca false i opis w Error, gdy przęsła nie da się policzyć +bool solve_span_points( network_definition const &Definition, std::vector const &Hangergaps, span_points const &Points, + bool Ystart, bool Yend, span_solution &Solution, std::string &Error ); + +// uzgadnia linkę Y przy jednym podwieszeniu z połową linki z sąsiedniego przęsła (patrz join_y w solverze) +void join_y( network_definition const &Definition, span_solution &Solution, bool Atend, y_half const &Neighbour ); + +// połowa linki Y w układzie sceny: dane do wyznaczenia przebiegu linki w planie razem z sąsiednim przęsłem +struct y_side { + bool valid { false }; // false = przy tym podwieszeniu nie ma linki Y + double reach { 0.0 }; // m, odległość zacisku od osi podwieszenia + vec3 junction; // zacisk na linie nośnej + vec3 axis; // punkt liny nośnej w osi podwieszenia +}; + +// przebieg linki Y w planie po jednej stronie podwieszenia: łagodny łuk od zacisku do zacisku +struct y_plan { + bool valid { false }; // false = linka w planie pod cięciwą liny nośnej + double reach { 0.0 }; // m, odległość zacisku w tym przęśle od osi podwieszenia + double total { 0.0 }; // m, odległość między zaciskami mierzona wzdłuż liny nośnej + vec3 start, control, end; // łuk: zacisk w tym przęśle, punkt kierujący, zacisk w sąsiednim (liczą się x i z) + // punkt linki w odległości Distance od osi podwieszenia, w głąb tego przęsła + vec3 point( double Distance ) const; +}; + +// zbiera dane połowy linki Y z policzonego przęsła +y_side make_y_side( span_solution const &Solution, span_points const &Points, bool Atend ); + +// wyznacza przebieg linki Y w planie. Na łuku lina nośna łamie się w osi podwieszenia, a linka Y, zaciśnięta na linie +// po obu stronach, przechodzi obok tego załomu łagodnym łukiem. O tym, jak daleko od załomu, decyduje wpis typu +// y_plan_offset: 0 = linka przechodzi przez oś podwieszenia, 1 = biegnie prosto od zacisku do zacisku. +// Obie strony liczone z tych samych danych dają ten sam przebieg +y_plan solve_y_plan( network_definition const &Definition, y_side const &Mine, y_side const &Other ); + +// elementy przęsła do narysowania +enum class element_kind { contact, messenger, y_rope, hanger, link }; +enum class fitting_kind { rope_clamp, wire_clamp, y_clamp, link_clamp }; + +// odcinek przewodu, liny albo linki +struct span_element { + vec3 from, to; + element_kind kind; +}; + +// miejsce połączenia: uchwyt wieszaka na linie i na przewodzie, zacisk linki Y na linie nośnej, zacisk łączący dwie liny +struct span_fitting { + vec3 position; + vec3 along; // kierunek przewodu w tym miejscu, poziomy, jednostkowy + fitting_kind kind; +}; + +struct span_geometry { + std::vector elements; + std::vector fittings; +}; + +// zbiera elementy przęsła w układzie sceny. +// Fallbackoffset: połowa rozstawu przewodów jezdnych ze scenerii, używana gdy typ nie podaje rozstawu. +// Ystart, Yend: przebieg linek Y w planie; domyślnie pod cięciwą liny nośnej +void build_geometry( network_definition const &Definition, span_solution const &Solution, span_points const &Points, + double Fallbackoffset, span_geometry &Output, y_plan const &Ystart = {}, y_plan const &Yend = {} ); + +// odcinki do narysowania przęsła liniami: kolejne pary punktów w Output +void build_lines( network_definition const &Definition, span_solution const &Solution, span_points const &Points, + double Fallbackoffset, std::vector &Output, y_plan const &Ystart = {}, y_plan const &Yend = {} ); + +// wierzchołek bryły: położenie i wektor normalny do cieniowania +struct solid_vertex { + vec3 position; + vec3 normal; +}; + +// bryły do oglądania z bliska: przewody i liny jako graniastosłupy o średnicach z tabeli przewodów (Wires), w miejscach +// połączeń prostopadłościany zastępcze, do zastąpienia modelami uchwytów (Fittings). Kolejne trójki wierzchołków to trójkąty. +// Przewody i osprzęt są osobno, bo rysowane są z różnych materiałów +void build_solid( network_definition const &Definition, span_geometry const &Geometry, std::vector &Wires, std::vector &Fittings ); + +// średnica przewodu: z tabeli, a gdy jej tam nie ma, oszacowana z masy jak dla linki miedzianej +double wire_diameter( wire_data const &Wire ); + +// to samo w jednym kroku dla przęsła liczonego osobno, z linką Y przy obu podwieszeniach +bool build_span_lines( network_definition const &Definition, std::vector const &Hangergaps, span_points const &Points, + double Fallbackoffset, std::vector &Output, std::string &Error ); + +} // namespace catenary diff --git a/world/catenary_scene.cpp b/world/catenary_scene.cpp new file mode 100644 index 00000000..5bf8bcad --- /dev/null +++ b/world/catenary_scene.cpp @@ -0,0 +1,1602 @@ +/* +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 "world/catenary_scene.h" + +#include "world/catenary_suspension.h" +#include "world/catenary_cantilever.h" +#include "world/catenary_span.h" +#include "world/catenary_layout.h" +#include "world/catenary_network.h" +#include "world/catenary_tensioner.h" +#include "world/Traction.h" +#include "model/AnimModel.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" + +#include +#include +#include "utilities/Logs.h" +#include "utilities/parser.h" + +#include +#include +#include +#include +#include + +// części wysięgników w scenerii (world/catenary_scene_objects.cpp, w testach atrapa): odłączenie od pliku scenerii i usunięcie +void catenary_part_detach( TAnimModel *Model ); +void catenary_part_delete( TAnimModel *Model ); +// przęsła łańcuchów (tamże): Body to treść wpisu node traction od nazwy zasilacza do endtraction włącznie. +// Link: od razu połączyć z sąsiednimi przęsłami i zasilaczem (po inicjalizacji scenerii; wcześniej robi to InitTraction) +TTraction *catenary_span_create( std::string const &Name, double RangeMax, double RangeMin, std::string const &Body, bool Link ); +void catenary_span_delete( TTraction *Span ); + +namespace { + +// katalog z plikami danych sieci, z ukośnikiem na końcu +std::string Datadirectory { "data/" }; + +// podwieszenie powiązane z obiektami scenerii +struct scene_suspension { + catenary::suspension data; + std::string skin; // wymienna tekstura: własna wpisu albo domyślna z pliku + TAnimModel *support { nullptr }; // konstrukcja wsporcza + std::vector messenger; // przęsła, których linę nośną trzyma + std::vector contact; // przęsła, których przewód jezdny trzyma + std::string file; // plik sieci, z którego pochodzi wpis (albo do którego trafi) + std::vector parts; // części wysięgnika wstawione do scenerii + bool removed { false }; // usunięty w edytorze +}; + +std::vector Suspensions; + +// plik sieci znany scenerii +struct scene_file { + std::string name; // tak, jak w dyrektywie (względem katalogu scenerii) + std::string skin; // domyślna tekstura z pliku + std::string header; // komentarze z początku pliku, zachowywane przy zapisie + bool created { false }; // założony w edytorze, sceneria go jeszcze nie dołącza + bool changed { false }; // są zmiany do zapisania +}; +std::vector Files; + +// łańcuch sieci z pliku: przęsła od podwieszenia do podwieszenia +struct scene_chain { + catenary::chain data; + std::string file; + std::vector spans; // przęsło k: od podwieszenia k do k+1; nullptr gdy nie dało się go zrobić + bool removed { false }; +}; +std::vector Chains; + +// bramka z pliku: słupy i dźwigar wstawione jako modele +struct scene_gantry { + catenary::gantry data; + std::string file; + std::vector parts; + bool removed { false }; +}; +std::vector Gantries; + +// kotwienie ciężarowe z pliku: słup kotwowy i urządzenie naprężające; końce drutów dla łańcuchów +struct scene_anchor { + catenary::anchor data; + std::string file; + std::vector parts; + bool removed { false }; + bool solved { false }; + double messenger[ 3 ] { 0.0, 0.0, 0.0 }; // koniec liny nośnej (uchwyt krańcowy), współrzędne scenerii + double contact[ 3 ] { 0.0, 0.0, 0.0 }; // koniec przewodów jezdnych (środek między przewodami) + double temperature { 0.0 }; // temperatura, przy której złożono + catenary::tensioner_result result; +}; +std::vector Anchors; + +// bramka wieloprzęsłowa: przęsła (wpisy gantry) stykają się na wspólnych słupach - ten sam punkt (w planie do 0,05 m). +// Model wspólnego słupa stawia przęsło o najniższym numerze; true, gdy słup K przęsła Index stawia inne przęsło +bool +mast_shared_earlier( std::size_t const Index, int const K ) { + auto const &point { Gantries[ Index ].data.points[ K ] }; + if( Gantries[ Index ].data.masts[ K ].empty() ) { return false; } + for( std::size_t other = 0; other < Index && other < Gantries.size(); ++other ) { + if( Gantries[ other ].removed ) { continue; } + for( int k = 0; k < 2; ++k ) { + if( Gantries[ other ].data.masts[ k ].empty() ) { continue; } + auto const dx { Gantries[ other ].data.points[ k ][ 0 ] - point[ 0 ] }, dz { Gantries[ other ].data.points[ k ][ 2 ] - point[ 2 ] }; + if( dx * dx + dz * dz < 0.0025 ) { return true; } + } + } + return false; +} + +// przęsła, które mają słup we wskazanym punkcie (poza Except) +std::vector +gantries_at( double const *Point, std::size_t const Except ) { + std::vector result; + for( std::size_t other = 0; other < Gantries.size(); ++other ) { + if( other == Except || Gantries[ other ].removed ) { continue; } + for( int k = 0; k < 2; ++k ) { + if( Gantries[ other ].data.masts[ k ].empty() ) { continue; } + auto const dx { Gantries[ other ].data.points[ k ][ 0 ] - Point[ 0 ] }, dz { Gantries[ other ].data.points[ k ][ 2 ] - Point[ 2 ] }; + if( dx * dx + dz * dz < 0.0025 ) { result.push_back( other ); break; } + } + } + return result; +} + +// bramka, której słup Mast stoi w punkcie Point (oś słupa na wysokości główki szyny, w planie do 0,10 m); nullptr gdy żadna +catenary::gantry const * +gantry_at_mast( std::string const &Mast, double const *Point ) { + for( auto const &gantry : Gantries ) { + if( gantry.removed ) { continue; } + for( int k = 0; k < 2; ++k ) { + if( gantry.data.masts[ k ] != Mast ) { continue; } + auto const dx { gantry.data.points[ k ][ 0 ] - Point[ 0 ] }, dz { gantry.data.points[ k ][ 2 ] - Point[ 2 ] }; + if( dx * dx + dz * dz < 0.01 ) { return &gantry.data; } + } + } + return nullptr; +} + +scene_file * +file_named( std::string const &Name ) { + for( auto &file : Files ) { if( file.name == Name ) { return &file; } } + return nullptr; +} +// ile podwieszeń z początku listy jest już powiązanych i złożonych +std::size_t Resolved { 0 }; +// czy sceneria przeszła już inicjalizację (FirstInit); plik podwieszeń dołączony później jest wiązany od razu +bool Initialized { false }; +// uwagi solvera już wypisane do logu; ta sama uwaga dla tej samej karty idzie tylko raz, żeby nie zalać logu +std::set Notes; + +// katalog wysięgników: części, zestawy ramion i schematy kart (traction_cantilevers.txt) oraz słupy i mocowania +// (traction_supports.txt); czytany przy pierwszym użyciu +catenary::cantilever_catalog const * +cantilevers() { + + static catenary::cantilever_catalog catalog; + static bool loaded { false }; + static bool good { false }; + if( false == loaded ) { + loaded = true; + std::vector errors; + auto const filename { Datadirectory + "traction_cantilevers.txt" }; + good = catalog.load( filename, errors ); + if( false == good ) { + ErrorLog( "Catenary: file \"" + filename + "\" is missing, cantilevers won't be assembled" ); + } + // słupy i mocowania do słupów: osobny plik pisany ręcznie (generator go nie rusza), dopisywany do tego samego katalogu + if( good ) { + auto const supports { Datadirectory + "traction_supports.txt" }; + if( false == catalog.load( supports, errors ) ) { + ErrorLog( "Catenary: file \"" + supports + "\" is missing, cantilevers on poles won't be assembled" ); + } + } + for( auto const &error : errors ) { + ErrorLog( error ); + } + } + return ( good ? &catalog : nullptr ); +} + +// zasady rozstawiania konstrukcji (traction_layout.txt); czytane przy pierwszym użyciu, brak pliku = pusta tablica +catenary::layout_tables const & +layout() { + + static catenary::layout_tables tables; + static bool loaded { false }; + if( false == loaded ) { + loaded = true; + std::vector errors; + auto const filename { Datadirectory + "traction_layout.txt" }; + if( false == tables.load( filename, errors ) ) { + WriteLog( ( "Catenary: file \"" + filename + "\" is missing, spans won't be checked against the catalogue" ).c_str() ); + } + for( auto const &error : errors ) { ErrorLog( error ); } + } + return tables; +} + +std::string +text_number( double const Value, int const Precision = 4 ) { + + std::ostringstream stream; + stream.imbue( std::locale::classic() ); + stream.setf( std::ios::fixed ); + stream.precision( Precision ); + stream << Value; + return stream.str(); +} + +// składa wysięgnik podwieszenia z części katalogowych i wstawia części do scenerii jako egzemplarze modeli. +// zwraca liczbę wstawionych części; 0 gdy wysięgnika nie dało się złożyć +// Wire: oś przewodów jezdnych przy podwieszeniu, Rope: oś liny nośnej, Attach: pion przez zaczepy izolatorów, +// a przy wpisie ze słupem oś słupa na wysokości główki szyny. Umbrella: ustawienia strony parasola (tylko przy słupie); +// Probe: tylko złożenie bez wstawiania części, wynik solvera trafia tam (zwraca 1, gdy się złożył) +std::size_t +assemble( scene_suspension &Suspension, catenary::cantilever_catalog const &Catalog, glm::dvec3 const &Wire, glm::dvec3 const *Rope, glm::dvec3 const &Attach, + catenary::cantilever_umbrella const *Umbrella = nullptr, catenary::cantilever_result *Probe = nullptr ) { + + auto const &data { Suspension.data }; + auto const label { "Bad scenario: suspension \"" + ( data.name.empty() ? std::string( "none" ) : data.name ) + "\" " }; + // przy próbnym złożeniu (edytor) przyczyna trafia do wyniku zamiast do logu + auto const report = [ & ]( std::string const &Reason ) { + if( Probe != nullptr ) { Probe->warnings.push_back( Reason ); } + else { ErrorLog( label + Reason ); } + }; + auto const *scheme { Catalog.scheme( data.card, data.execution ) }; + if( scheme == nullptr ) { + report( "uses scheme " + std::to_string( data.card ) + " " + std::to_string( data.execution ) + " which isn't in the cantilever catalogue" ); + return 0; + } + // kierunek poziomy od przewodów do konstrukcji wyznacza płaszczyznę wysięgnika + auto const towardx { Attach.x - Wire.x }; + auto const towardz { Attach.z - Wire.z }; + auto const reach { std::sqrt( towardx * towardx + towardz * towardz ) }; + if( reach < 0.5 ) { + report( "has its attachment point less than 0.5 m from the contact wires" ); + return 0; + } + auto const heading { std::atan2( towardx, towardz ) }; + // lina nośna w układzie wysięgnika; bez podanego punktu jest nad przewodami na wysokości konstrukcyjnej schematu + catenary::cantilever_input input; + if( Rope != nullptr ) { + input.messenger.y = ( ( Rope->x - Wire.x ) * towardx + ( Rope->z - Wire.z ) * towardz ) / reach; + input.messenger.z = Rope->y - Wire.y; + } + else { + input.messenger.z = scheme->height; + } + // zaczepy izolatorów na wysokościach ze schematu; zaczep odciągu idzie za liną nośną, gdy ta wisi inaczej niż w schemacie, + // a zaczep ukośnika razem z nim tam, gdzie schemat trzyma je w stałym odstępie (karty z trzema zaczepami) + auto const raise { input.messenger.z - scheme->height }; + input.stay_mount = { reach, scheme->mount_stay + raise }; + input.strut_mount = { reach, scheme->mount_strut + ( scheme->steady_mounted ? raise : 0.0 ) }; + input.steady_mount = { reach, scheme->mount_steady }; + // zestaw izolatorów (np. "25" - 25 kV): każdy izolator schematu NNNN-xx podmieniony na NNNN-, jeśli taka część jest + if( false == data.insulators.empty() ) { + for( auto const &role : scheme->roles ) { + auto const *part { Catalog.part( role.second ) }; + if( part == nullptr || part->kind != "insulator" ) { continue; } + auto const dash { role.second.find( '-' ) }; + auto const variant { role.second.substr( 0, dash == std::string::npos ? role.second.size() : dash ) + "-" + data.insulators }; + if( Catalog.part( variant ) != nullptr ) { input.overrides[ role.first ] = variant; } + else if( Probe == nullptr ) { ErrorLog( label + "asks for insulator set \"" + data.insulators + "\", but part \"" + variant + "\" isn't in the cantilever catalogue; \"" + role.second + "\" kept" ); } + } + } + for( auto const &replacement : data.overrides ) { + input.overrides[ replacement.first ] = replacement.second; + } + + catenary::cantilever_pole const *pole { nullptr }; + catenary::cantilever_pole onmast; + if( data.has_pole ) { + pole = Catalog.pole( data.pole_type ); + if( pole == nullptr ) { + report( "uses pole \"" + data.pole_type + "\" which isn't in the cantilever catalogue" ); + return 0; + } + if( pole->gantry ) { + // wysięgnik na słupie bramki: mocowania najwyżej pod dźwigarem, na poziomie tej bramki, której słup tu stoi + onmast = *pole; + if( auto const *gantry { gantry_at_mast( data.pole_type, data.pole ) }; gantry != nullptr ) { + onmast.mount_max = ( gantry->high ? pole->mast_high : pole->mast_low ) - pole->below_beam; + } + else { + report( "stands on mast \"" + data.pole_type + "\", but no gantry has that mast there; the mounts go under the lower beam level" ); + } + pole = &onmast; + } + } + // Drop: obniżenie dolnego zaczepu ukośnika (dobór przy innym zestawie izolatorów, niżej) + auto const solve = [ & ]( std::map const &Overrides, double const Drop, catenary::cantilever_result &Result ) { + if( pole != nullptr ) { + // słup z katalogu: zaczepy, mocowania i słup liczy solver; Attach to oś słupa na wysokości główki szyny + auto lowered { *scheme }; + lowered.mount_strut -= Drop; + return catenary::solve_cantilever_on_pole( Catalog, lowered, *pole, reach, Attach.y - Wire.y, input.messenger, Overrides, + ( Umbrella != nullptr ? *Umbrella : catenary::cantilever_umbrella() ), Result ); + } + auto attempt { input }; + attempt.overrides = Overrides; + attempt.strut_mount.z -= Drop; + return catenary::solve_cantilever( Catalog, *scheme, attempt, Result ); + }; + // inny zestaw izolatorów (25 kV) bez kart katalogowych (Jan 2026-10-17: „lepić na oko”): rury ukośnika, odciągu i wysięgu + // z izolatorem dobierane z tej samej rodziny części - długość bliska pierwotnej minus przyrost izolatora i po jednym + // kroku w obie strony; wygrywa złożenie z najmniejszą liczbą uwag solvera, przy równej - najbliższe tej długości. + // Rury podane w scenerii przez override nie są ruszane. Gdy mimo to zostają uwagi (zwykle uchwyt wysięgu 4050 wchodzi na + // dłuższy izolator ukośnika - Jan 2026-10-17, 1232), dolny zaczep ukośnika schodzi co 0,1 m (najwyżej 0,5 m), aż uwag + // nie będzie; zostaje najmniejsze obniżenie z najmniejszą liczbą uwag. + std::vector>> fits; + if( false == data.insulators.empty() ) { + for( auto const *tube : { "strut", "stay", "steady" } ) { + auto const role { scheme->roles.find( tube ) }, insulator { scheme->roles.find( std::string( tube ) + "_insulator" ) }; + auto const replaced { input.overrides.find( std::string( tube ) + "_insulator" ) }; + if( role == scheme->roles.end() || insulator == scheme->roles.end() || replaced == input.overrides.end() ) { continue; } + if( std::any_of( data.overrides.begin(), data.overrides.end(), [ & ]( std::pair const &O ) { return O.first == tube; } ) ) { continue; } + auto const *original { Catalog.part( role->second ) }; + auto const *oldins { Catalog.part( insulator->second ) }, *newins { Catalog.part( replaced->second ) }; + auto const dash { role->second.find( '-' ) }; + if( original == nullptr || oldins == nullptr || newins == nullptr || dash == std::string::npos ) { continue; } + auto const family { role->second.substr( 0, dash + 1 ) }; + auto const target { original->length - ( newins->length - oldins->length ) }; + std::vector> sorted; + for( auto const &part : Catalog.parts() ) { + if( part.first.compare( 0, family.size(), family ) == 0 && part.second.kind == original->kind ) { sorted.emplace_back( part.second.length, part.first ); } + } + std::sort( sorted.begin(), sorted.end() ); + if( sorted.empty() ) { continue; } + std::size_t nearest { 0 }; + for( std::size_t index = 1; index < sorted.size(); ++index ) { + if( std::abs( sorted[ index ].first - target ) < std::abs( sorted[ nearest ].first - target ) ) { nearest = index; } + } + std::vector candidates { sorted[ nearest ].second }; // pierwszy = najbliższy + if( nearest > 0 ) { candidates.push_back( sorted[ nearest - 1 ].second ); } + if( nearest + 1 < sorted.size() ) { candidates.push_back( sorted[ nearest + 1 ].second ); } + fits.emplace_back( tube, candidates ); + } + } + catenary::cantilever_result result; + auto solved { false }; + if( data.insulators.empty() ) { + solved = solve( input.overrides, 0.0, result ); + } + else { + auto best { std::numeric_limits::max() }; + std::size_t baseline { 0 }; // uwagi niezależne od doboru (np. mocowanie odciągu ścięte do mount_max) + { catenary::cantilever_result plain; solve( input.overrides, 0.0, plain ); + for( auto const &warning : plain.warnings ) { if( warning.find( "highest mount position" ) != std::string::npos ) { ++baseline; } } } + for( int step = 0; step <= 5 && best > baseline; ++step ) { + auto const drop { 0.1 * step }; + std::vector choice( fits.size(), 0 ); + while( true ) { + auto overrides { input.overrides }; + for( std::size_t index = 0; index < fits.size(); ++index ) { overrides[ fits[ index ].first ] = fits[ index ].second[ choice[ index ] ]; } + catenary::cantilever_result attempt; + if( solve( overrides, drop, attempt ) && attempt.warnings.size() < best ) { + best = attempt.warnings.size(); + result = attempt; + result.values[ "strut_mount.drop" ] = drop; + solved = true; + } + // następna kombinacja (kolejność: najbliższe długości najpierw, więc przy równej liczbie uwag zostaje pierwsza) + std::size_t index { 0 }; + while( index < fits.size() && ++choice[ index ] == fits[ index ].second.size() ) { choice[ index ] = 0; ++index; } + if( index == fits.size() ) { break; } + } + } + if( false == solved ) { solved = solve( input.overrides, 0.0, result ); } + } + if( Probe != nullptr ) { *Probe = result; return ( solved ? 1 : 0 ); } + // uwagi solvera to informacja o dopasowaniu części, nie błąd scenerii; błędem jest dopiero wysięgnik, którego nie da się złożyć + for( auto const &warning : result.warnings ) { + if( solved ) { + if( Notes.insert( warning ).second ) { WriteLog( ( "Catenary: " + warning + " (first seen in suspension \"" + ( data.name.empty() ? std::string( "none" ) : data.name ) + "\")" ).c_str() ); } + } + else { ErrorLog( label + "can't be assembled: " + warning ); } + } + if( false == solved ) { return 0; } + + // każda część to zwykły egzemplarz modelu, taki sam jak z wpisu node model + std::size_t count { 0 }; + auto const skin { Suspension.skin.empty() ? std::string( "none" ) : Suspension.skin }; + for( auto const &placement : result.parts ) { + auto const part { catenary::cantilever_to_scene( placement, heading ) }; + auto entry { + "node " + text_number( data.range_max, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " + + ( placement.model.empty() ? Catalog.models() + placement.part : placement.model ) + ".t3d " + skin + + " angles " + text_number( part.angles[ 0 ] ) + " " + text_number( part.angles[ 1 ] ) + " " + text_number( part.angles[ 2 ] ) }; + if( part.scale[ 0 ] != 1.0 || part.scale[ 1 ] != 1.0 || part.scale[ 2 ] != 1.0 ) { + entry += " scale " + text_number( part.scale[ 0 ], 5 ) + " " + text_number( part.scale[ 1 ], 5 ) + " " + text_number( part.scale[ 2 ], 5 ); + } + entry += " endmodel"; + glm::dvec3 const position { Wire.x + part.offset[ 0 ], Wire.y + part.offset[ 1 ], Wire.z + part.offset[ 2 ] }; + if( auto *model { simulation::State.create_model( entry, "none", position ) }; model != nullptr ) { + // części należą do pliku sieci, nie do pliku scenerii: zapis scenerii w edytorze ich nie przepisuje + catenary_part_detach( model ); + Suspension.parts.push_back( model ); + ++count; + } + } + return count; +} + + +// punkty wpisu z punktami podanymi wprost; Attach to oś słupa (wpis ze słupem) albo pion przez zaczepy +void +entry_points( catenary::suspension const &Data, glm::dvec3 &Wire, glm::dvec3 &Rope, glm::dvec3 &Attach ) { + Wire = { Data.at[ 0 ], Data.at[ 1 ], Data.at[ 2 ] }; + Rope = { Data.rope[ 0 ], Data.rope[ 1 ], Data.rope[ 2 ] }; + Attach = ( Data.has_pole ? glm::dvec3 { Data.pole[ 0 ], Data.pole[ 1 ], Data.pole[ 2 ] } : glm::dvec3 { Data.attach[ 0 ], Data.attach[ 1 ], Data.attach[ 2 ] } ); +} + +// składa wysięgnik wpisu z punktami, bez parasola; zwraca liczbę wstawionych części +std::size_t +assemble_alone( std::size_t const Index, catenary::cantilever_catalog const &Catalog ) { + auto &suspension { Suspensions[ Index ] }; + glm::dvec3 wire, rope, attach; + entry_points( suspension.data, wire, rope, attach ); + return assemble( suspension, Catalog, wire, ( suspension.data.has_rope ? &rope : nullptr ), attach ); +} + +// parasol: oba wysięgniki składane najpierw osobno, żeby poznać wysokości mocowań, potem razem na wspólnym słupie +void +assemble_umbrella( std::string const &Name, std::vector const &List, catenary::cantilever_catalog const &Catalog, std::size_t &Assembled, std::size_t &Parts ) { + auto const alone = [ & ]( std::size_t const Index ) { + auto const count { assemble_alone( Index, Catalog ) }; + if( count > 0 ) { ++Assembled; Parts += count; } + }; + auto const &first { Suspensions[ List.front() ].data }; + auto const *pole { Catalog.pole( first.pole_type ) }; + auto paired { List.size() == 2 && pole != nullptr }; + if( List.size() != 2 ) { + ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has " + std::to_string( List.size() ) + " suspension(s), it needs exactly two; they're assembled separately" ); + } + else { + auto const &second { Suspensions[ List.back() ].data }; + auto const dx { second.pole[ 0 ] - first.pole[ 0 ] }, dy { second.pole[ 1 ] - first.pole[ 1 ] }, dz { second.pole[ 2 ] - first.pole[ 2 ] }; + if( second.pole_type != first.pole_type || std::sqrt( dx * dx + dy * dy + dz * dz ) > 0.05 ) { + ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has its suspensions on different poles; they're assembled separately" ); + paired = false; + } + } + if( false == paired ) { for( auto const index : List ) { alone( index ); } return; } + glm::dvec3 wires[ 2 ], ropes[ 2 ], poles[ 2 ]; + catenary::cantilever_result probes[ 2 ]; + auto probed { true }; + for( int side = 0; side < 2; ++side ) { + auto &suspension { Suspensions[ List[ side ] ] }; + entry_points( suspension.data, wires[ side ], ropes[ side ], poles[ side ] ); + probed = probed && assemble( suspension, Catalog, wires[ side ], ( suspension.data.has_rope ? &ropes[ side ] : nullptr ), poles[ 0 ], nullptr, &probes[ side ] ) > 0; + } + if( false == probed ) { for( auto const index : List ) { alone( index ); } return; } + catenary::cantilever_umbrella sides[ 2 ]; + catenary::umbrella_mounts( Catalog, *pole, probes[ 0 ], probes[ 1 ], sides[ 0 ], sides[ 1 ] ); + sides[ 1 ].pole = false; + for( int side = 0; side < 2; ++side ) { + auto &suspension { Suspensions[ List[ side ] ] }; + auto const count { assemble( suspension, Catalog, wires[ side ], ( suspension.data.has_rope ? &ropes[ side ] : nullptr ), poles[ 0 ], &sides[ side ] ) }; + if( count > 0 ) { ++Assembled; Parts += count; } + } +} + +// usuwa ze scenerii części wysięgnika podwieszenia +void +drop_parts( scene_suspension &Suspension ) { + for( auto *part : Suspension.parts ) { catenary_part_delete( part ); } + Suspension.parts.clear(); +} + +} // namespace + +// końce spodu dźwigara bramki: każdy przy ścianie swojego słupa (oś słupa przesunięta o jego offset w stronę drugiego słupa), +// na prześwicie tego słupa (dolne albo górne mocowanie) +void +gantry_beam_ends( catenary::gantry const &Data, catenary::gantry_mast const &First, catenary::gantry_mast const *Second, double const Span, double *From, double *To ) { + glm::dvec3 const a { Data.points[ 0 ][ 0 ], Data.points[ 0 ][ 1 ], Data.points[ 0 ][ 2 ] }; + glm::dvec3 const b { Data.points[ 1 ][ 0 ], Data.points[ 1 ][ 1 ], Data.points[ 1 ][ 2 ] }; + auto const plan { glm::dvec3 { b.x - a.x, 0.0, b.z - a.z } }; + auto const length { std::max( glm::length( plan ), 1e-6 ) }; + auto const inward { plan * ( 1.0 / length ) }; + auto const from { a + inward * First.offset + glm::dvec3 { 0.0, Data.high ? First.mount_high : First.mount_low, 0.0 } }; + // pojedynczy element (bez drugiego słupa): poziomo od ściany słupa na swoją długość + auto const to { Second != nullptr ? b - inward * Second->offset + glm::dvec3 { 0.0, Data.high ? Second->mount_high : Second->mount_low, 0.0 } : from + inward * Span }; + From[ 0 ] = from.x; From[ 1 ] = from.y; From[ 2 ] = from.z; + To[ 0 ] = to.x; To[ 1 ] = to.y; To[ 2 ] = to.z; +} + +namespace { + +// wstawia słupy i dźwigar bramki (albo usuwa, gdy bramka usunięta). zwraca liczbę wstawionych modeli +std::size_t +assemble_gantry( scene_gantry &Gantry ) { + for( auto *part : Gantry.parts ) { catenary_part_delete( part ); } + Gantry.parts.clear(); + if( Gantry.removed ) { return 0; } + auto const *catalog { cantilevers() }; + if( catalog == nullptr ) { return 0; } + auto const &data { Gantry.data }; + auto const label { "Bad scenario: gantry \"" + ( data.name.empty() ? std::string( "none" ) : data.name ) + "\" " }; + auto const *first { catalog->mast( data.masts[ 0 ] ) }; + auto const single { data.masts[ 1 ].empty() }; + auto const *second { single ? nullptr : catalog->mast( data.masts[ 1 ] ) }; + auto const *beam { catalog->beam( data.beam ) }; + if( first == nullptr || ( second == nullptr && false == single ) || beam == nullptr ) { + ErrorLog( label + "uses a mast or a beam which isn't in the supports catalogue (traction_supports.txt)" ); + return 0; + } + if( single != beam->single ) { + ErrorLog( label + ( single ? "has one mast, but beam \"" + beam->name + "\" isn't a single element" : "has two masts, but beam \"" + beam->name + "\" is a single element" ) ); + return 0; + } + if( single && false == first->through ) { + ErrorLog( label + "has a single beam element on mast \"" + first->name + "\", which is an end mast; it goes on a through mast" ); + } + glm::dvec3 const a { data.points[ 0 ][ 0 ], data.points[ 0 ][ 1 ], data.points[ 0 ][ 2 ] }; + glm::dvec3 const b { data.points[ 1 ][ 0 ], data.points[ 1 ][ 1 ], data.points[ 1 ][ 2 ] }; + auto const dx { b.x - a.x }, dz { b.z - a.z }; + auto const distance { std::sqrt( dx * dx + dz * dz ) }; + if( distance < ( single ? 0.1 : 1.0 ) ) { ErrorLog( label + ( single ? "has no direction of its single beam element" : "has its masts less than 1 m apart" ) ); return 0; } + // między ścianami słupów jest o 0,20 m mniej niż numer dźwigara (E101: 9,9 m - użytkownik), osie słupów o offset każdego dalej + if( false == single && std::abs( distance - catenary::gantry_spacing( *beam, *first, *second ) ) > 0.05 ) { + WriteLog( ( "Catenary: gantry \"" + data.name + "\" has its masts " + text_number( distance, 3 ) + " m apart, beam \"" + beam->name + "\" with these masts needs " + + text_number( catenary::gantry_spacing( *beam, *first, *second ), 3 ) + " m" ).c_str() ); + } + // układ jak wysięgnika: Y wzdłuż dźwigara (od pierwszego słupa do drugiego), X wzdłuż toru, Z w górę + auto const heading { std::atan2( dx, dz ) }; + std::vector placements; + auto const add = [ & ]( std::string const &Model, double const X, double const Y, double const Z ) { + catenary::cantilever_placement placement; + placement.model = Model; + placement.position[ 0 ] = X; + placement.position[ 1 ] = Y; + placement.position[ 2 ] = Z; + // słup: X modelu (szerokość) wzdłuż dźwigara, jak dźwigar + double const turned[ 9 ] { 0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 }; + std::copy( std::begin( turned ), std::end( turned ), placement.rotation ); + placements.push_back( placement ); + }; + // bramka wieloprzęsłowa: wspólny słup stawia tylko jedno przęsło; dźwigary z obu stron może mieć tylko słup przelotowy + auto const index { static_cast( &Gantry - Gantries.data() ) }; + auto const shared = [ & ]( int const K ) { return index < Gantries.size() && mast_shared_earlier( index, K ); }; + for( int k = 0; k < 2; ++k ) { + auto const *type { k == 0 ? first : second }; + if( type == nullptr ) { continue; } + if( index < Gantries.size() && false == type->through && false == gantries_at( data.points[ k ], index ).empty() ) { + ErrorLog( label + "has beams on both sides of mast \"" + type->name + "\", which is an end mast; a through mast is needed there" ); + } + } + if( false == shared( 0 ) ) { add( first->model, 0.0, 0.0, 0.0 ); } + if( second != nullptr && false == shared( 1 ) ) { add( second->model, 0.0, distance, b.y - a.y ); } + // dźwigar: każdy koniec na mocowaniu swojego słupa (przesunięcie wzdłuż toru i prześwit), więc przy słupach różnej + // szerokości albo prześwitu leży lekko skośnie + double from[ 3 ], to[ 3 ]; + gantry_beam_ends( data, *first, second, beam->span, from, to ); + glm::dvec3 const ends[ 2 ] { { from[ 0 ], from[ 1 ], from[ 2 ] }, { to[ 0 ], to[ 1 ], to[ 2 ] } }; + auto const beamx { ends[ 1 ].x - ends[ 0 ].x }, beamz { ends[ 1 ].z - ends[ 0 ].z }; + auto const beamheading { std::atan2( beamx, beamz ) }; + auto const pitch { std::atan2( ends[ 1 ].y - ends[ 0 ].y, std::sqrt( beamx * beamx + beamz * beamz ) ) }; + catenary::cantilever_placement beamplacement; + beamplacement.model = beam->model; + { + auto const c { std::cos( pitch ) }, s { std::sin( pitch ) }; + double const turned[ 9 ] { 0.0, -1.0, 0.0, c, 0.0, -s, s, 0.0, c }; + std::copy( std::begin( turned ), std::end( turned ), beamplacement.rotation ); + } + auto const skin { data.skin.empty() ? std::string( "none" ) : data.skin }; + std::size_t count { 0 }; + std::vector> all; + for( auto const &placement : placements ) { all.emplace_back( placement, heading ); } + all.emplace_back( beamplacement, beamheading ); + for( auto const &item : all ) { + auto const &placement { item.first }; + auto const part { catenary::cantilever_to_scene( placement, item.second ) }; + auto const entry { + "node " + text_number( data.range_max, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " + placement.model + ".t3d " + skin + + " angles " + text_number( part.angles[ 0 ] ) + " " + text_number( part.angles[ 1 ] ) + " " + text_number( part.angles[ 2 ] ) + " endmodel" }; + auto const &origin { &placement == &all.back().first ? ends[ 0 ] : a }; + glm::dvec3 const position { origin.x + part.offset[ 0 ], origin.y + part.offset[ 1 ], origin.z + part.offset[ 2 ] }; + if( auto *model { simulation::State.create_model( entry, "none", position ) }; model != nullptr ) { + catenary_part_detach( model ); + Gantry.parts.push_back( model ); + ++count; + } + } + return count; +} + +// urządzenia naprężające (traction_tensioners.txt); czytane przy pierwszym użyciu +catenary::tensioner_catalog const * +tensioners() { + + static catenary::tensioner_catalog catalog; + static bool loaded { false }; + static bool good { false }; + if( false == loaded ) { + loaded = true; + std::vector errors; + auto const filename { Datadirectory + "traction_tensioners.txt" }; + good = catalog.load( filename, errors ); + if( false == good ) { ErrorLog( "Catenary: file \"" + filename + "\" is missing, anchors won't be assembled" ); } + for( auto const &error : errors ) { ErrorLog( error ); } + } + return ( good ? &catalog : nullptr ); +} + +// składa kotwienie: słup i urządzenie naprężające przy temperaturze powietrza; zapamiętuje końce drutów. +// Układ urządzenia (X od słupa w stronę drutów, Y w lewo, Z w górę) to układ wysięgnika obrócony tak, że jego X leży +// wzdłuż kierunku drutów - części eksportowane wzdłuż toru (Jan 2026-10-23) stoją wtedy w nim wprost +std::size_t +assemble_anchor( scene_anchor &Anchor ) { + for( auto *part : Anchor.parts ) { catenary_part_delete( part ); } + Anchor.parts.clear(); + Anchor.solved = false; + if( Anchor.removed ) { return 0; } + auto const &data { Anchor.data }; + auto const label { "Bad scenario: anchor \"" + ( data.name.empty() ? std::string( "none" ) : data.name ) + "\" " }; + auto const *catalog { cantilevers() }; + auto const *devices { tensioners() }; + if( catalog == nullptr || devices == nullptr ) { return 0; } + auto const *device { devices->device( data.device ) }; + if( device == nullptr ) { ErrorLog( label + "uses device \"" + data.device + "\" which isn't in traction_tensioners.txt" ); return 0; } + auto const *pole { catalog->pole( data.pole_type ) }; + if( pole == nullptr ) { ErrorLog( label + "uses pole \"" + data.pole_type + "\" which isn't in the supports catalogue" ); return 0; } + catenary::tensioner_input input; + if( false == data.network.empty() ) { + if( auto const *type { catenary::types().find( data.network ) }; type != nullptr ) { + input.type = type->type.code; + input.contacts = type->type.contact_count; + } + else { ErrorLog( label + "uses network type \"" + data.network + "\" which has no file" ); } + } + input.temperature = Global.AirTemperature; + input.distance = data.distance; + input.elongation = data.elongation; + input.missing = data.missing; + input.ground = data.ground; + input.color = data.color; + input.top_color = data.top_color; + auto &result { Anchor.result }; + if( false == catenary::solve_tensioner( *devices, *device, input, result ) ) { + for( auto const &warning : result.warnings ) { ErrorLog( label + "can't be assembled: " + warning ); } + return 0; + } + for( auto const &warning : result.warnings ) { + if( Notes.insert( warning ).second ) { WriteLog( ( "Catenary: " + warning + " (first seen in anchor \"" + data.name + "\")" ).c_str() ); } + } + if( auto const *cord { catalog->part( device->rope ) }; cord != nullptr ) { + catenary::tensioner_rope_parts( result, device->rope, cord->length, cord->girth ); + } + else { ErrorLog( label + "rope part \"" + device->rope + "\" isn't in the cantilever catalogue, the rope isn't drawn" ); } + Anchor.temperature = Global.AirTemperature; + + // kierunek drutów w planie; zero urządzenia na czole słupa + glm::dvec3 const axis { data.pole[ 0 ], data.pole[ 1 ], data.pole[ 2 ] }; + auto dx { data.toward[ 0 ] - axis.x }, dz { data.toward[ 2 ] - axis.z }; + auto const along { std::sqrt( dx * dx + dz * dz ) }; + if( along < 1.0 ) { ErrorLog( label + "has its \"toward\" point less than 1 m from the pole" ); return 0; } + dx /= along; + dz /= along; + auto const table { devices->table( device->table ) }; + auto const face { 0.5 * pole->depth( table != nullptr ? table->height : 6.7 ) }; + glm::dvec3 const origin { axis.x + dx * face, axis.y, axis.z + dz * face }; + auto const heading { std::atan2( dz, -dx ) }; + auto const toscene = [ & ]( double const *Point ) { + catenary::cantilever_placement placement; + std::copy( Point, Point + 3, placement.position ); + auto const part { catenary::cantilever_to_scene( placement, heading ) }; + return glm::dvec3 { origin.x + part.offset[ 0 ], origin.y + part.offset[ 1 ], origin.z + part.offset[ 2 ] }; + }; + { + auto const messenger { toscene( result.messenger_end ) }; + double contact[ 3 ] { 0.0, 0.0, 0.0 }; + for( auto const &end : result.contact_ends ) { + double point[ 3 ] { end[ 0 ], end[ 1 ], end[ 2 ] }; + auto const at { toscene( point ) }; + contact[ 0 ] += at.x; + contact[ 1 ] += at.y; + contact[ 2 ] += at.z; + } + auto const ends { std::max( 1, result.contact_ends.size() ) }; + Anchor.messenger[ 0 ] = messenger.x; + Anchor.messenger[ 1 ] = messenger.y; + Anchor.messenger[ 2 ] = messenger.z; + for( int k = 0; k < 3; ++k ) { Anchor.contact[ k ] = contact[ k ] / ends; } + } + Anchor.solved = true; + + auto const skin { data.skin.empty() ? std::string( "none" ) : data.skin }; + std::size_t count { 0 }; + auto const place = [ & ]( std::string const &Model, catenary::cantilever_placement const &Placement, double const Heading, glm::dvec3 const &Origin ) { + auto const part { catenary::cantilever_to_scene( Placement, Heading ) }; + auto entry { + "node " + text_number( data.range_max, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " + Model + ".t3d " + skin + + " angles " + text_number( part.angles[ 0 ] ) + " " + text_number( part.angles[ 1 ] ) + " " + text_number( part.angles[ 2 ] ) }; + if( part.scale[ 0 ] != 1.0 || part.scale[ 1 ] != 1.0 || part.scale[ 2 ] != 1.0 ) { + entry += " scale " + text_number( part.scale[ 0 ], 5 ) + " " + text_number( part.scale[ 1 ], 5 ) + " " + text_number( part.scale[ 2 ], 5 ); + } + entry += " endmodel"; + glm::dvec3 const position { Origin.x + part.offset[ 0 ], Origin.y + part.offset[ 1 ], Origin.z + part.offset[ 2 ] }; + if( auto *model { simulation::State.create_model( entry, "none", position ) }; model != nullptr ) { + catenary_part_detach( model ); + Anchor.parts.push_back( model ); + ++count; + } + }; + // słup: Y wysięgnika od toru do słupa (słup po tej stronie drutów, po której stoi względem punktu "toward") + { + double sidex { dz }, sidez { -dx }; + if( ( axis.x - data.toward[ 0 ] ) * sidex + ( axis.z - data.toward[ 2 ] ) * sidez < 0.0 ) { sidex = -sidex; sidez = -sidez; } + auto const poleheading { std::atan2( sidex, sidez ) }; + catenary::cantilever_placement placement; + if( pole->turn != 0.0 ) { + auto const angle { pole->turn * 3.14159265358979323846 / 180.0 }; + auto const c { std::cos( angle ) }, s { std::sin( angle ) }; + double const turned[ 9 ] { c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0 }; + std::copy( std::begin( turned ), std::end( turned ), placement.rotation ); + } + place( pole->model, placement, poleheading, axis ); + if( false == pole->foundation.empty() ) { place( pole->foundation, placement, poleheading, axis ); } + } + for( auto const &item : result.parts ) { + catenary::cantilever_placement placement; + std::copy( std::begin( item.position ), std::end( item.position ), placement.position ); + std::copy( std::begin( item.rotation ), std::end( item.rotation ), placement.rotation ); + std::copy( std::begin( item.scale ), std::end( item.scale ), placement.scale ); + auto const *part { catalog->part( item.part ) }; + auto const model { catalog->models() + ( part != nullptr && false == part->model.empty() ? part->model : item.part ) }; + place( model, placement, heading, origin ); + } + return count; +} + +// kotwienie o podanej nazwie z podanego pliku; nullptr gdy nie ma +scene_anchor * +anchor_named( std::string const &File, std::string const &Name ) { + if( Name.empty() ) { return nullptr; } + for( auto &anchor : Anchors ) { + if( false == anchor.removed && anchor.file == File && anchor.data.name == Name ) { return &anchor; } + } + return nullptr; +} + +// numer podwieszenia o podanej nazwie z podanego pliku; Suspensions.size() gdy nie ma +std::size_t +suspension_named( std::string const &File, std::string const &Name ) { + for( std::size_t index = 0; index < Suspensions.size(); ++index ) { + auto const &suspension { Suspensions[ index ] }; + if( false == suspension.removed && suspension.file == File && suspension.data.name == Name ) { return index; } + } + return Suspensions.size(); +} + +std::string +point_text( double const *Point ) { + return text_number( Point[ 0 ], 3 ) + " " + text_number( Point[ 1 ], 3 ) + " " + text_number( Point[ 2 ], 3 ); +} + +// lina nośna podwieszenia: podana wprost albo nad przewodem na wysokości konstrukcyjnej karty +void +rope_of( catenary::suspension const &Data, double *Rope ) { + if( Data.has_rope ) { for( int i = 0; i < 3; ++i ) { Rope[ i ] = Data.rope[ i ]; } return; } + auto const *catalog { cantilevers() }; + auto const *scheme { catalog != nullptr ? catalog->scheme( Data.card, Data.execution ) : nullptr }; + Rope[ 0 ] = Data.at[ 0 ]; + Rope[ 1 ] = Data.at[ 1 ] + ( scheme != nullptr ? scheme->height : 1.70 ); + Rope[ 2 ] = Data.at[ 2 ]; +} + +// punkty końca przęsła: przewód jezdny i lina nośna. Miejsce k w łańcuchu z kotwieniami: najpierw kotwienie początkowe (gdy +// jest), potem podwieszenia, na końcu kotwienie końcowe. false, gdy brakuje wpisu albo kotwienie się nie złożyło +std::size_t +chain_points( scene_chain const &Chain ) { + return Chain.data.suspensions.size() + ( Chain.data.anchor_start.empty() ? 0 : 1 ) + ( Chain.data.anchor_end.empty() ? 0 : 1 ); +} + +bool +chain_point( scene_chain const &Chain, std::size_t Point, double *Wire, double *Rope, bool &Anchored ) { + auto const &data { Chain.data }; + Anchored = false; + auto const *anchor { static_cast( nullptr ) }; + if( false == data.anchor_start.empty() ) { + if( Point == 0 ) { anchor = anchor_named( Chain.file, data.anchor_start ); Anchored = true; } + else { --Point; } + } + if( false == Anchored && Point >= data.suspensions.size() ) { + if( Point == data.suspensions.size() && false == data.anchor_end.empty() ) { anchor = anchor_named( Chain.file, data.anchor_end ); Anchored = true; } + else { return false; } + } + if( Anchored ) { + if( anchor == nullptr || false == anchor->solved ) { return false; } + std::copy( std::begin( anchor->contact ), std::end( anchor->contact ), Wire ); + std::copy( std::begin( anchor->messenger ), std::end( anchor->messenger ), Rope ); + return true; + } + auto const index { suspension_named( Chain.file, data.suspensions[ Point ] ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { return false; } + std::copy( std::begin( Suspensions[ index ].data.at ), std::end( Suspensions[ index ].data.at ), Wire ); + rope_of( Suspensions[ index ].data, Rope ); + return true; +} + +// treść wpisu node traction dla przęsła k łańcucha; pusta, gdy brakuje podwieszenia. Przęsło do kotwienia bez wieszaków +std::string +span_body( scene_chain const &Chain, std::size_t const Span ) { + auto const &data { Chain.data }; + if( Span + 1 >= chain_points( Chain ) ) { return {}; } + double wirea[ 3 ], wireb[ 3 ], ropea[ 3 ], ropeb[ 3 ]; + bool anchoreda, anchoredb; + if( false == chain_point( Chain, Span, wirea, ropea, anchoreda ) || false == chain_point( Chain, Span + 1, wireb, ropeb, anchoredb ) ) { return {}; } + std::string body { data.power + " " + text_number( data.voltage, 1 ) + " " + text_number( data.current, 1 ) + " " + text_number( data.resistivity, 4 ) + " " + + data.material + " " + text_number( data.thickness, 2 ) + " " + std::to_string( data.state ) + "\n" }; + body += point_text( wirea ) + "\n" + point_text( wireb ) + "\n" + point_text( ropea ) + "\n" + point_text( ropeb ) + "\n"; + auto const spacing { anchoreda || anchoredb ? 0.0 : data.hangers[ 1 ] }; + body += text_number( data.hangers[ 0 ], 3 ) + " " + text_number( spacing, 3 ) + " " + std::to_string( static_cast( data.hangers[ 2 ] ) ) + " " + text_number( data.hangers[ 3 ], 3 ) + " vis"; + if( false == data.type.empty() ) { body += " type " + data.type; } + if( false == data.parallel.empty() ) { body += " parallel " + data.parallel; } + return body + " endtraction"; +} + +std::string +span_name( scene_chain const &Chain, std::size_t const Span ) { + return ( Chain.data.name.empty() ? std::string( "none" ) : Chain.data.name + "_" + std::to_string( Span + 1 ) ); +} + +// robi od nowa przęsło k łańcucha (przy podwieszeniu przesuniętym w edytorze tylko dwa przęsła przy nim) +void +rebuild_span( scene_chain &Chain, std::size_t const Span ) { + if( Span >= Chain.spans.size() ) { return; } + if( Chain.spans[ Span ] != nullptr ) { + catenary_span_delete( Chain.spans[ Span ] ); + Chain.spans[ Span ] = nullptr; + } + auto const body { span_body( Chain, Span ) }; + if( body.empty() ) { return; } + Chain.spans[ Span ] = catenary_span_create( span_name( Chain, Span ), Chain.data.range_max, Chain.data.range_min, body, Initialized ); +} + +// wszystkie przęsła łańcucha od nowa (zmiana typu, materiału, listy podwieszeń) +void +rebuild_chain( scene_chain &Chain ) { + for( auto *span : Chain.spans ) { if( span != nullptr ) { catenary_span_delete( span ); } } + Chain.spans.assign( Chain.removed || Chain.data.suspensions.size() < 2 ? 0 : chain_points( Chain ) - 1, nullptr ); + for( std::size_t span = 0; span < Chain.spans.size(); ++span ) { rebuild_span( Chain, span ); } +} + +// numery podwieszeń parasola o podanej nazwie (bez usuniętych) +std::vector +umbrella_members( std::string const &Name ) { + std::vector list; + for( std::size_t index = 0; index < Suspensions.size(); ++index ) { + if( false == Suspensions[ index ].removed && Suspensions[ index ].data.has_at && Suspensions[ index ].data.umbrella == Name ) { list.push_back( index ); } + } + return list; +} + +} // namespace + +// obsługa dyrektywy "catenary" ze scenerii: wczytuje wskazany plik podwieszeń +void +catenary_scene_load( cParser &Input ) { + + // nazwa pliku z zachowaniem wielkości liter, potem reszta dyrektywy do endcatenary + auto const filename { Input.getToken( false ) }; + if( filename.empty() || filename == "endcatenary" ) { + ErrorLog( "Bad scenario: \"catenary\" without a file name in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() ) + ")" ); + return; + } + { + std::string token; + do { + token = Input.getToken(); + } while( false == token.empty() && token != "endcatenary" ); + } + if( false == Global.bLoadTraction ) { return; } + + std::vector tokens; + { + cParser parser( filename, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction ); + auto token { parser.getToken( false ) }; + while( false == token.empty() ) { + tokens.emplace_back( token ); + token = parser.getToken( false ); + } + } + if( tokens.empty() ) { + ErrorLog( "Bad scenario: catenary file \"" + filename + "\" is missing or empty" ); + return; + } + catenary::suspension_file file; + std::vector errors; + catenary::parse_suspensions( tokens, filename, file, errors ); + for( auto const &error : errors ) { + ErrorLog( error ); + } + for( auto const &entry : file.entries ) { + scene_suspension suspension; + suspension.data = entry; + suspension.skin = ( entry.skin.empty() ? file.skin : entry.skin ); + suspension.file = filename; + Suspensions.emplace_back( suspension ); + } + for( auto const &line : file.gantries ) { + scene_gantry gantry; + gantry.data = line; + gantry.file = filename; + Gantries.emplace_back( gantry ); + assemble_gantry( Gantries.back() ); + } + // kotwienia przed łańcuchami: ostatnie przęsło łańcucha kończy się na ich urządzeniu + for( auto const &line : file.anchors ) { + scene_anchor anchor; + anchor.data = line; + anchor.file = filename; + Anchors.emplace_back( anchor ); + assemble_anchor( Anchors.back() ); + } + // przęsła łańcuchów powstają od razu: przed inicjalizacją scenerii łączy je InitTraction razem z resztą sieci + for( auto const &line : file.chains ) { + scene_chain chain; + chain.data = line; + chain.file = filename; + Chains.emplace_back( chain ); + rebuild_chain( Chains.back() ); + } + if( file_named( filename ) == nullptr ) { + // komentarze z początku pliku, żeby zapis z edytora ich nie zgubił + scene_file known; + known.name = filename; + known.skin = file.skin; + std::ifstream source { Global.asCurrentSceneryPath + filename }; + std::string line; + while( std::getline( source, line ) ) { + if( false == line.empty() && line.back() == '\r' ) { line.pop_back(); } + auto const first { line.find_first_not_of( " \t" ) }; + if( first == std::string::npos ) { continue; } + if( line.compare( first, 2, "//" ) != 0 ) { break; } + known.header += line + "\n"; + } + Files.emplace_back( known ); + } + WriteLog( ( "Catenary: " + std::to_string( file.entries.size() ) + " suspension(s) loaded from file \"" + filename + "\"" ).c_str() ); + if( Initialized ) { + // dyrektywa po FirstInit: tory, przęsła i modele już są, więc nie ma na co czekać + catenary_scene_resolve(); + } +} + +// po wczytaniu scenerii: wiąże podwieszenia z konstrukcjami wsporczymi i przęsłami po nazwach, zgłasza braki +void +catenary_scene_resolve() { + + Initialized = true; + if( Resolved >= Suspensions.size() ) { return; } + + auto const first { Resolved }; + std::size_t complete { 0 }; + std::size_t assembled { 0 }; + std::size_t parts { 0 }; + std::map> umbrellas; // parasole: nazwa -> wpisy + for( ; Resolved < Suspensions.size(); ++Resolved ) { + auto &suspension { Suspensions[ Resolved ] }; + if( suspension.removed ) { continue; } + auto const &data { suspension.data }; + auto const label { "Bad scenario: suspension \"" + ( data.name.empty() ? std::string( "none" ) : data.name ) + "\" " }; + if( data.has_at ) { + // wpis z punktami podanymi wprost: nie ma czego wiązać, od razu składanie wysięgnika + ++complete; + auto const *catalog { cantilevers() }; + if( catalog == nullptr ) { continue; } + if( false == data.umbrella.empty() ) { umbrellas[ data.umbrella ].push_back( Resolved ); continue; } // parasole po pętli, parami + auto const count { assemble_alone( Resolved, *catalog ) }; + if( count > 0 ) { ++assembled; parts += count; } + continue; + } + bool good { true }; + suspension.support = simulation::Instances.find( data.support ); + if( suspension.support == nullptr ) { + ErrorLog( label + "refers to nonexistent support model \"" + data.support + "\"" ); + good = false; + } + auto const find_spans = [ & ]( std::vector const &Names, std::vector &Spans ) { + Spans.clear(); + for( auto const &name : Names ) { + auto *span { simulation::Traction.find( name ) }; + if( span == nullptr ) { + ErrorLog( label + "refers to nonexistent traction piece \"" + name + "\"" ); + good = false; + continue; + } + if( catenary_span_symbol( *span ).empty() ) { + ErrorLog( label + "refers to traction piece \"" + name + "\" which has no network type" ); + good = false; + } + Spans.emplace_back( span ); + } + }; + find_spans( data.messenger, suspension.messenger ); + find_spans( data.contact, suspension.contact ); + if( good ) { ++complete; } + } + auto const *catalog { umbrellas.empty() ? nullptr : cantilevers() }; + if( catalog != nullptr ) { + for( auto const &umbrella : umbrellas ) { assemble_umbrella( umbrella.first, umbrella.second, *catalog, assembled, parts ); } + } + WriteLog( ( "Catenary: " + std::to_string( complete ) + " of " + std::to_string( Suspensions.size() - first ) + " suspension(s) linked to supports and traction pieces" ).c_str() ); + if( assembled > 0 ) { + WriteLog( ( "Catenary: " + std::to_string( assembled ) + " cantilever(s) assembled from " + std::to_string( parts ) + " part(s)" ).c_str() ); + } +} + +// zmienia katalog, z którego czytany jest katalog wysięgników; domyślnie "data/". do testów poza symulatorem +void +catenary_scene_data( std::string const &Directory ) { + + Datadirectory = Directory; + if( false == Datadirectory.empty() && Datadirectory.back() != '/' && Datadirectory.back() != '\\' ) { Datadirectory += '/'; } +} + +// przed wczytaniem nowej scenerii: usuwa podwieszenia poprzedniej +void +catenary_scene_clear() { + + Suspensions.clear(); + Chains.clear(); + Gantries.clear(); + Anchors.clear(); + Files.clear(); + Notes.clear(); + Resolved = 0; + Initialized = false; +} + +// ---- edytor scenerii ------------------------------------------------------------------------------------------------- + +catenary::cantilever_catalog const * +catenary_scene_catalog() { + return cantilevers(); +} + +catenary::layout_tables const & +catenary_scene_layout() { + return layout(); +} + +catenary::structure_catalog const & +catenary_scene_structures() { + static catenary::structure_catalog catalog; + static bool loaded { false }; + if( false == loaded ) { + loaded = true; + std::vector errors; + auto const filename { Datadirectory + "traction_catalog.txt" }; + if( false == catalog.load( filename, errors ) ) { + WriteLog( ( "Catenary: file \"" + filename + "\" is missing, the editor picks the cards without the described catalogue" ).c_str() ); + } + for( auto const &error : errors ) { ErrorLog( error ); } + } + return catalog; +} + +std::size_t +catenary_scene_size() { + return Suspensions.size(); +} + +catenary::suspension const * +catenary_scene_get( std::size_t const Index ) { + if( Index >= Suspensions.size() || Suspensions[ Index ].removed ) { return nullptr; } + return &Suspensions[ Index ].data; +} + +std::string +catenary_scene_file_of( std::size_t const Index ) { + return ( Index < Suspensions.size() ? Suspensions[ Index ].file : std::string() ); +} + +std::size_t +catenary_scene_parts( std::size_t const Index ) { + return ( Index < Suspensions.size() ? Suspensions[ Index ].parts.size() : 0 ); +} + +std::vector +catenary_scene_files() { + std::vector names; + for( auto const &file : Files ) { names.push_back( file.name ); } + return names; +} + +namespace { + +// składa wysięgnik podwieszenia od nowa, w parasolu razem z drugim wysięgnikiem +void +reassemble( std::size_t const Index ) { + auto const *catalog { cantilevers() }; + auto &suspension { Suspensions[ Index ] }; + if( catalog == nullptr || suspension.removed || false == suspension.data.has_at ) { return; } + std::size_t assembled { 0 }, parts { 0 }; + if( false == suspension.data.umbrella.empty() ) { + auto const members { umbrella_members( suspension.data.umbrella ) }; + for( auto const index : members ) { drop_parts( Suspensions[ index ] ); } + assemble_umbrella( suspension.data.umbrella, members, *catalog, assembled, parts ); + return; + } + drop_parts( suspension ); + assemble_alone( Index, *catalog ); +} + +// przęsła łańcuchów przy podwieszeniu idą za nim +void +follow_chains( std::size_t const Index ) { + auto const &suspension { Suspensions[ Index ] }; + if( suspension.data.name.empty() ) { return; } + for( auto &chain : Chains ) { + if( chain.removed || chain.file != suspension.file ) { continue; } + auto const &names { chain.data.suspensions }; + for( std::size_t position = 0; position < names.size(); ++position ) { + if( names[ position ] != suspension.data.name ) { continue; } + auto const point { position + ( chain.data.anchor_start.empty() ? 0 : 1 ) }; + if( point > 0 ) { rebuild_span( chain, point - 1 ); } + rebuild_span( chain, point ); + } + } +} + +void +mark_changed( std::string const &File ) { + if( auto *file { file_named( File ) }; file != nullptr ) { file->changed = true; } +} + +} // namespace + +std::size_t +catenary_scene_add( std::string const &File, catenary::suspension const &Data ) { + + if( file_named( File ) == nullptr ) { + scene_file created; + created.name = File; + created.created = true; + created.header = "// Plik sieci trakcyjnej (podwieszenia), zalozony w edytorze scenerii.\n"; + Files.emplace_back( created ); + } + scene_suspension suspension; + suspension.data = Data; + suspension.file = File; + auto const *file { file_named( File ) }; + suspension.skin = ( Data.skin.empty() ? file->skin : Data.skin ); + Suspensions.emplace_back( suspension ); + auto const index { Suspensions.size() - 1 }; + // wpis dodany po wczytaniu scenerii nie przejdzie już przez catenary_scene_resolve() + if( Resolved == index ) { ++Resolved; } + mark_changed( File ); + reassemble( index ); + return index; +} + +bool +catenary_scene_update( std::size_t const Index, catenary::suspension const &Data ) { + + if( Index >= Suspensions.size() || Suspensions[ Index ].removed ) { return false; } + auto &suspension { Suspensions[ Index ] }; + // zmiana nazwy parasola: stary partner zostaje sam i też musi się złożyć od nowa + auto const before { suspension.data.umbrella }; + suspension.data = Data; + auto const *file { file_named( suspension.file ) }; + suspension.skin = ( Data.skin.empty() ? ( file != nullptr ? file->skin : std::string() ) : Data.skin ); + mark_changed( suspension.file ); + if( false == before.empty() && before != Data.umbrella ) { + drop_parts( suspension ); + for( auto const index : umbrella_members( before ) ) { reassemble( index ); break; } + } + reassemble( Index ); + follow_chains( Index ); + return true; +} + +void +catenary_scene_remove( std::size_t const Index ) { + + if( Index >= Suspensions.size() || Suspensions[ Index ].removed ) { return; } + auto &suspension { Suspensions[ Index ] }; + auto const umbrella { suspension.data.umbrella }; + drop_parts( suspension ); + suspension.removed = true; + mark_changed( suspension.file ); + if( false == umbrella.empty() ) { + for( auto const index : umbrella_members( umbrella ) ) { reassemble( index ); break; } + } + // z łańcuchów wypada; łańcuch z jednym podwieszeniem przestaje istnieć + if( false == suspension.data.name.empty() ) { + for( auto &chain : Chains ) { + if( chain.removed || chain.file != suspension.file ) { continue; } + auto &names { chain.data.suspensions }; + auto const before { names.size() }; + names.erase( std::remove( names.begin(), names.end(), suspension.data.name ), names.end() ); + if( names.size() == before ) { continue; } + if( names.size() < 2 ) { chain.removed = true; } + rebuild_chain( chain ); + } + } +} + +bool +catenary_scene_restore( std::size_t const Index, catenary::suspension const &Data ) { + + if( Index >= Suspensions.size() ) { return false; } + auto &suspension { Suspensions[ Index ] }; + suspension.removed = false; + suspension.data = Data; + mark_changed( suspension.file ); + reassemble( Index ); + follow_chains( Index ); + return true; +} + +bool +catenary_scene_probe( catenary::suspension const &Data, catenary::cantilever_result &Result ) { + + Result = catenary::cantilever_result(); + auto const *catalog { cantilevers() }; + if( catalog == nullptr ) { Result.warnings.push_back( "the cantilever catalogue isn't loaded" ); return false; } + if( false == Data.has_at || false == ( Data.has_pole || Data.has_attach ) ) { Result.warnings.push_back( "the suspension has no explicit points" ); return false; } + scene_suspension probe; + probe.data = Data; + glm::dvec3 wire, rope, attach; + entry_points( Data, wire, rope, attach ); + return assemble( probe, *catalog, wire, ( Data.has_rope ? &rope : nullptr ), attach, nullptr, &Result ) > 0; +} + +bool +catenary_scene_changed() { + return std::any_of( Files.begin(), Files.end(), []( scene_file const &File ) { return File.changed; } ); +} + +std::vector +catenary_scene_new_files() { + std::vector names; + for( auto const &file : Files ) { if( file.created ) { names.push_back( file.name ); } } + return names; +} + +void +catenary_scene_file_included( std::string const &File ) { + if( auto *file { file_named( File ) }; file != nullptr ) { file->created = false; } +} + +bool +catenary_scene_save( std::string &Message ) { + + std::vector written; + for( auto &file : Files ) { + if( false == file.changed ) { continue; } + // wpisy w kolejności listy; usunięte pomijane. CRLF jak w pozostałych plikach scenerii + std::string text; + std::istringstream header { file.header }; + for( std::string line; std::getline( header, line ); ) { text += line + "\r\n"; } + if( false == file.skin.empty() ) { text += "skin " + file.skin + "\r\n"; } + std::size_t count { 0 }; + for( auto const &suspension : Suspensions ) { + if( suspension.removed || suspension.file != file.name ) { continue; } + text += catenary::write_suspension( suspension.data ) + "\r\n"; + ++count; + } + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != file.name ) { continue; } + text += catenary::write_chain( chain.data ) + "\r\n"; + } + for( auto const &gantry : Gantries ) { + if( gantry.removed || gantry.file != file.name ) { continue; } + text += catenary::write_gantry( gantry.data ) + "\r\n"; + } + for( auto const &anchor : Anchors ) { + if( anchor.removed || anchor.file != file.name ) { continue; } + text += catenary::write_anchor( anchor.data ) + "\r\n"; + } + auto const path { Global.asCurrentSceneryPath + file.name }; + std::ofstream output { path, std::ios::binary | std::ios::trunc }; + if( false == output.is_open() ) { + Message = "catenary file \"" + file.name + "\" can't be written"; + return false; + } + output << text; + output.close(); + if( output.fail() ) { + Message = "catenary file \"" + file.name + "\" wasn't written completely"; + return false; + } + file.changed = false; + written.push_back( file.name + " (" + std::to_string( count ) + ")" ); + } + Message.clear(); + for( auto const &name : written ) { Message += ( Message.empty() ? "" : ", " ) + name; } + return true; +} + +// ---- łańcuchy ---------------------------------------------------------------------------------------------------------- + +std::string +catenary_scene_new_name( std::string const &File ) { + // s0001, s0002, ... pierwsza wolna w pliku + for( int number = 1; ; ++number ) { + auto name { std::to_string( number ) }; + name = "s" + std::string( name.size() < 4 ? 4 - name.size() : 0, '0' ) + name; + auto const taken { std::any_of( Suspensions.begin(), Suspensions.end(), [ & ]( scene_suspension const &Suspension ) { return false == Suspension.removed && Suspension.file == File && Suspension.data.name == name; } ) }; + if( false == taken ) { return name; } + } +} + +std::string +catenary_scene_new_chain_name( std::string const &File ) { + auto stem { File }; + if( auto const slash { stem.find_last_of( "/\\" ) }; slash != std::string::npos ) { stem.erase( 0, slash + 1 ); } + if( auto const dot { stem.rfind( '.' ) }; dot != std::string::npos ) { stem.erase( dot ); } + for( int number = 1; ; ++number ) { + auto const name { stem + "_l" + std::to_string( number ) }; + auto const taken { std::any_of( Chains.begin(), Chains.end(), [ & ]( scene_chain const &Chain ) { return Chain.data.name == name; } ) }; + if( false == taken ) { return name; } + } +} + +std::size_t +catenary_chain_size() { + return Chains.size(); +} + +catenary::chain const * +catenary_chain_get( std::size_t const Index ) { + if( Index >= Chains.size() || Chains[ Index ].removed ) { return nullptr; } + return &Chains[ Index ].data; +} + +std::string +catenary_chain_file( std::size_t const Index ) { + return ( Index < Chains.size() ? Chains[ Index ].file : std::string() ); +} + +std::vector +catenary_chain_members( std::size_t const Index ) { + std::vector members; + if( Index >= Chains.size() || Chains[ Index ].removed ) { return members; } + for( auto const &name : Chains[ Index ].data.suspensions ) { members.push_back( suspension_named( Chains[ Index ].file, name ) ); } + return members; +} + +std::size_t +catenary_chain_add( std::string const &File, catenary::chain const &Data ) { + scene_chain chain; + chain.data = Data; + chain.file = File; + Chains.emplace_back( chain ); + rebuild_chain( Chains.back() ); + mark_changed( File ); + return Chains.size() - 1; +} + +bool +catenary_chain_update( std::size_t const Index, catenary::chain const &Data ) { + if( Index >= Chains.size() ) { return false; } + auto &chain { Chains[ Index ] }; + chain.data = Data; + chain.removed = Data.suspensions.size() < 2; + rebuild_chain( chain ); + mark_changed( chain.file ); + return true; +} + +void +catenary_chain_remove( std::size_t const Index ) { + if( Index >= Chains.size() || Chains[ Index ].removed ) { return; } + Chains[ Index ].removed = true; + rebuild_chain( Chains[ Index ] ); + mark_changed( Chains[ Index ].file ); +} + +int +catenary_chain_of( std::size_t const Suspension, std::size_t &Position ) { + if( Suspension >= Suspensions.size() || Suspensions[ Suspension ].data.name.empty() ) { return -1; } + auto const &suspension { Suspensions[ Suspension ] }; + for( std::size_t index = 0; index < Chains.size(); ++index ) { + auto const &chain { Chains[ index ] }; + if( chain.removed || chain.file != suspension.file ) { continue; } + auto const found { std::find( chain.data.suspensions.begin(), chain.data.suspensions.end(), suspension.data.name ) }; + if( found != chain.data.suspensions.end() ) { + Position = static_cast( found - chain.data.suspensions.begin() ); + return static_cast( index ); + } + } + return -1; +} + +// ---- bramki ------------------------------------------------------------------------------------------------------------- + +std::size_t +catenary_gantry_size() { + return Gantries.size(); +} + +catenary::gantry const * +catenary_gantry_get( std::size_t const Index ) { + if( Index >= Gantries.size() || Gantries[ Index ].removed ) { return nullptr; } + return &Gantries[ Index ].data; +} + +std::string +catenary_gantry_file( std::size_t const Index ) { + return ( Index < Gantries.size() ? Gantries[ Index ].file : std::string() ); +} + +std::size_t +catenary_gantry_parts( std::size_t const Index ) { + return ( Index < Gantries.size() ? Gantries[ Index ].parts.size() : 0 ); +} + +namespace { +// przęsła bramki wieloprzęsłowej stykające się z przęsłem Index w jego starych (Before) albo obecnych słupach - składane od +// nowa, bo wspólny słup stawia przęsło o najniższym numerze +void +reassemble_gantry_neighbours( std::size_t const Index, catenary::gantry const &Before ) { + std::set touched; + for( int k = 0; k < 2; ++k ) { + for( auto const other : gantries_at( Before.points[ k ], Index ) ) { touched.insert( other ); } + if( false == Gantries[ Index ].removed ) { + for( auto const other : gantries_at( Gantries[ Index ].data.points[ k ], Index ) ) { touched.insert( other ); } + } + } + for( auto const other : touched ) { assemble_gantry( Gantries[ other ] ); } +} +} // namespace + +std::size_t +catenary_gantry_add( std::string const &File, catenary::gantry const &Data ) { + if( file_named( File ) == nullptr ) { + scene_file created; + created.name = File; + created.created = true; + created.header = "// Plik sieci trakcyjnej (podwieszenia), zalozony w edytorze scenerii.\n"; + Files.emplace_back( created ); + } + scene_gantry gantry; + gantry.data = Data; + gantry.file = File; + Gantries.emplace_back( gantry ); + assemble_gantry( Gantries.back() ); + mark_changed( File ); + return Gantries.size() - 1; +} + +bool +catenary_gantry_update( std::size_t const Index, catenary::gantry const &Data ) { + if( Index >= Gantries.size() ) { return false; } + auto &gantry { Gantries[ Index ] }; + auto const before { gantry.data }; + gantry.data = Data; + gantry.removed = false; + assemble_gantry( gantry ); + mark_changed( gantry.file ); + reassemble_gantry_neighbours( Index, before ); + // wysięgniki na słupach tej bramki: mocowania pod dźwigarem na jej (może zmienionym) poziomie + if( auto const *catalog { cantilevers() }; catalog != nullptr ) { + for( std::size_t index = 0; index < Suspensions.size(); ++index ) { + auto const &data { Suspensions[ index ].data }; + if( Suspensions[ index ].removed || false == data.has_pole ) { continue; } + auto const *pole { catalog->pole( data.pole_type ) }; + if( pole != nullptr && pole->gantry && gantry_at_mast( data.pole_type, data.pole ) == &gantry.data ) { reassemble( index ); } + } + } + return true; +} + +void +catenary_gantry_remove( std::size_t const Index ) { + if( Index >= Gantries.size() || Gantries[ Index ].removed ) { return; } + Gantries[ Index ].removed = true; + assemble_gantry( Gantries[ Index ] ); + mark_changed( Gantries[ Index ].file ); + // wspólny słup, który stawiało to przęsło, stawia teraz sąsiednie + reassemble_gantry_neighbours( Index, Gantries[ Index ].data ); +} + +std::string +catenary_scene_new_gantry_name( std::string const &File ) { + auto stem { File }; + if( auto const slash { stem.find_last_of( "/\\" ) }; slash != std::string::npos ) { stem.erase( 0, slash + 1 ); } + if( auto const dot { stem.rfind( '.' ) }; dot != std::string::npos ) { stem.erase( dot ); } + for( int number = 1; ; ++number ) { + auto const name { stem + "_b" + std::to_string( number ) }; + if( std::none_of( Gantries.begin(), Gantries.end(), [ & ]( scene_gantry const &Gantry ) { return Gantry.data.name == name; } ) ) { return name; } + } +} + +// ---- kotwienia --------------------------------------------------------------------------------------------------------- + +namespace { + +// przęsła do kotwienia (pierwsze albo ostatnie łańcucha) idą za nim +void +follow_anchor( scene_anchor const &Anchor ) { + for( auto &chain : Chains ) { + if( chain.removed || chain.file != Anchor.file || Anchor.data.name.empty() ) { continue; } + if( chain.data.anchor_start == Anchor.data.name && false == chain.spans.empty() ) { rebuild_span( chain, 0 ); } + if( chain.data.anchor_end == Anchor.data.name && false == chain.spans.empty() ) { rebuild_span( chain, chain.spans.size() - 1 ); } + } +} + +} // namespace + +std::size_t +catenary_anchor_size() { + return Anchors.size(); +} + +catenary::anchor const * +catenary_anchor_get( std::size_t const Index ) { + if( Index >= Anchors.size() || Anchors[ Index ].removed ) { return nullptr; } + return &Anchors[ Index ].data; +} + +std::string +catenary_anchor_file( std::size_t const Index ) { + return ( Index < Anchors.size() ? Anchors[ Index ].file : std::string() ); +} + +std::size_t +catenary_anchor_parts( std::size_t const Index ) { + return ( Index < Anchors.size() ? Anchors[ Index ].parts.size() : 0 ); +} + +catenary::tensioner_result const * +catenary_anchor_result( std::size_t const Index ) { + if( Index >= Anchors.size() || Anchors[ Index ].removed ) { return nullptr; } + return &Anchors[ Index ].result; +} + +std::size_t +catenary_anchor_add( std::string const &File, catenary::anchor const &Data ) { + if( file_named( File ) == nullptr ) { + scene_file created; + created.name = File; + created.created = true; + Files.emplace_back( created ); + } + scene_anchor anchor; + anchor.data = Data; + anchor.file = File; + Anchors.emplace_back( anchor ); + assemble_anchor( Anchors.back() ); + follow_anchor( Anchors.back() ); + mark_changed( File ); + return Anchors.size() - 1; +} + +bool +catenary_anchor_update( std::size_t const Index, catenary::anchor const &Data ) { + if( Index >= Anchors.size() ) { return false; } + auto &anchor { Anchors[ Index ] }; + anchor.data = Data; + anchor.removed = false; + assemble_anchor( anchor ); + follow_anchor( anchor ); + mark_changed( anchor.file ); + return true; +} + +void +catenary_anchor_remove( std::size_t const Index ) { + if( Index >= Anchors.size() || Anchors[ Index ].removed ) { return; } + auto &anchor { Anchors[ Index ] }; + anchor.removed = true; + assemble_anchor( anchor ); + anchor.solved = false; + follow_anchor( anchor ); // przęsło do usuniętego kotwienia znika (łańcuch kończy się na ostatnim podwieszeniu) + mark_changed( anchor.file ); +} + +std::string +catenary_scene_new_anchor_name( std::string const &File ) { + for( int number = 1; ; ++number ) { + auto const name { "k" + std::to_string( number ) }; + auto used { false }; + for( auto const &anchor : Anchors ) { if( false == anchor.removed && anchor.file == File && anchor.data.name == name ) { used = true; break; } } + if( false == used ) { return name; } + } +} + +void +catenary_scene_temperature() { + for( auto &anchor : Anchors ) { + if( anchor.removed || std::abs( anchor.temperature - Global.AirTemperature ) < 0.5 ) { continue; } + assemble_anchor( anchor ); + follow_anchor( anchor ); + } +} diff --git a/world/catenary_scene.h b/world/catenary_scene.h new file mode 100644 index 00000000..cde4e2e5 --- /dev/null +++ b/world/catenary_scene.h @@ -0,0 +1,121 @@ +/* +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/. +*/ + +// Podwieszenia sieci trakcyjnej w scenerii: plik dołączany dyrektywą "catenary endcatenary". +// Starsze wersje symulatora nie znają tej dyrektywy, zgłaszają trzy nieznane słowa i pliku nie otwierają + +#pragma once + +#include + +class cParser; + +// obsługa dyrektywy "catenary" ze scenerii: wczytuje wskazany plik podwieszeń +void catenary_scene_load( cParser &Input ); +// po wczytaniu scenerii: wiąże podwieszenia z konstrukcjami wsporczymi i przęsłami po nazwach, zgłasza braki +void catenary_scene_resolve(); +// przed wczytaniem nowej scenerii: usuwa podwieszenia poprzedniej +void catenary_scene_clear(); +// zmienia katalog, z którego czytany jest katalog wysięgników (traction_cantilevers.txt i traction_supports.txt); domyślnie "data/" +void catenary_scene_data( std::string const &Directory ); + +// ---- edytor scenerii: podwieszenia zmieniane na żywo ----------------------------------------------------------------- +// Każde podwieszenie ma numer (miejsce na liście, stały do końca sesji; usunięte zostaje na liście jako puste). +// Zmiany trafiają do plików sieci dopiero przy catenary_scene_save(). + +#include +#include "world/catenary_suspension.h" +#include "world/catenary_cantilever.h" +#include "world/catenary_layout.h" +#include "world/catenary_catalog.h" +#include "world/catenary_tensioner.h" + +// katalog wysięgników (części, karty, słupy, mocowania); nullptr gdy nie dało się go wczytać +catenary::cantilever_catalog const *catenary_scene_catalog(); +// zasady rozstawiania konstrukcji (data/traction_layout.txt); pusta tablica, gdy pliku nie ma +catenary::layout_tables const &catenary_scene_layout(); +// opisany katalog konstrukcji dla edytora (traction_catalog.txt): kategorie, grupy, rodziny kart, grupy słupów; pusty gdy brak pliku +catenary::structure_catalog const &catenary_scene_structures(); +// liczba miejsc na liście podwieszeń (z usuniętymi) +std::size_t catenary_scene_size(); +// podwieszenie o podanym numerze; nullptr gdy usunięte albo numer spoza listy +catenary::suspension const *catenary_scene_get( std::size_t Index ); +// plik sieci, z którego pochodzi podwieszenie (albo do którego trafi) +std::string catenary_scene_file_of( std::size_t Index ); +// liczba części wstawionych do scenerii dla podwieszenia; 0 gdy wysięgnik się nie złożył +std::size_t catenary_scene_parts( std::size_t Index ); +// pliki sieci znane scenerii: dołączone dyrektywą i założone w edytorze +std::vector catenary_scene_files(); +// dodaje podwieszenie do podanego pliku (nowy plik jest zakładany) i od razu składa wysięgnik. zwraca numer +std::size_t catenary_scene_add( std::string const &File, catenary::suspension const &Data ); +// zmienia podwieszenie i składa wysięgnik od nowa (w parasolu oba wysięgniki). false gdy numer jest zły +bool catenary_scene_update( std::size_t Index, catenary::suspension const &Data ); +// usuwa podwieszenie razem z jego częściami +void catenary_scene_remove( std::size_t Index ); +// przywraca usunięte podwieszenie (cofnięcie w edytorze) z podanymi danymi +bool catenary_scene_restore( std::size_t Index, catenary::suspension const &Data ); +// próbne złożenie bez wstawiania części: wynik solvera z uwagami; false gdy wysięgnika nie da się złożyć +// (przyczyna w Result.warnings) +bool catenary_scene_probe( catenary::suspension const &Data, catenary::cantilever_result &Result ); +// czy są zmiany do zapisania +bool catenary_scene_changed(); +// pliki założone w edytorze, których scenerii jeszcze nie dołączono dyrektywą "catenary"; po zapisie trzeba ją dopisać +std::vector catenary_scene_new_files(); +// sceneria dołącza już plik (edytor dopisał dyrektywę "catenary") +void catenary_scene_file_included( std::string const &File ); +// zapisuje zmienione pliki sieci do katalogu scenerii. Message: co zapisano albo co poszło źle +bool catenary_scene_save( std::string &Message ); + +// ---- łańcuchy sieci (wpisy "chain" w pliku sieci): przęsła od podwieszenia do podwieszenia ------------------------------ +// nowa, wolna w pliku nazwa podwieszenia (s0001, s0002, ...) i łańcucha (_l1, ...) +std::string catenary_scene_new_name( std::string const &File ); +std::string catenary_scene_new_chain_name( std::string const &File ); +std::size_t catenary_chain_size(); +// łańcuch o podanym numerze; nullptr gdy usunięty +catenary::chain const *catenary_chain_get( std::size_t Index ); +std::string catenary_chain_file( std::size_t Index ); +// numery podwieszeń łańcucha po kolei (catenary_scene_size() w miejscu brakującego) +std::vector catenary_chain_members( std::size_t Index ); +// dodaje łańcuch (podwieszenia muszą mieć nazwy i być w tym samym pliku) i od razu robi jego przęsła. zwraca numer +std::size_t catenary_chain_add( std::string const &File, catenary::chain const &Data ); +// zmienia łańcuch i robi jego przęsła od nowa (mniej niż dwa podwieszenia = łańcuch usunięty) +bool catenary_chain_update( std::size_t Index, catenary::chain const &Data ); +void catenary_chain_remove( std::size_t Index ); +// łańcuch, do którego należy podwieszenie, i jego miejsce w łańcuchu; -1 gdy żaden +int catenary_chain_of( std::size_t Suspension, std::size_t &Position ); + +// ---- kotwienia (wpisy "anchor" w pliku sieci): słup kotwowy z urządzeniem naprężającym --------------------------------- +std::size_t catenary_anchor_size(); +catenary::anchor const *catenary_anchor_get( std::size_t Index ); +std::string catenary_anchor_file( std::size_t Index ); +std::size_t catenary_anchor_parts( std::size_t Index ); +// wynik solvera ostatniego złożenia (b, s, ciężary, naciągi, uwagi); nullptr gdy usunięte +catenary::tensioner_result const *catenary_anchor_result( std::size_t Index ); +// dodaje kotwienie, składa je i przebudowuje łańcuchy, które go używają. zwraca numer +std::size_t catenary_anchor_add( std::string const &File, catenary::anchor const &Data ); +// zmienia kotwienie (także przywraca usunięte) i przebudowuje łańcuchy, które go używają +bool catenary_anchor_update( std::size_t Index, catenary::anchor const &Data ); +void catenary_anchor_remove( std::size_t Index ); +std::string catenary_scene_new_anchor_name( std::string const &File ); +// po zmianie temperatury powietrza: kotwienia złożone przy innej (różnica od 0,5 °C) składa od nowa, z przęsłami do nich +void catenary_scene_temperature(); + +// ---- bramki (wpisy "gantry" w pliku sieci): słupy i dźwigar; zwisy z wysięgnikami to zwykłe podwieszenia ----------------- +std::size_t catenary_gantry_size(); +catenary::gantry const *catenary_gantry_get( std::size_t Index ); +std::string catenary_gantry_file( std::size_t Index ); +std::size_t catenary_gantry_parts( std::size_t Index ); +std::size_t catenary_gantry_add( std::string const &File, catenary::gantry const &Data ); +// zmienia bramkę (także przywraca usuniętą) i wstawia jej modele od nowa +bool catenary_gantry_update( std::size_t Index, catenary::gantry const &Data ); +void catenary_gantry_remove( std::size_t Index ); +std::string catenary_scene_new_gantry_name( std::string const &File ); +// końce spodu dźwigara bramki (każdy na mocowaniu swojego słupa), tam gdzie wiszą zwisy +// Second == nullptr: pojedynczy element dźwigara o długości Span od ściany pierwszego słupa +void gantry_beam_ends( catenary::gantry const &Data, catenary::gantry_mast const &First, catenary::gantry_mast const *Second, double Span, double *From, double *To ); diff --git a/world/catenary_scene_objects.cpp b/world/catenary_scene_objects.cpp new file mode 100644 index 00000000..8f7be0df --- /dev/null +++ b/world/catenary_scene_objects.cpp @@ -0,0 +1,106 @@ +/* +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/. +*/ + +// Części wysięgników sieci trakcyjnej jako obiekty scenerii: odłączenie od pliku scenerii i usunięcie. +// Osobno od catenary_scene.cpp, żeby testy poza symulatorem mogły podstawić atrapę + +#include "stdafx.h" + +#include "model/AnimModel.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "world/Traction.h" +#include "world/TractionPower.h" +#include "world/catenary_span.h" + +// część należy do pliku sieci, nie do pliku scenerii, w którym akurat jest dyrektywa: bez warstwy zapis scenerii +// w edytorze jej nie przepisuje (inaczej każda część wysięgnika trafiłaby do scenerii jako zwykły model) +void +catenary_part_detach( TAnimModel *Model ) { + + if( Model == nullptr ) { return; } + scene::Layers.count( Model->layer(), scene::layer_item::model, -1 ); + Model->layer( null_handle ); +} + +void +catenary_part_delete( TAnimModel *Model ) { + + if( Model == nullptr ) { return; } + simulation::State.delete_model( Model ); +} + +// przęsło łańcucha jako zwykły TTraction, wczytany z treści wpisu node traction. Bez warstwy scenerii: zapis scenerii +// w edytorze go nie przepisuje, przęsła łańcucha zapisują się w pliku sieci jako jeden wpis +TTraction * +catenary_span_create( std::string const &Name, double const RangeMax, double const RangeMin, std::string const &Body, bool const Link ) { + + scene::node_data nodedata; + nodedata.name = Name; + nodedata.type = "traction"; + nodedata.range_max = RangeMax; + nodedata.range_min = RangeMin; + cParser parser( Body ); + auto *span { new TTraction( nodedata ) }; + span->Load( &parser, glm::dvec3() ); + if( false == simulation::Traction.insert( span ) ) { + ErrorLog( "Bad scenario: duplicate traction piece name \"" + span->name() + "\" made by a catenary chain" ); + } + simulation::Region->insert_and_register( span ); + if( false == Link ) { return span; } + // po inicjalizacji scenerii: zasilacz i sąsiednie przęsła, tak jak robi to InitTraction przy wczytaniu + if( auto *powersource { simulation::Powergrid.find( span->asPowerSupplyName ) }; powersource != nullptr ) { + span->PowerSet( powersource ); + } + std::vector neighbours; + for( int end = 0; end < 2; ++end ) { + auto const [ matching, connection ] = simulation::Region->find_traction( end == 0 ? span->pPoint1 : span->pPoint2, span ); + if( matching != nullptr && ( connection == 0 || connection == 1 ) && matching->hvNext[ connection ] == nullptr ) { + span->Connect( end, matching, connection ); + if( span->psSection == nullptr && matching->psSection != nullptr ) { span->PowerSet( matching->psSection ); } + neighbours.push_back( matching ); + } + } + auto §ion { simulation::Region->section( span->location() ) }; + if( section.m_geometrycreated ) { + span->create_geometry( section.m_geometrybank ); + } + // sąsiedzi mają teraz z tej strony przęsło (np. wspólną linkę Y): ich linie i bryły od nowa + for( auto *neighbour : neighbours ) { catenary_span_refresh( *neighbour ); } + return span; +} + +void +catenary_span_delete( TTraction *Span ) { + + if( Span == nullptr ) { return; } + // sąsiednie przęsła tracą połączenie z tym, a z nim np. wspólną linkę Y: ich linie i bryły od nowa + std::vector neighbours; + for( int end = 0; end < 2; ++end ) { + auto *neighbour { Span->hvNext[ end ] }; + if( neighbour == nullptr ) { continue; } + for( int other = 0; other < 2; ++other ) { + if( neighbour->hvNext[ other ] == Span ) { + neighbour->hvNext[ other ] = nullptr; + neighbour->iLast = 1; + } + } + neighbours.push_back( neighbour ); + } + for( auto *neighbour : neighbours ) { catenary_span_refresh( *neighbour ); } + catenary_span_release( *Span ); + simulation::Region->erase_and_unregister( Span ); + auto const name { Span->name() }; + simulation::Traction.purge( Span ); + // nazwa prowadziła do usuniętego przęsła: zwolniona, żeby nowe przęsło o tej nazwie dało się znaleźć + if( false == name.empty() && simulation::Traction.find( name ) == nullptr ) { simulation::Traction.detach( name ); } +} diff --git a/world/catenary_solver.cpp b/world/catenary_solver.cpp new file mode 100644 index 00000000..ae87a005 --- /dev/null +++ b/world/catenary_solver.cpp @@ -0,0 +1,321 @@ +#include "world/catenary_solver.h" + +#include +#include +#include +#include + +namespace catenary { + +double network_type::y_length( double const Span ) const { + for( auto const &entry : y_length_by_span ) { + if( Span >= entry.first ) { return entry.second; } + } + return 0.0; +} + +namespace { + +struct point_load { double x; double force; }; + +// cięgno o odcinkowo stałym naciągu poziomym, obciążone ciężarem ciągłym (odcinkowo stałym) i siłami skupionymi. +// wysokości obu końców są dane; reakcja na początku wynika z warunku na różnicę wysokości. +class cable { +public: + cable( double Length, double Ystart, double Yend ) : m_length( Length ), m_ystart( Ystart ), m_yend( Yend ) {} + // strefy muszą pokrywać całą długość bez przerw, w kolejności rosnącego x + void add_zone( double X0, double X1, double Tension, double Weight ) { m_zones.push_back( { X0, X1, Tension, Weight } ); } + void add_load( double X, double Force ) { m_loads.push_back( { X, Force } ); } + void solve() { + std::sort( m_loads.begin(), m_loads.end(), []( point_load const &L, point_load const &R ) { return L.x < R.x; } ); + // punkty podziału: granice stref i siły skupione + m_nodes.clear(); + m_nodes.push_back( 0.0 ); + for( auto const &zone : m_zones ) { m_nodes.push_back( zone.x1 ); } + for( auto const &load : m_loads ) { m_nodes.push_back( load.x ); } + std::sort( m_nodes.begin(), m_nodes.end() ); + m_nodes.erase( std::unique( m_nodes.begin(), m_nodes.end(), []( double L, double R ) { return std::abs( L - R ) < 1e-9; } ), m_nodes.end() ); + // y(L) - y(0) = suma po odcinkach całki (R - W(x)) / H ; W(x) = obciążenie na lewo od x + // na odcinku bez sił skupionych: W(x) = W0 + w (x - x0) -> całka = [ (R - W0) d - w d^2 / 2 ] / H + double sumcoefficient { 0.0 }, sumconstant { 0.0 }, loadleft { 0.0 }; + m_segments.clear(); + for( std::size_t index = 0; index + 1 < m_nodes.size(); ++index ) { + auto const x0 { m_nodes[ index ] }, x1 { m_nodes[ index + 1 ] }; + for( auto const &load : m_loads ) { + if( std::abs( load.x - x0 ) < 1e-9 ) { loadleft += load.force; } + } + auto const &zone { zone_at( 0.5 * ( x0 + x1 ) ) }; + auto const d { x1 - x0 }; + sumcoefficient += d / zone.tension; + sumconstant += ( -loadleft * d - 0.5 * zone.weight * d * d ) / zone.tension; + m_segments.push_back( { x0, x1, zone.tension, zone.weight, loadleft } ); + loadleft += zone.weight * d; + } + // dodatnie y w górę, lina opada gdy R - W > 0, stąd znak minus + m_reaction = ( ( m_ystart - m_yend ) - sumconstant ) / sumcoefficient; + double y { m_ystart }; + for( auto &segment : m_segments ) { + segment.y0 = y; + auto const d { segment.x1 - segment.x0 }; + y -= ( ( m_reaction - segment.loadleft ) * d - 0.5 * segment.weight * d * d ) / segment.tension; + } + } + double height( double X ) const { + X = std::clamp( X, 0.0, m_length ); + for( auto const &segment : m_segments ) { + if( X <= segment.x1 + 1e-9 ) { + auto const d { X - segment.x0 }; + return segment.y0 - ( ( m_reaction - segment.loadleft ) * d - 0.5 * segment.weight * d * d ) / segment.tension; + } + } + return m_yend; + } + std::vector const &nodes() const { return m_nodes; } + +private: + struct zone { double x0, x1, tension, weight; }; + struct segment { double x0, x1, tension, weight, loadleft; double y0 { 0.0 }; }; + zone const &zone_at( double X ) const { + for( auto const &zone : m_zones ) { + if( X <= zone.x1 + 1e-9 ) { return zone; } + } + return m_zones.back(); + } + double m_length, m_ystart, m_yend; + std::vector m_zones; + std::vector m_loads; + std::vector m_nodes; + std::vector m_segments; + double m_reaction { 0.0 }; +}; + +} // namespace + +span_result solve_span( network_type const &Type, span_input const &Input, double const Samplestep ) { + + auto const length { Input.length }; + if( length <= 0.0 ) { throw std::invalid_argument( "span length must be positive" ); } + if( Input.hanger_gaps.size() < 2 ) { throw std::invalid_argument( "at least one hanger is required" ); } + auto const gapsum { std::accumulate( Input.hanger_gaps.begin(), Input.hanger_gaps.end(), 0.0 ) }; + if( std::abs( gapsum - length ) > 0.01 ) { throw std::invalid_argument( "hanger gaps do not add up to span length" ); } + if( Type.messenger_tension <= 0.0 ) { throw std::invalid_argument( "messenger tension must be positive" ); } + + span_result result; + + // położenia wieszaków + std::vector positions; + { + double x { 0.0 }; + for( std::size_t index = 0; index + 1 < Input.hanger_gaps.size(); ++index ) { + x += Input.hanger_gaps[ index ]; + positions.push_back( x ); + } + } + auto const count { positions.size() }; + + // obciążenie każdego wieszaka: przewód jezdny po połowie sąsiednich odcinków plus masa wieszaka + auto const contactweight { Type.contact_mass * Type.contact_count * gravity }; + std::vector loads( count ); + for( std::size_t index = 0; index < count; ++index ) { + auto const left { index == 0 ? 0.0 : 0.5 * ( positions[ index - 1 ] + positions[ index ] ) }; + auto const right { index + 1 == count ? length : 0.5 * ( positions[ index ] + positions[ index + 1 ] ) }; + loads[ index ] = contactweight * ( right - left ) + Type.hanger_mass * gravity; + } + + // strefa podwieszenia Y po obu stronach przęsła + auto ylength { Type.y_length( length ) }; + if( ylength >= length ) { ylength = 0.0; } + if( Type.y_tension <= 0.0 ) { ylength = 0.0; } + auto const reachstart { Input.y_at_start ? 0.5 * ylength : 0.0 }; + auto const reachend { Input.y_at_end ? 0.5 * ylength : 0.0 }; + auto const hasystart { reachstart > 0.0 }, hasyend { reachend > 0.0 }; + auto const hasy { hasystart || hasyend }; + result.y_length = hasy ? ylength : 0.0; + + auto const on_y_start = [ & ]( double X ) { return hasystart && X < reachstart - 1e-9; }; + auto const on_y_end = [ & ]( double X ) { return hasyend && X > length - reachend + 1e-9; }; + + auto const ropeweight { Type.messenger_mass * gravity }; + auto const fullweight { ropeweight * Type.messenger_count }; + auto const yweight { Type.y_mass * gravity }; + + // siła pionowa przekazywana przez koniec linki Y na linę nośną (połowa linki po jednej stronie podwieszenia) + double ystartforce { 0.0 }, yendforce { 0.0 }; + for( std::size_t index = 0; index < count; ++index ) { + if( on_y_start( positions[ index ] ) ) { ystartforce += loads[ index ]; } + if( on_y_end( positions[ index ] ) ) { yendforce += loads[ index ]; } + } + ystartforce += yweight * reachstart; + yendforce += yweight * reachend; + + auto const heightstart { Input.height_start >= 0.0 ? Input.height_start : Type.structure_height }; + auto const heightend { Input.height_end >= 0.0 ? Input.height_end : Type.structure_height }; + + // górna lina nośna + cable messenger { length, heightstart, heightend }; + if( hasy ) { + // w strefie Y górna lina niesie naciąg pomniejszony o naciąg linki Y, a przy dwóch linach tylko własny ciężar + auto const zonetension { Type.messenger_tension - Type.y_tension }; + if( zonetension <= 0.0 ) { throw std::invalid_argument( "Y tension must be lower than messenger tension" ); } + auto const zoneweight { Type.messenger_count > 1 ? ropeweight : fullweight }; + if( hasystart ) { messenger.add_zone( 0.0, reachstart, zonetension, zoneweight ); } + messenger.add_zone( reachstart, length - reachend, Type.messenger_tension, fullweight ); + if( hasyend ) { messenger.add_zone( length - reachend, length, zonetension, zoneweight ); } + if( hasystart ) { messenger.add_load( reachstart, ystartforce ); } + if( hasyend ) { messenger.add_load( length - reachend, yendforce ); } + } + else { + messenger.add_zone( 0.0, length, Type.messenger_tension, fullweight ); + } + for( std::size_t index = 0; index < count; ++index ) { + if( on_y_start( positions[ index ] ) || on_y_end( positions[ index ] ) ) { continue; } + messenger.add_load( positions[ index ], loads[ index ] ); + } + messenger.solve(); + + // linka Y: liczona od osi podwieszenia (tam styczna pozioma – założenie symetrii z sąsiednim przęsłem) do zacisku + auto const lowerdrop { Type.y_junction_drop }; + auto const solve_y = [ & ]( bool const Atend ) { + std::vector points; + auto const reach { Atend ? reachend : reachstart }; + if( reach <= 0.0 ) { return points; } + // współrzędna s: odległość od osi podwieszenia w głąb przęsła + std::vector yloads; + for( std::size_t index = 0; index < count; ++index ) { + auto const x { positions[ index ] }; + if( Atend ? on_y_end( x ) : on_y_start( x ) ) { yloads.push_back( { Atend ? length - x : x, loads[ index ] } ); } + } + std::sort( yloads.begin(), yloads.end(), []( point_load const &L, point_load const &R ) { return L.x < R.x; } ); + // zacisk może leżeć poniżej górnej liny, np. gdy dolna lina jest do niej przypięta od spodu + auto const junction { messenger.height( Atend ? length - reach : reach ) - lowerdrop }; + // nachylenie w punkcie s: ( ciężar linki na odcinku 0..s + wieszaki na 0..s ) / naciąg ; wysokość liczona wstecz od zacisku + std::vector stations { 0.0, reach }; + for( auto const &load : yloads ) { stations.push_back( load.x ); } + std::sort( stations.begin(), stations.end() ); + stations.erase( std::unique( stations.begin(), stations.end(), []( double L, double R ) { return std::abs( L - R ) < 1e-9; } ), stations.end() ); + std::vector heights( stations.size() ); + heights.back() = junction; + for( auto index = stations.size() - 1; index > 0; --index ) { + auto const s0 { stations[ index - 1 ] }, s1 { stations[ index ] }; + double carried { 0.0 }; + for( auto const &load : yloads ) { + if( load.x <= s0 + 1e-9 ) { carried += load.force; } + } + // całka od s0 do s1 z ( yweight * s + carried ) / T + auto const rise { ( 0.5 * yweight * ( s1 * s1 - s0 * s0 ) + carried * ( s1 - s0 ) ) / Type.y_tension }; + heights[ index - 1 ] = heights[ index ] - rise; + } + for( std::size_t index = 0; index < stations.size(); ++index ) { + points.push_back( { Atend ? length - stations[ index ] : stations[ index ], heights[ index ] } ); + } + if( Atend ) { std::reverse( points.begin(), points.end() ); } + return points; + }; + result.y_start = solve_y( false ); + result.y_end = solve_y( true ); + // połowy linki Y do ewentualnego przeliczenia razem z sąsiednimi przęsłami + result.y_half_start.reach = reachstart; + result.y_half_end.reach = reachend; + if( hasystart ) { result.y_half_start.junction = messenger.height( reachstart ) - lowerdrop; } + if( hasyend ) { result.y_half_end.junction = messenger.height( length - reachend ) - lowerdrop; } + for( std::size_t index = 0; index < count; ++index ) { + auto const x { positions[ index ] }; + if( on_y_start( x ) ) { result.y_half_start.loads.emplace_back( x, loads[ index ] ); } + if( on_y_end( x ) ) { result.y_half_end.loads.emplace_back( length - x, loads[ index ] ); } + } + + auto const y_height = []( std::vector const &Points, double const X ) { + for( auto const &point : Points ) { + if( std::abs( point.x - X ) < 1e-6 ) { return point.y; } + } + return 0.0; + }; + + // wieszaki + for( std::size_t index = 0; index < count; ++index ) { + auto const x { positions[ index ] }; + hanger item; + item.x = x; + if( on_y_start( x ) ) { item.on_y = true; item.length = y_height( result.y_start, x ) - Type.y_hanger_drop; } + else if( on_y_end( x ) ) { item.on_y = true; item.length = y_height( result.y_end, x ) - Type.y_hanger_drop; } + else { item.length = messenger.height( x ) - Type.messenger_stack; } + result.hangers.push_back( item ); + } + + // przebieg liny do rysowania: węzły obciążeń i punkty pośrednie co Samplestep + { + auto nodes { messenger.nodes() }; + auto const step { std::max( 0.1, Samplestep ) }; + for( double x = step; x < length; x += step ) { nodes.push_back( x ); } + std::sort( nodes.begin(), nodes.end() ); + nodes.erase( std::unique( nodes.begin(), nodes.end(), []( double L, double R ) { return std::abs( L - R ) < 1e-6; } ), nodes.end() ); + for( auto const x : nodes ) { + auto const y { messenger.height( x ) }; + result.messenger.push_back( { x, y } ); + auto const chord { heightstart + ( heightend - heightstart ) * x / length }; + result.sag = std::max( result.sag, chord - y ); + } + } + + return result; +} + +span_result solve_free_span( network_type const &Type, double const Length, double const Heightstart, double const Heightend, double const Samplestep ) { + + if( Length <= 0.0 ) { throw std::invalid_argument( "span length must be positive" ); } + if( Type.messenger_tension <= 0.0 ) { throw std::invalid_argument( "messenger tension must be positive" ); } + span_result result; + cable messenger { Length, Heightstart, Heightend }; + messenger.add_zone( 0.0, Length, Type.messenger_tension, Type.messenger_mass * gravity * Type.messenger_count ); + messenger.solve(); + auto const pieces { std::max( 1, static_cast( std::ceil( Length / std::max( 0.5, Samplestep ) ) ) ) }; + for( int piece = 0; piece <= pieces; ++piece ) { + auto const x { Length * piece / pieces }; + auto const y { messenger.height( x ) }; + result.messenger.push_back( { x, y } ); + result.sag = std::max( result.sag, Heightstart + ( Heightend - Heightstart ) * x / Length - y ); + } + // bez wieszaków przewód jezdny też wisi pod własnym ciężarem + if( Type.contact_tension > 0.0 ) { + cable contact { Length, 0.0, 0.0 }; + contact.add_zone( 0.0, Length, Type.contact_tension, Type.contact_mass * gravity * Type.contact_count ); + contact.solve(); + for( int piece = 0; piece <= pieces; ++piece ) { + auto const x { Length * piece / pieces }; + result.contact.push_back( { x, contact.height( x ) } ); + } + } + return result; +} + +void join_y( network_type const &Type, span_result &Span, double const Length, bool const Atend, y_half const &Neighbour ) { + + auto const &mine { Atend ? Span.y_half_end : Span.y_half_start }; + if( mine.reach <= 0.0 || Neighbour.reach <= 0.0 || Type.y_tension <= 0.0 ) { return; } + // współrzędna u: od zacisku w tym przęśle (0), przez oś podwieszenia (mine.reach), do zacisku w sąsiednim + auto const total { mine.reach + Neighbour.reach }; + cable rope { total, mine.junction, Neighbour.junction }; + rope.add_zone( 0.0, total, Type.y_tension, Type.y_mass * gravity ); + for( auto const &load : mine.loads ) { rope.add_load( mine.reach - load.first, load.second ); } + for( auto const &load : Neighbour.loads ) { rope.add_load( mine.reach + load.first, load.second ); } + rope.solve(); + // punkty linki w tym przęśle: oś podwieszenia, wieszaki, zacisk + std::vector stations { 0.0, mine.reach }; + for( auto const &load : mine.loads ) { stations.push_back( load.first ); } + std::sort( stations.begin(), stations.end() ); + stations.erase( std::unique( stations.begin(), stations.end(), []( double L, double R ) { return std::abs( L - R ) < 1e-9; } ), stations.end() ); + std::vector points; + for( auto const s : stations ) { + points.push_back( { Atend ? Length - s : s, rope.height( mine.reach - s ) } ); + } + if( Atend ) { std::reverse( points.begin(), points.end() ); } + for( auto &item : Span.hangers ) { + if( false == item.on_y ) { continue; } + for( auto const &point : points ) { + if( std::abs( point.x - item.x ) < 1e-6 ) { item.length = point.y - Type.y_hanger_drop; } + } + } + ( Atend ? Span.y_end : Span.y_start ) = points; +} + +} // namespace catenary diff --git a/world/catenary_solver.h b/world/catenary_solver.h new file mode 100644 index 00000000..385cf7f3 --- /dev/null +++ b/world/catenary_solver.h @@ -0,0 +1,88 @@ +// Solver geometrii przęsła sieci trakcyjnej (lina nośna, podwieszenie Y, wieszaki). +// Kod samodzielny: C++17, bez zależności od symulatora. Jednostki SI (m, kg, N). +// Model: lina jako cięgno bez sztywności, obciążone ciężarem własnym i siłami od wieszaków; +// przewód jezdny poziomy, jego ciężar rozłożony na wieszaki po połowie odcinków sąsiednich. +#pragma once +#include +#include +#include + +namespace catenary { + +constexpr double gravity = 9.80665; + +// opis typu sieci (jeden wpis katalogu typów) +struct network_type { + std::string symbol; // np. "YwsC120-2C" + int code { 0 }; // kod sieci wg karty 0002 + int contact_count { 2 }; // liczba przewodów jezdnych + double contact_mass { 0.890 }; // kg/m jednego przewodu jezdnego + double contact_tension { 0.0 }; // N, naciąg łączny przewodów jezdnych; 0 = nieznany + int messenger_count { 1 }; // liczba lin nośnych + double messenger_mass { 1.060 }; // kg/m jednej liny + double messenger_tension { 0.0 }; // N, naciąg łączny lin nośnych + double y_tension { 0.0 }; // N, naciąg linki Y (dla sieci z dwiema linami: naciąg dolnej liny) + double y_mass { 0.0 }; // kg/m linki Y + double structure_height { 1.70 }; // m, wysokość konstrukcyjna przy podwieszeniu + double hanger_mass { 0.0 }; // kg, masa wieszaka z uchwytami + double messenger_stack { 0.0 };// m, odstęp pionowy lin przy dwóch linach (wieszaki wiszą na dolnej) + double y_junction_drop { 0.0 };// m, o ile zacisk linki Y (dolnej liny) leży poniżej górnej liny nośnej + double y_hanger_drop { 0.0 }; // m, korekta długości wieszaków linki Y (dopasowana do tablic, nie z katalogu) + // długość podwieszenia Y zależna od rozpiętości: pary (minimalna rozpiętość, Ly), malejąco wg rozpiętości + std::vector> y_length_by_span; + + double y_length( double Span ) const; +}; + +struct span_input { + double length { 0.0 }; // m, rozpiętość przęsła + std::vector hanger_gaps; // m, odstępy: oś podwieszenia–1, 1–2, …, ostatni–oś podwieszenia + double height_start { -1.0 }; // m, wysokość liny nad przewodem przy pierwszym podwieszeniu (<0: z typu) + double height_end { -1.0 }; // m, to samo przy drugim podwieszeniu + bool y_at_start { true }; // czy przy pierwszym podwieszeniu jest linka Y (gdy typ ją przewiduje) + bool y_at_end { true }; // to samo przy drugim podwieszeniu +}; + +struct hanger { + double x { 0.0 }; // m od pierwszego podwieszenia + double length { 0.0 }; // m, od przewodu jezdnego do liny, na której wisi + bool on_y { false }; // wisi na lince Y +}; + +struct point2 { double x; double y; }; // x wzdłuż przęsła, y nad przewodem jezdnym + +// połowa linki Y po jednej stronie podwieszenia: dane potrzebne do policzenia całej linki razem z sąsiednim przęsłem +struct y_half { + double reach { 0.0 }; // m, odległość zacisku na linie nośnej od osi podwieszenia; 0 = brak linki Y + double junction { 0.0 }; // m, wysokość zacisku nad przewodem jezdnym + std::vector> loads; // wieszaki: odległość od osi podwieszenia [m], siła [N] +}; + +struct span_result { + std::vector hangers; + std::vector messenger; // górna lina nośna, od podwieszenia do podwieszenia + std::vector y_start; // linka Y przy pierwszym podwieszeniu (od osi podwieszenia do zacisku) + std::vector y_end; // linka Y przy drugim podwieszeniu (od zacisku do osi podwieszenia) + double y_length { 0.0 }; // użyta długość Ly + double sag { 0.0 }; // największe obniżenie liny względem cięciwy + y_half y_half_start; // połowy linki Y przy obu podwieszeniach + y_half y_half_end; + // przewód jezdny względem prostej łączącej jego końce (y <= 0), liczony tylko w przęśle bez wieszaków; pusty = prosty + std::vector contact; +}; + +// liczy geometrię przęsła. Rzuca std::invalid_argument przy sprzecznych danych +// Linka Y przy każdym podwieszeniu jest tu liczona tak, jakby sąsiednie przęsło było lustrzanym odbiciem +span_result solve_span( network_type const &Type, span_input const &Input, double Samplestep = 1.0 ); + +// przęsło bez wieszaków (odcinek do kotwienia albo urządzenia naprężającego): liny wiszą tylko pod własnym ciężarem. +// Heightstart, Heightend: wysokość liny nad przewodem jezdnym na końcach +span_result solve_free_span( network_type const &Type, double Length, double Heightstart, double Heightend, double Samplestep = 2.0 ); + +// przelicza linkę Y przy jednym podwieszeniu jako jedno cięgno od zacisku w tym przęśle do zacisku w sąsiednim +// (linka przechodzi swobodnie pod podwieszeniem). Poprawia przebieg linki i długości jej wieszaków w Span. +// Obie strony liczone z tych samych danych dają tę samą wysokość w osi podwieszenia, więc połówki się spotykają. +// Atend: podwieszenie na końcu przęsła; Neighbour: połowa linki z sąsiedniego przęsła przy tym samym podwieszeniu +void join_y( network_type const &Type, span_result &Span, double Length, bool Atend, y_half const &Neighbour ); + +} // namespace catenary diff --git a/world/catenary_span.cpp b/world/catenary_span.cpp new file mode 100644 index 00000000..01de759d --- /dev/null +++ b/world/catenary_span.cpp @@ -0,0 +1,385 @@ +/* +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 "world/catenary_span.h" + +#include "world/Traction.h" +#include "world/catenary_network.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "rendering/renderer.h" +#include "utilities/utilities.h" + +#include +#include +#include + +catenary_settings CatenarySettings; + +// stan przęsła rysowanego wg typu sieci +struct catenary_span { + std::string symbol; // typ sieci z dopisku "type" + catenary::network_definition const *definition { nullptr }; // opis typu z katalogu; nullptr gdy brak albo przęsła nie da się policzyć + std::vector hangergaps; // odstępy wieszaków dobrane do rozpiętości; puste = odcinek bez wieszaków + gfx::geometrybank_handle solidbank; // osobny bank na bryły tego przęsła + catenary_solid solid; // bryły do oglądania z bliska + bool solidloaded { false }; +}; + +namespace { + + +catenary::network_definition const * +definition( TTraction const &Traction ) { + return ( Traction.m_catenaryspan ? Traction.m_catenaryspan->definition : nullptr ); +} + +catenary::span_points +points( TTraction const &Traction ) { + return { + { Traction.pPoint1.x, Traction.pPoint1.y, Traction.pPoint1.z }, + { Traction.pPoint2.x, Traction.pPoint2.y, Traction.pPoint2.z }, + { Traction.pPoint3.x, Traction.pPoint3.y, Traction.pPoint3.z }, + { Traction.pPoint4.x, Traction.pPoint4.y, Traction.pPoint4.z } }; +} + +// długość podwieszenia Y, jaką typ sieci przewiduje dla tego przęsła; 0 gdy brak +double +y_length( TTraction const &Traction ) { + + auto const *type { definition( Traction ) }; + if( type == nullptr ) { return 0.0; } + if( Traction.m_catenaryspan->hangergaps.empty() ) { return 0.0; } // odcinek bez wieszaków + if( type->type.y_tension <= 0.0 ) { return 0.0; } + auto const length { std::hypot( Traction.pPoint2.x - Traction.pPoint1.x, Traction.pPoint2.z - Traction.pPoint1.z ) }; + auto const ylength { type->type.y_length( length ) }; + return ( ylength < length ? ylength : 0.0 ); +} + +// czy przy podwieszeniu od strony wskazanego końca (0: Point1, 1: Point2) jest linka Y wspólna z sąsiednim przęsłem. +// linka Y to jedno cięgno na dwa przęsła, więc jest tylko wtedy, gdy oba przęsła ją przewidują, w tej samej długości +// i z tych samych przewodów, a ich liny nośne schodzą się w jednym punkcie +bool +y_at( TTraction const &Traction, int const End ) { + + auto const ylength { y_length( Traction ) }; + if( ylength <= 0.0 ) { return false; } + auto const *neighbour { Traction.hvNext[ End ] }; + if( neighbour == nullptr ) { return false; } + auto const neighbourend { Traction.iNext[ End ] }; + if( neighbour->hvNext[ neighbourend ] != &Traction ) { return false; } + if( std::abs( y_length( *neighbour ) - ylength ) > 0.01 ) { return false; } + auto const &type { definition( Traction )->type }; + auto const &neighbourtype { definition( *neighbour )->type }; + if( type.y_tension != neighbourtype.y_tension + || type.y_mass != neighbourtype.y_mass + || type.messenger_count != neighbourtype.messenger_count ) { + return false; + } + auto const &rope { End == 0 ? Traction.pPoint3 : Traction.pPoint4 }; + auto const &neighbourrope { neighbourend == 0 ? neighbour->pPoint3 : neighbour->pPoint4 }; + return ( glm::length( rope - neighbourrope ) < 0.05 ); +} + +// liczy przęsło osobno, z linkami Y tam, gdzie są wspólne z sąsiadami; false gdy się nie da (zgłasza błąd raz) +bool +solve( TTraction &Traction, catenary::span_solution &Solution ) { + + auto const *type { definition( Traction ) }; + if( type == nullptr ) { return false; } + + std::string error; + if( false == catenary::solve_span_points( *type, Traction.m_catenaryspan->hangergaps, points( Traction ), y_at( Traction, 0 ), y_at( Traction, 1 ), Solution, error ) ) { + ErrorLog( + "Bad scenario: traction piece \"" + Traction.name() + "\" of type \"" + Traction.m_catenaryspan->symbol + "\" at location " + to_string( Traction.pPoint1 ) + + " can't be calculated, " + error + "; legacy geometry used instead" ); + Traction.m_catenaryspan->definition = nullptr; + return false; + } + return true; +} + +// połowa linki Y od strony wskazanego końca, do uzgodnienia z sąsiednim przęsłem; false gdy przęsła nie da się policzyć +bool +y_half( TTraction &Traction, int const End, catenary::y_half &Half ) { + + catenary::span_solution solution; + if( false == solve( Traction, solution ) ) { return false; } + Half = ( End == 0 ? solution.result.y_half_start : solution.result.y_half_end ); + return true; +} + +// liczy przęsło i uzgadnia linki Y z sąsiednimi przęsłami, żeby połówki spotkały się w osi podwieszenia +bool +solve_joined( TTraction &Traction, catenary::span_solution &Solution ) { + + if( false == solve( Traction, Solution ) ) { return false; } + for( int end = 0; end < 2; ++end ) { + if( false == y_at( Traction, end ) ) { continue; } + catenary::y_half neighbourhalf; + if( y_half( *Traction.hvNext[ end ], Traction.iNext[ end ], neighbourhalf ) ) { + catenary::join_y( *definition( Traction ), Solution, end == 1, neighbourhalf ); + } + else { + // sąsiad jednak nie da się policzyć i narysuje się po staremu, więc linki Y tu nie będzie + if( false == solve( Traction, Solution ) ) { return false; } + end = -1; // od nowa, już bez tego sąsiada + } + } + return true; +} + +// zbiera elementy przęsła: policzone, z linkami Y uzgodnionymi z sąsiadami także w planie +bool +build( TTraction &Traction, catenary::span_geometry &Geometry ) { + + catenary::span_solution solution; + if( false == solve_joined( Traction, solution ) ) { return false; } + auto const spanpoints { points( Traction ) }; + // przebieg linek Y w planie, wspólny z sąsiednim przęsłem: na łuku linka przechodzi obok załomu liny przy podwieszeniu + catenary::y_plan plans[ 2 ]; + for( int end = 0; end < 2; ++end ) { + if( false == y_at( Traction, end ) ) { continue; } + auto &neighbour { *Traction.hvNext[ end ] }; + catenary::span_solution neighboursolution; + if( false == solve_joined( neighbour, neighboursolution ) ) { continue; } + plans[ end ] = + catenary::solve_y_plan( + *definition( Traction ), + catenary::make_y_side( solution, spanpoints, end == 1 ), + catenary::make_y_side( neighboursolution, points( neighbour ), Traction.iNext[ end ] == 1 ) ); + } + catenary::build_geometry( *definition( Traction ), solution, spanpoints, Traction.WireOffset, Geometry, plans[ 0 ], plans[ 1 ] ); + return true; +} + +gfx::basic_vertex +vertex( TTraction const &Traction, catenary::vec3 const &Point ) { + gfx::basic_vertex result; + result.position = + glm::vec3( + Point.x - Traction.m_origin.x, + Point.y - Traction.m_origin.y, + Point.z - Traction.m_origin.z ); + return result; +} + +// zamienia bryły na wierzchołki do banku geometrii +gfx::vertex_array +vertices( TTraction const &Traction, std::vector const &Solid ) { + gfx::vertex_array result; + result.reserve( Solid.size() ); + for( auto const &point : Solid ) { + auto item { vertex( Traction, point.position ) }; + item.normal = glm::vec3( point.normal.x, point.normal.y, point.normal.z ); + result.emplace_back( item ); + } + return result; +} + +// opróżnia fragment geometrii, zwalniając jego pamięć +void +release( gfx::geometry_handle const &Geometry ) { + if( Geometry == null_handle ) { return; } + gfx::vertex_array empty; + gfx::userdata_array userdata; + GfxRenderer->Replace( empty, userdata, Geometry, GL_TRIANGLES ); +} + +// wygląd metalu: [ metal: miedź, aluminium ][ przewód, osprzęt ][ stan: nowa, przejściowa, zaśniedziała ] +// osprzęt sieci miedzianej jest z brązu albo mosiądzu +catenary_metal Metals[ 2 ][ 2 ][ 3 ] { + { { { { 0.80f, 0.50f, 0.27f }, 0.6f, 0.45f, 0.40f }, { { 0.50f, 0.30f, 0.16f }, 0.8f, 0.18f, 0.15f }, { { 0.30f, 0.52f, 0.46f }, 0.3f, 0.10f, 0.10f } }, + { { { 0.82f, 0.60f, 0.31f }, 0.6f, 0.40f, 0.35f }, { { 0.52f, 0.40f, 0.20f }, 0.8f, 0.15f, 0.15f }, { { 0.36f, 0.46f, 0.38f }, 0.4f, 0.10f, 0.10f } } }, + { { { { 0.80f, 0.81f, 0.83f }, 0.9f, 0.45f, 0.30f }, { { 0.55f, 0.55f, 0.56f }, 0.8f, 0.25f, 0.15f }, { { 0.30f, 0.30f, 0.31f }, 0.6f, 0.15f, 0.10f } }, + { { { 0.70f, 0.71f, 0.73f }, 0.9f, 0.35f, 0.30f }, { { 0.48f, 0.48f, 0.49f }, 0.8f, 0.20f, 0.15f }, { { 0.32f, 0.32f, 0.33f }, 0.6f, 0.15f, 0.10f } } } }; +bool MetalsLoaded { false }; + +// czyta data/traction_materials.txt, o ile jest. wiersz: r g b metaliczność połysk odbicie +void +load_metals() { + + MetalsLoaded = true; + std::ifstream file { "data/traction_materials.txt" }; + if( !file ) { return; } + std::string line; + int linenumber { 0 }; + while( std::getline( file, line ) ) { + ++linenumber; + auto const comment { line.find( "//" ) }; + if( comment != std::string::npos ) { line.erase( comment ); } + std::istringstream stream { line }; + stream.imbue( std::locale::classic() ); + std::string metal, part, state; + if( !( stream >> metal ) ) { continue; } // pusty wiersz + catenary_metal value {}; + stream >> part >> state >> value.color[ 0 ] >> value.color[ 1 ] >> value.color[ 2 ] >> value.metalness >> value.gloss >> value.reflection; + auto const metalindex { metal == "copper" ? 0 : metal == "aluminium" ? 1 : -1 }; + auto const partindex { part == "wire" ? 0 : part == "fitting" ? 1 : -1 }; + auto const stateindex { state == "new" ? 0 : state == "worn" ? 1 : state == "tarnished" ? 2 : -1 }; + if( stream.fail() || metalindex < 0 || partindex < 0 || stateindex < 0 ) { + ErrorLog( "Bad file \"data/traction_materials.txt\" (line " + std::to_string( linenumber ) + "): expected and six numbers" ); + continue; + } + for( auto *number : { &value.color[ 0 ], &value.color[ 1 ], &value.color[ 2 ], &value.metalness, &value.gloss, &value.reflection } ) { + *number = std::clamp( *number, 0.f, 1.f ); + } + Metals[ metalindex ][ partindex ][ stateindex ] = value; + } +} + +} // namespace + +// wygląd przewodów albo osprzętu przęsła o podanym materiale i stanie z wpisu scenerii +catenary_metal const & +catenary_span_metal( TTraction const &Traction, bool const Fitting ) { + + if( false == MetalsLoaded ) { load_metals(); } + // stan z wpisu przęsła: bit 0 zaśniedziała, bit 1 przejściowa; zaśniedziała ma pierwszeństwo + auto const state { ( Traction.DamageFlag & 1 ) != 0 ? 2 : ( Traction.DamageFlag & 2 ) != 0 ? 1 : 0 }; + return Metals[ Traction.Material == 2 ? 1 : 0 ][ Fitting ? 1 : 0 ][ state ]; +} + +// przypisuje przęsłu typ sieci z dopisku "type" i dobiera rozstaw wieszaków +void +catenary_span_assign( TTraction &Traction, std::string const &Symbol ) { + + Traction.m_catenaryspan.reset(); + if( Symbol.empty() ) { return; } + + auto span { std::make_shared() }; + span->symbol = Symbol; + Traction.m_catenaryspan = span; + if( false == Global.CatenaryProcedural ) { return; } + + auto ®istry { catenary::types() }; + if( !registry.on_error ) { + registry.on_error = []( std::string const &Message ) { ErrorLog( Message ); }; + } + span->definition = registry.find( Symbol ); + if( span->definition == nullptr ) { return; } // brak typu został już zgłoszony, przęsło rysuje się po staremu + // odstęp wieszaków 0 w scenerii oznacza odcinek bez wieszaków (do kotwienia albo urządzenia naprężającego): + // rysowane są same liny i przewody w liczbie wg typu, a rozstaw wieszaków zostaje pusty + if( Traction.iNumSections == 0 ) { return; } + span->hangergaps = + registry.hanger_gaps( + span->definition->hanger_table, + std::hypot( Traction.pPoint2.x - Traction.pPoint1.x, Traction.pPoint2.z - Traction.pPoint1.z ) ); +} + +// symbol typu sieci przypisany przęsłu; pusty gdy brak +std::string +catenary_span_symbol( TTraction const &Traction ) { + return ( Traction.m_catenaryspan ? Traction.m_catenaryspan->symbol : std::string() ); +} + +// odcinki przęsła policzone wg typu sieci; false gdy przęsło ma być narysowane po staremu +bool +catenary_span_lines( TTraction &Traction, gfx::vertex_array &Vertices ) { + + if( definition( Traction ) == nullptr ) { return false; } + catenary::span_geometry geometry; + if( false == build( Traction, geometry ) ) { return false; } + Vertices.reserve( Vertices.size() + 2 * geometry.elements.size() ); + for( auto const &element : geometry.elements ) { + Vertices.emplace_back( vertex( Traction, element.from ) ); + Vertices.emplace_back( vertex( Traction, element.to ) ); + } + return true; +} + +// bryły przęsła do oglądania z bliska; tworzone, gdy kamera jest blisko przęsła, i zwalniane, gdy się oddali +catenary_solid +catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camera ) { + + if( definition( Traction ) == nullptr ) { return {}; } + auto &span { *Traction.m_catenaryspan }; + // odległość kamery od odcinka łączącego końce przewodu jezdnego + auto const axis { Traction.pPoint2 - Traction.pPoint1 }; + auto const lengthsquared { glm::dot( axis, axis ) }; + auto const t { lengthsquared > 0.0 ? std::clamp( glm::dot( Camera - Traction.pPoint1, axis ) / lengthsquared, 0.0, 1.0 ) : 0.0 }; + // przybliżenie kamery i rozdzielczość skracają odległość tak samo, jak przy doborze szczegółowości reszty scenerii + auto const distance { + glm::length( Camera - ( Traction.pPoint1 + axis * t ) ) + / ( std::max( 0.01, static_cast( Global.ZoomFactor ) ) * std::sqrt( std::max( 0.01, static_cast( Global.fDistanceFactor ) ) ) ) }; + // bryły powstają w zasięgu z ustawień i są zwalniane z zapasem, żeby nie migały na granicy + auto const loaddistance { static_cast( CatenarySettings.solid_range ) }; + auto const releasedistance { loaddistance * 1.25 + 10.0 }; + + if( span.solidloaded ) { + if( distance > releasedistance || loaddistance <= 0.0 ) { + // kamera daleko: zwolnienie pamięci, linie wystarczą + release( span.solid.wires ); + release( span.solid.fittings ); + span.solid = {}; + span.solidloaded = false; + return {}; + } + return span.solid; + } + if( loaddistance <= 0.0 || distance > loaddistance ) { return {}; } + + catenary::span_geometry geometry; + if( false == build( Traction, geometry ) ) { return {}; } + std::vector wires, fittings; + catenary::build_solid( *definition( Traction ), geometry, wires, fittings ); + if( wires.empty() ) { return {}; } + if( span.solidbank == null_handle ) { + span.solidbank = GfxRenderer->Create_Bank(); + } + // każde ponowne wczytanie tworzy nowe fragmenty w banku tego przęsła; poprzednie zostały opróżnione przy zwalnianiu + gfx::userdata_array userdata; + { + auto data { vertices( Traction, wires ) }; + span.solid.wires = GfxRenderer->Insert( data, userdata, span.solidbank, GL_TRIANGLES ); + } + if( false == fittings.empty() ) { + auto data { vertices( Traction, fittings ) }; + span.solid.fittings = GfxRenderer->Insert( data, userdata, span.solidbank, GL_TRIANGLES ); + } + span.solidloaded = ( span.solid.wires != null_handle ); + return ( span.solidloaded ? span.solid : catenary_solid() ); +} + +namespace { + +// bryły przęsła zwolnione; zrobią się od nowa, gdy kamera podejdzie +void +drop_solid( TTraction &Traction ) { + if( false == static_cast( Traction.m_catenaryspan ) ) { return; } + auto &span { *Traction.m_catenaryspan }; + if( false == span.solidloaded ) { return; } + release( span.solid.wires ); + release( span.solid.fittings ); + span.solid = {}; + span.solidloaded = false; +} + +} // namespace + +// przęsło po zmianie sąsiadów (przęsło obok zrobione albo usunięte w edytorze): linie liczone od nowa, bryły do zrobienia +// od nowa. kształt przęsła zależy od sąsiadów (wspólne linki Y), a linie powstają raz, przy tworzeniu geometrii sekcji, +// i bryły też są trzymane, dopóki kamera jest blisko – bez tego zostawały policzone dla poprzednich sąsiadów +void +catenary_span_refresh( TTraction &Traction ) { + drop_solid( Traction ); + if( Traction.m_geometry == null_handle ) { return; } + gfx::vertex_array vertices; + if( false == catenary_span_lines( Traction, vertices ) ) { return; } + gfx::userdata_array userdata; + GfxRenderer->Replace( vertices, userdata, Traction.m_geometry, GL_LINES ); +} + +// zwalnia linie i bryły usuwanego przęsła +void +catenary_span_release( TTraction &Traction ) { + drop_solid( Traction ); + release( Traction.m_geometry ); +} diff --git a/world/catenary_span.h b/world/catenary_span.h new file mode 100644 index 00000000..1a18931e --- /dev/null +++ b/world/catenary_span.h @@ -0,0 +1,57 @@ +/* +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/. +*/ + +// Przęsło sieci trakcyjnej rysowane wg typu sieci: powiązanie TTraction z katalogiem typów i solverem. +// Cały stan potrzebny do tego trzymany jest w strukturze catenary_span, do której TTraction ma tylko wskaźnik, +// żeby zmiany w tej części nie wymagały ruszania nagłówka Traction.h + +#pragma once + +#include + +#include "rendering/geometrybank.h" + +class TTraction; + +// ustawienia rysowania sieci, zmieniane z eu07.ini i z panelu ustawień +struct catenary_settings { + float solid_range { 150.f }; // zasięg rysowania przewodów, lin i uchwytów jako brył [m]; 0 wyłącza bryły +}; +extern catenary_settings CatenarySettings; + +// przypisuje przęsłu typ sieci z dopisku "type" i dobiera rozstaw wieszaków; wołane po wczytaniu punktów przęsła. +// pusty symbol albo wyłączone rysowanie wg typu zostawia przęsło rysowane po staremu +void catenary_span_assign( TTraction &Traction, std::string const &Symbol ); +// symbol typu sieci przypisany przęsłu; pusty gdy brak +std::string catenary_span_symbol( TTraction const &Traction ); +// odcinki przęsła policzone wg typu sieci, dopisywane do Vertices; false gdy przęsło ma być narysowane po staremu +bool catenary_span_lines( TTraction &Traction, gfx::vertex_array &Vertices ); +// bryły przęsła do oglądania z bliska: przewody i liny w prawdziwej grubości oraz osprzęt (bryły zastępcze w miejscach połączeń). +// osobno, bo rysowane są z różnych materiałów; pusty uchwyt oznacza, że nie ma czego rysować +struct catenary_solid { + gfx::geometry_handle wires; + gfx::geometry_handle fittings; +}; +// bryły tworzone są, gdy kamera jest blisko przęsła, i zwalniane, gdy się oddali +catenary_solid catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camera ); +// przęsło po zmianie sąsiadów (np. w edytorze): linie liczone od nowa, bryły zrobią się od nowa +void catenary_span_refresh( TTraction &Traction ); +// zwalnia linie i bryły przęsła przed jego usunięciem +void catenary_span_release( TTraction &Traction ); + +// wygląd metalu brył: kolor własny (sRGB), metaliczność, połysk i siła odbicia otoczenia, każde w zakresie 0–1 +struct catenary_metal { + float color[ 3 ]; + float metalness; + float gloss; + float reflection; +}; +// wygląd przewodów (Fitting = false) albo osprzętu przęsła o podanym materiale i stanie z wpisu scenerii. +// wartości domyślne można zmienić bez kompilacji plikiem data/traction_materials.txt, czytanym przy pierwszym użyciu +catenary_metal const &catenary_span_metal( TTraction const &Traction, bool Fitting ); diff --git a/world/catenary_suspension.cpp b/world/catenary_suspension.cpp new file mode 100644 index 00000000..b778bdf8 --- /dev/null +++ b/world/catenary_suspension.cpp @@ -0,0 +1,453 @@ +#include "world/catenary_suspension.h" + +#include +#include +#include +#include +#include +#include + +namespace catenary { + +namespace { + +bool to_number( std::string const &Text, double &Value ) { + std::istringstream stream { Text }; + stream.imbue( std::locale::classic() ); + stream >> Value; + return false == stream.fail() && stream.eof(); +} + +std::string trim( std::string const &Text ) { + auto const first { Text.find_first_not_of( " \t\r\n" ) }; + if( first == std::string::npos ) { return {}; } + auto const last { Text.find_last_not_of( " \t\r\n" ) }; + return Text.substr( first, last - first + 1 ); +} + +// ścieżka modelu w postaci do porównań: małe litery, ukośniki w jedną stronę +std::string normalize_path( std::string Path ) { + std::transform( Path.begin(), Path.end(), Path.begin(), []( unsigned char const Character ) { return static_cast( Character == '\\' ? '/' : std::tolower( Character ) ); } ); + return Path; +} + +// słowa kluczowe wewnątrz wpisu podwieszenia +bool is_keyword( std::string const &Token ) { + static std::set const keywords { "support", "scheme", "messenger", "contact", "insulators", "override", "skin", "at", "attach", "rope", "pole", "umbrella", "endsuspension" }; + return keywords.find( Token ) != keywords.end(); +} + +} // namespace + +void parse_suspensions( std::vector const &Tokens, std::string const &Source, suspension_file &Output, std::vector &Errors ) { + + auto const count { Tokens.size() }; + std::size_t index { 0 }; + std::set names; + auto const report = [ & ]( std::string const &Entry, std::string const &Reason ) { + Errors.push_back( "Bad catenary file \"" + Source + "\"" + ( Entry.empty() ? "" : ", suspension \"" + Entry + "\"" ) + ": " + Reason ); + }; + // wartości słowa kluczowego: kolejne słowa do następnego słowa kluczowego + auto const values = [ & ]() { + std::vector list; + while( index < count && false == is_keyword( Tokens[ index ] ) && Tokens[ index ] != "node" ) { list.push_back( Tokens[ index++ ] ); } + return list; + }; + + while( index < count ) { + auto const &token { Tokens[ index++ ] }; + if( token == "skin" ) { + // domyślna tekstura dla całego pliku + if( index < count ) { Output.skin = Tokens[ index++ ]; } + else { report( "", "\"skin\" without a texture name" ); } + continue; + } + if( token != "node" ) { + report( "", "unexpected token \"" + token + "\"" ); + continue; + } + if( index + 4 > count ) { report( "", "incomplete \"node\" entry at the end of the file" ); break; } + suspension entry; + auto good { to_number( Tokens[ index ], entry.range_max ) && to_number( Tokens[ index + 1 ], entry.range_min ) }; + entry.name = Tokens[ index + 2 ]; + auto const type { Tokens[ index + 3 ] }; + index += 4; + if( false == good ) { report( entry.name, "visibility range isn't a number" ); } + if( type == "chain" ) { + // łańcuch: słowa kluczowe z wartościami do endchain + chain line; + line.name = ( entry.name == "none" ? std::string() : entry.name ); + line.range_max = entry.range_max; + line.range_min = entry.range_min; + auto closed { false }; + while( index < count ) { + auto const key { Tokens[ index++ ] }; + if( key == "endchain" ) { closed = true; break; } + if( key == "node" ) { --index; break; } + auto const one = [ & ]( std::string &Value ) { + if( index < count && Tokens[ index ] != "endchain" ) { Value = Tokens[ index++ ]; return true; } + report( line.name, "\"" + key + "\" needs a value" ); good = false; return false; }; + auto const number = [ & ]( double &Value ) { + std::string text; + if( one( text ) && false == to_number( text, Value ) ) { report( line.name, "\"" + key + "\" needs a number" ); good = false; } }; + if( key == "power" ) { one( line.power ); } + else if( key == "voltage" ) { number( line.voltage ); } + else if( key == "current" ) { number( line.current ); } + else if( key == "resistivity" ) { number( line.resistivity ); } + else if( key == "material" ) { one( line.material ); } + else if( key == "thickness" ) { number( line.thickness ); } + else if( key == "state" ) { double value { 0.0 }; number( value ); line.state = static_cast( value ); } + else if( key == "type" ) { one( line.type ); } + else if( key == "parallel" ) { one( line.parallel ); } + else if( key == "anchor_start" ) { one( line.anchor_start ); } + else if( key == "anchor_end" ) { one( line.anchor_end ); } + else if( key == "hangers" ) { for( auto &value : line.hangers ) { number( value ); } } + else if( key == "suspensions" ) { + while( index < count && Tokens[ index ] != "endchain" && Tokens[ index ] != "node" ) { line.suspensions.push_back( Tokens[ index++ ] ); } + } + else { report( line.name, "unexpected token \"" + key + "\" in a chain" ); good = false; } + } + if( false == closed ) { report( line.name, "missing \"endchain\"" ); good = false; } + if( line.power.empty() ) { report( line.name, "the chain has no \"power\"" ); good = false; } + if( line.suspensions.size() < 2 ) { report( line.name, "a chain needs at least two suspensions" ); good = false; } + if( good ) { Output.chains.push_back( line ); } + continue; + } + if( type == "gantry" ) { + gantry line; + line.name = ( entry.name == "none" ? std::string() : entry.name ); + line.range_max = entry.range_max; + line.range_min = entry.range_min; + auto closed { false }; + int masts { 0 }; + while( index < count ) { + auto const key { Tokens[ index++ ] }; + if( key == "endgantry" ) { closed = true; break; } + if( key == "node" ) { --index; break; } + auto const one = [ & ]() -> std::string { + if( index < count && Tokens[ index ] != "endgantry" ) { return Tokens[ index++ ]; } + report( line.name, "\"" + key + "\" needs a value" ); good = false; return {}; }; + if( key == "beam" ) { line.beam = one(); } + else if( key == "level" ) { line.high = ( one() == "high" ); } + else if( key == "side" ) { line.side = ( one() == "-1" ? -1 : 1 ); } + else if( key == "skin" ) { line.skin = one(); } + else if( key == "mast" ) { + if( masts >= 2 ) { report( line.name, "a gantry has two masts" ); good = false; break; } + line.masts[ masts ] = one(); + for( auto &coordinate : line.points[ masts ] ) { + if( false == to_number( one(), coordinate ) ) { report( line.name, "\"mast\" needs a type and three numbers" ); good = false; break; } + } + ++masts; + } + else if( key == "tip" ) { + // pojedynczy element dźwigara na jednym słupie: koniec wspornika zamiast drugiego słupa + if( masts != 1 ) { report( line.name, "\"tip\" goes after the one mast of a single beam element" ); good = false; break; } + line.masts[ 1 ].clear(); + for( auto &coordinate : line.points[ 1 ] ) { + if( false == to_number( one(), coordinate ) ) { report( line.name, "\"tip\" needs three numbers" ); good = false; break; } + } + ++masts; + } + else { report( line.name, "unexpected token \"" + key + "\" in a gantry" ); good = false; } + } + if( false == closed ) { report( line.name, "missing \"endgantry\"" ); good = false; } + if( masts != 2 || line.beam.empty() ) { report( line.name, "a gantry needs a beam and two masts (or one mast and the tip of a single beam element)" ); good = false; } + if( good ) { Output.gantries.push_back( line ); } + continue; + } + if( type == "anchor" ) { + anchor line; + line.name = ( entry.name == "none" ? std::string() : entry.name ); + line.range_max = entry.range_max; + line.range_min = entry.range_min; + auto closed { false }; + auto haspole { false }, hastoward { false }; + while( index < count ) { + auto const key { Tokens[ index++ ] }; + if( key == "endanchor" ) { closed = true; break; } + if( key == "node" ) { --index; break; } + auto const one = [ & ]() -> std::string { + if( index < count && Tokens[ index ] != "endanchor" ) { return Tokens[ index++ ]; } + report( line.name, "\"" + key + "\" needs a value" ); good = false; return {}; }; + auto const number = [ & ]( double &Value ) { + if( false == to_number( one(), Value ) ) { report( line.name, "\"" + key + "\" needs a number" ); good = false; } }; + auto const point = [ & ]( double *Point ) { + for( int k = 0; k < 3; ++k ) { number( Point[ k ] ); } }; + if( key == "device" ) { line.device = one(); } + else if( key == "network" ) { line.network = one(); } + else if( key == "pole" ) { line.pole_type = one(); point( line.pole ); haspole = true; } + else if( key == "toward" ) { point( line.toward ); hastoward = true; } + else if( key == "distance" ) { number( line.distance ); } + else if( key == "color" ) { line.color = one(); } + else if( key == "top_color" ) { line.top_color = one(); } + else if( key == "elongation" ) { number( line.elongation ); } + else if( key == "missing" ) { double value { 0.0 }; number( value ); line.missing = static_cast( value ); } + else if( key == "ground" ) { number( line.ground ); } + else if( key == "skin" ) { line.skin = one(); } + else { report( line.name, "unexpected token \"" + key + "\" in an anchor" ); good = false; } + } + if( false == closed ) { report( line.name, "missing \"endanchor\"" ); good = false; } + if( false == haspole || false == hastoward ) { report( line.name, "an anchor needs \"pole\" and \"toward\"" ); good = false; } + if( good ) { Output.anchors.push_back( line ); } + continue; + } + if( type != "suspension" ) { + // inny rodzaj wpisu: jeden komunikat i pominięcie całej jego treści, do następnego wpisu + report( entry.name, "node type \"" + type + "\" isn't supported in a catenary file" ); + while( index < count && Tokens[ index ] != "node" ) { ++index; } + continue; + } + // treść wpisu do endsuspension + bool closed { false }; + while( index < count ) { + auto const key { Tokens[ index++ ] }; + if( key == "endsuspension" ) { closed = true; break; } + if( key == "node" ) { + // brak endsuspension: ten wpis przepada, a następny czytany jest normalnie + --index; + break; + } + if( false == is_keyword( key ) ) { + report( entry.name, "unexpected token \"" + key + "\"" ); + good = false; + continue; + } + auto const list { values() }; + if( key == "support" ) { + if( list.size() == 1 ) { entry.support = list.front(); } + else { report( entry.name, "\"support\" needs exactly one node name" ); good = false; } + } + else if( key == "scheme" ) { + double card { 0.0 }, execution { 0.0 }; + if( list.size() == 2 && to_number( list[ 0 ], card ) && to_number( list[ 1 ], execution ) ) { + entry.card = static_cast( card ); + entry.execution = static_cast( execution ); + } + else { report( entry.name, "\"scheme\" needs two numbers: catalogue card and variant" ); good = false; } + } + else if( key == "messenger" ) { + entry.messenger.insert( entry.messenger.end(), list.begin(), list.end() ); + } + else if( key == "contact" ) { + entry.contact.insert( entry.contact.end(), list.begin(), list.end() ); + } + else if( key == "insulators" ) { + if( list.size() == 1 ) { entry.insulators = list.front(); } + else { report( entry.name, "\"insulators\" needs exactly one set name" ); good = false; } + } + else if( key == "override" ) { + if( list.size() == 2 ) { entry.overrides.emplace_back( list[ 0 ], list[ 1 ] ); } + else { report( entry.name, "\"override\" needs a role and a part" ); good = false; } + } + else if( key == "skin" ) { + if( list.size() == 1 ) { entry.skin = list.front(); } + else { report( entry.name, "\"skin\" needs exactly one texture name" ); good = false; } + } + else if( key == "at" || key == "attach" || key == "rope" ) { + auto *point { key == "at" ? entry.at : key == "attach" ? entry.attach : entry.rope }; + if( list.size() == 3 && to_number( list[ 0 ], point[ 0 ] ) && to_number( list[ 1 ], point[ 1 ] ) && to_number( list[ 2 ], point[ 2 ] ) ) { + ( key == "at" ? entry.has_at : key == "attach" ? entry.has_attach : entry.has_rope ) = true; + } + else { report( entry.name, "\"" + key + "\" needs three numbers: a point in scenery coordinates" ); good = false; } + } + else if( key == "umbrella" ) { + if( list.size() == 1 ) { entry.umbrella = list.front(); } + else { report( entry.name, "\"umbrella\" needs exactly one name" ); good = false; } + } + else if( key == "pole" ) { + if( list.size() == 4 && to_number( list[ 1 ], entry.pole[ 0 ] ) && to_number( list[ 2 ], entry.pole[ 1 ] ) && to_number( list[ 3 ], entry.pole[ 2 ] ) ) { + entry.pole_type = list[ 0 ]; + entry.has_pole = true; + } + else { report( entry.name, "\"pole\" needs a pole type and three numbers: the pole axis at the rail head in scenery coordinates" ); good = false; } + } + } + if( false == closed ) { report( entry.name, "missing \"endsuspension\"" ); good = false; } + if( good ) { + if( entry.has_at || entry.has_attach || entry.has_rope || entry.has_pole ) { + // wpis z punktami podanymi wprost: konstrukcja i przęsła nie są potrzebne, ale punkty muszą być w komplecie + if( false == entry.has_at || false == ( entry.has_attach || entry.has_pole ) ) { report( entry.name, "explicit points need \"at\" and either \"attach\" or \"pole\"" ); good = false; } + if( entry.has_attach && entry.has_pole ) { report( entry.name, "\"attach\" and \"pole\" can't be used together" ); good = false; } + if( entry.card == 0 ) { report( entry.name, "explicit points need a \"scheme\"" ); good = false; } + if( false == entry.umbrella.empty() && false == entry.has_pole ) { report( entry.name, "\"umbrella\" needs a \"pole\"" ); good = false; } + } + else { + if( entry.support.empty() ) { report( entry.name, "missing \"support\"" ); good = false; } + if( entry.messenger.empty() && entry.contact.empty() ) { report( entry.name, "no spans listed under \"messenger\" or \"contact\"" ); good = false; } + } + if( entry.messenger.size() > 2 ) { report( entry.name, "more than two spans listed under \"messenger\"" ); good = false; } + if( entry.name.empty() || entry.name == "none" ) { entry.name.clear(); } + else if( false == names.insert( entry.name ).second ) { report( entry.name, "duplicate suspension name" ); good = false; } + } + if( good ) { Output.entries.push_back( entry ); } + } + // łańcuchy wskazują podwieszenia z tego samego pliku: brakujące nazwy to błąd całego łańcucha + for( auto chain { Output.chains.begin() }; chain != Output.chains.end(); ) { + std::string missing; + for( auto const &name : chain->suspensions ) { + if( std::none_of( Output.entries.begin(), Output.entries.end(), [ & ]( suspension const &Entry ) { return Entry.name == name; } ) ) { missing = name; break; } + } + if( missing.empty() ) { ++chain; continue; } + report( chain->name, "chain refers to suspension \"" + missing + "\" which isn't in the file; the chain is left out" ); + chain = Output.chains.erase( chain ); + } +} + +std::string write_suspension( suspension const &Entry ) { + + auto const number = []( double const Value, int const Precision ) { + std::ostringstream stream; + stream.imbue( std::locale::classic() ); + stream.setf( std::ios::fixed ); + stream.precision( Precision ); + stream << Value; + return stream.str(); + }; + auto const point = [ & ]( double const *Point ) { return number( Point[ 0 ], 3 ) + " " + number( Point[ 1 ], 3 ) + " " + number( Point[ 2 ], 3 ); }; + auto const join = []( std::vector const &Names ) { std::string list; for( auto const &name : Names ) { list += " " + name; } return list; }; + std::string text { "node " + number( Entry.range_max, 0 ) + " " + number( Entry.range_min, 0 ) + " " + ( Entry.name.empty() ? std::string( "none" ) : Entry.name ) + " suspension" }; + if( false == Entry.support.empty() ) { text += " support " + Entry.support; } + if( Entry.card != 0 ) { text += " scheme " + std::to_string( Entry.card ) + " " + std::to_string( Entry.execution ); } + if( false == Entry.messenger.empty() ) { text += " messenger" + join( Entry.messenger ); } + if( false == Entry.contact.empty() ) { text += " contact" + join( Entry.contact ); } + if( false == Entry.insulators.empty() ) { text += " insulators " + Entry.insulators; } + for( auto const &replacement : Entry.overrides ) { text += " override " + replacement.first + " " + replacement.second; } + if( false == Entry.skin.empty() ) { text += " skin " + Entry.skin; } + if( Entry.has_at ) { text += " at " + point( Entry.at ); } + if( Entry.has_rope ) { text += " rope " + point( Entry.rope ); } + if( Entry.has_attach ) { text += " attach " + point( Entry.attach ); } + if( Entry.has_pole ) { text += " pole " + Entry.pole_type + " " + point( Entry.pole ); } + if( false == Entry.umbrella.empty() ) { text += " umbrella " + Entry.umbrella; } + return text + " endsuspension"; +} + +std::string write_chain( chain const &Entry ) { + + auto const number = []( double const Value ) { + std::ostringstream stream; + stream.imbue( std::locale::classic() ); + stream << Value; + return stream.str(); + }; + std::string text { "node " + number( Entry.range_max ) + " " + number( Entry.range_min ) + " " + ( Entry.name.empty() ? std::string( "none" ) : Entry.name ) + " chain" }; + text += " power " + Entry.power + " voltage " + number( Entry.voltage ) + " current " + number( Entry.current ) + " resistivity " + number( Entry.resistivity ); + text += " material " + Entry.material + " thickness " + number( Entry.thickness ) + " state " + std::to_string( Entry.state ); + if( false == Entry.type.empty() ) { text += " type " + Entry.type; } + text += " hangers " + number( Entry.hangers[ 0 ] ) + " " + number( Entry.hangers[ 1 ] ) + " " + number( Entry.hangers[ 2 ] ) + " " + number( Entry.hangers[ 3 ] ); + if( false == Entry.parallel.empty() ) { text += " parallel " + Entry.parallel; } + if( false == Entry.anchor_start.empty() ) { text += " anchor_start " + Entry.anchor_start; } + if( false == Entry.anchor_end.empty() ) { text += " anchor_end " + Entry.anchor_end; } + text += " suspensions"; + for( auto const &name : Entry.suspensions ) { text += " " + name; } + return text + " endchain"; +} + +std::string write_gantry( gantry const &Entry ) { + + auto const number = []( double const Value, int const Precision ) { + std::ostringstream stream; + stream.imbue( std::locale::classic() ); + stream.setf( std::ios::fixed ); + stream.precision( Precision ); + stream << Value; + return stream.str(); + }; + std::string text { "node " + number( Entry.range_max, 0 ) + " " + number( Entry.range_min, 0 ) + " " + ( Entry.name.empty() ? std::string( "none" ) : Entry.name ) + " gantry" }; + text += " beam " + Entry.beam + " level " + ( Entry.high ? "high" : "low" ) + " side " + std::to_string( Entry.side ); + if( false == Entry.skin.empty() ) { text += " skin " + Entry.skin; } + for( int mast = 0; mast < 2; ++mast ) { + // pojedynczy element dźwigara: zamiast drugiego słupa koniec wspornika + text += ( Entry.masts[ mast ].empty() ? std::string( " tip" ) : " mast " + Entry.masts[ mast ] ) + " " + number( Entry.points[ mast ][ 0 ], 3 ) + " " + number( Entry.points[ mast ][ 1 ], 3 ) + " " + number( Entry.points[ mast ][ 2 ], 3 ); + } + return text + " endgantry"; +} + +std::string write_anchor( anchor const &Entry ) { + + auto const number = []( double const Value, int const Precision ) { + std::ostringstream stream; + stream.imbue( std::locale::classic() ); + stream.setf( std::ios::fixed ); + stream.precision( Precision ); + stream << Value; + return stream.str(); + }; + auto const point = [ & ]( double const *Point ) { + return number( Point[ 0 ], 3 ) + " " + number( Point[ 1 ], 3 ) + " " + number( Point[ 2 ], 3 ); + }; + std::string text { "node " + number( Entry.range_max, 0 ) + " " + number( Entry.range_min, 0 ) + " " + ( Entry.name.empty() ? std::string( "none" ) : Entry.name ) + " anchor" }; + text += " device " + Entry.device; + if( false == Entry.network.empty() ) { text += " network " + Entry.network; } + text += " pole " + Entry.pole_type + " " + point( Entry.pole ) + " toward " + point( Entry.toward ) + " distance " + number( Entry.distance, 1 ); + if( false == Entry.color.empty() ) { text += " color " + Entry.color; } + if( false == Entry.top_color.empty() ) { text += " top_color " + Entry.top_color; } + if( Entry.elongation != 0.0 ) { text += " elongation " + number( Entry.elongation, 3 ); } + if( Entry.missing != 0 ) { text += " missing " + std::to_string( Entry.missing ); } + if( Entry.ground != -0.50 ) { text += " ground " + number( Entry.ground, 2 ); } + if( false == Entry.skin.empty() ) { text += " skin " + Entry.skin; } + return text + " endanchor"; +} + +double support_data::half_width( double const Height ) const { + if( height <= 0.0 ) { return 0.5 * width_base; } + auto const t { std::clamp( Height / height, 0.0, 1.0 ) }; + return 0.5 * ( width_base + ( width_top - width_base ) * t ); +} + +bool support_registry::load( std::string const &Filename, std::vector &Errors ) { + + std::ifstream file { Filename }; + if( !file ) { return false; } + std::string line, model; + support_data data; + bool inside { false }; + int linenumber { 0 }; + auto const report = [ & ]( std::string const &Reason ) { + Errors.push_back( "Bad support registry \"" + Filename + "\" (line " + std::to_string( linenumber ) + "): " + Reason ); + }; + while( std::getline( file, line ) ) { + ++linenumber; + auto const comment { line.find( "//" ) }; + if( comment != std::string::npos ) { line.erase( comment ); } + line = trim( line ); + if( line.empty() ) { continue; } + if( false == inside ) { + // support <ścieżka modelu> { + std::istringstream stream { line }; + std::string keyword, brace; + stream >> keyword >> model >> brace; + if( keyword != "support" || model.empty() || brace != "{" ) { report( "expected: support {" ); continue; } + data = support_data(); + inside = true; + continue; + } + if( line == "}" ) { + if( data.type.empty() ) { report( "support \"" + model + "\" has no \"type\"" ); } + else if( data.height <= 0.0 || data.width_base <= 0.0 || data.width_top <= 0.0 ) { report( "support \"" + model + "\" needs positive height, width_base and width_top" ); } + else { m_supports[ normalize_path( model ) ] = data; } + inside = false; + continue; + } + auto const colon { line.find( ':' ) }; + if( colon == std::string::npos ) { report( "expected: : " ); continue; } + auto const key { trim( line.substr( 0, colon ) ) }, value { trim( line.substr( colon + 1 ) ) }; + if( key == "type" ) { data.type = value; } + else if( key == "height" || key == "width_base" || key == "width_top" ) { + double number { 0.0 }; + if( false == to_number( value, number ) ) { report( "\"" + key + "\" isn't a number" ); continue; } + if( key == "height" ) { data.height = number; } + else if( key == "width_base" ) { data.width_base = number; } + else { data.width_top = number; } + } + else { report( "unknown key \"" + key + "\"" ); } + } + if( inside ) { report( "support \"" + model + "\" isn't closed with \"}\"" ); } + return true; +} + +support_data const *support_registry::find( std::string const &Model ) const { + auto const lookup { m_supports.find( normalize_path( Model ) ) }; + return ( lookup != m_supports.end() ? &lookup->second : nullptr ); +} + +} // namespace catenary diff --git a/world/catenary_suspension.h b/world/catenary_suspension.h new file mode 100644 index 00000000..55686480 --- /dev/null +++ b/world/catenary_suspension.h @@ -0,0 +1,152 @@ +// Sieć trakcyjna: wpisy podwieszeń (wysięgniki, zwisy, podwieszenia na bramkach) z pliku dołączanego dyrektywą "catenary" +// oraz rejestr konstrukcji wsporczych. Kod niezależny od reszty symulatora (C++17, tylko biblioteka standardowa). +#pragma once +#include +#include +#include +#include + +namespace catenary { + +// jeden wpis "node suspension ... endsuspension" +struct suspension { + std::string name; // nazwa wpisu + double range_max { 0.0 }; // zasięg widoczności jak w innych wpisach node + double range_min { 0.0 }; + std::string support; // nazwa wpisu node model z konstrukcją wsporczą + int card { 0 }; // numer karty katalogowej schematu, np. 1110; 0 = dobór automatyczny + int execution { 0 }; // wykonanie z karty + std::vector messenger; // przęsła, których linę nośną trzyma (0–2) + std::vector contact; // przęsła, których przewód jezdny trzyma (dowolna liczba) + std::string insulators; // zestaw podmian izolatorów; pusty = domyślny + std::vector> overrides; // nadpisania pojedynczych ról: rola, część + std::string skin; // wymienna tekstura całego podwieszenia; pusta = domyślna z pliku + // punkty podane wprost, we współrzędnych scenerii: wpis bez konstrukcji wsporczej i bez przęseł. + // "at" to oś przewodów jezdnych przy podwieszeniu, "attach" to pion przez zaczepy izolatorów (wysokość bez znaczenia), + // "rope" to oś liny nośnej; bez niej lina jest nad przewodami na wysokości konstrukcyjnej schematu + bool has_at { false }; + bool has_attach { false }; + bool has_rope { false }; + double at[ 3 ] { 0.0, 0.0, 0.0 }; + double attach[ 3 ] { 0.0, 0.0, 0.0 }; + double rope[ 3 ] { 0.0, 0.0, 0.0 }; + // słup z katalogu wysięgników zamiast "attach": typ i punkt w osi słupa na wysokości główki szyny. + // zaczepy izolatorów, mocowania i sam słup liczy wtedy exe + bool has_pole { false }; + std::string pole_type; + double pole[ 3 ] { 0.0, 0.0, 0.0 }; + // parasol: dwa wpisy ze słupem i tą samą nazwą parasola to dwa wysięgniki na jednym słupie, po przeciwnych stronach. + // słup stawia pierwszy z nich; mocowania regulowane (z połówką z nakrętkami) idą na wspólną wysokość + std::string umbrella; +}; + +// łańcuch sieci: kolejne przęsła między podwieszeniami z listy (od kotwy do kotwy), wpis +// "node chain ... suspensions endchain". Exe robi z niego zwykłe przęsła (TTraction): +// przęsło k idzie od podwieszenia k do k+1, przewód jezdny przez punkty "at", lina nośna przez punkty "rope" +struct chain { + std::string name; // nazwa łańcucha; przęsła dostają nazwy _ + double range_max { -1.0 }; + double range_min { 0.0 }; + std::string power; // zasilacz (jak w node traction) + double voltage { 3000.0 }; // napięcie nominalne, V + double current { 4500.0 }; // prąd maksymalny, A + double resistivity { 0.01 }; // jak w node traction (0,01 = wartość domyślna scenerii) + std::string material { "cu" }; // cu, al + double thickness { 3.0 }; // grubość rysowanego drutu (rysowanie bez typu sieci) + int state { 0 }; // stan drutów: 0 nowe, 2 przejściowe, 1 zaśniedziałe + std::string type; // symbol typu sieci, np. YwsC120-2C + double hangers[ 4 ] { 0.4, 4.0, 3.0, 0.04 }; // jak w node traction: najniższy wieszak, odstęp, liczba drutów, odstęp drutów + std::string parallel; // przęsło bieżni wspólnej (jak "parallel" w node traction); puste = brak + std::string anchor_start, anchor_end; // kotwienia (wpisy "anchor") przed pierwszym i za ostatnim podwieszeniem; puste = brak + std::vector suspensions; // nazwy podwieszeń z tego samego pliku, po kolei +}; + +// kotwienie ciężarowe (urządzenie naprężające z pliku traction_tensioners.txt) na słupie kotwowym, wpis +// "node anchor device network pole x y z toward x y z distance +// [color ] [top_color ] [elongation ] [missing ] [ground ] [skin ] endanchor". +// pole: oś słupa na wysokości główki szyny; toward: punkt, z którego przychodzą druty (urządzenie leży w tym kierunku od +// słupa); distance: odległość od kotwienia środkowego albo stałego. Łańcuch przypina się do niego "anchor_start/anchor_end", +// a ostatnie przęsło (bez wieszaków) kończy się na uchwytach krańcowych urządzenia +struct anchor { + std::string name; + double range_max { 1000.0 }; + double range_min { 0.0 }; + std::string device { "0461" }; + std::string network; // symbol typu sieci (traction_.txt): numer typu i liczba przewodów jezdnych + std::string pole_type; + double pole[ 3 ] { 0.0, 0.0, 0.0 }; + double toward[ 3 ] { 0.0, 0.0, 0.0 }; + double distance { 700.0 }; + std::string color, top_color; + double elongation { 0.0 }; + int missing { 0 }; + double ground { -0.50 }; + std::string skin; +}; + +// bramka: dwa słupy i dźwigar między nimi, wpis +// "node gantry beam level low|high side 1|-1 mast x y z mast x y z endgantry". +// Punkty to osie słupów na wysokości główki szyny; dźwigar biegnie od pierwszego do drugiego, na prześwicie dolnego albo górnego +// mocowania pierwszego słupa, przesunięty wzdłuż toru o offset słupa na stronę "side". Zwisy z wysięgnikami to zwykłe podwieszenia +// ze "pole x y z" (punkt w spodzie dźwigara). +// Pojedynczy element dźwigara (wspornik na jednym słupie przelotowym, np. 3017-3025): "mast x y z tip x y z" - +// masts[1] puste, points[1] to punkt w kierunku końca wspornika +struct gantry { + std::string name; + double range_max { 1000.0 }; + double range_min { 0.0 }; + std::string beam; + bool high { false }; // górne mocowanie dźwigara + int side { 1 }; + std::string masts[ 2 ]; + double points[ 2 ][ 3 ] { { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 } }; + std::string skin; +}; + +// zawartość pliku podwieszeń +struct suspension_file { + std::string skin; // domyślna wymienna tekstura dla wpisów bez własnej + std::vector entries; + std::vector chains; + std::vector gantries; + std::vector anchors; +}; + +// rozbiera plik podwieszeń podany jako kolejne słowa (już bez komentarzy). Source: nazwa pliku do komunikatów. +// Błędne wpisy są pomijane, a opis każdego błędu trafia do Errors +void parse_suspensions( std::vector const &Tokens, std::string const &Source, suspension_file &Output, std::vector &Errors ); + +// wpis podwieszenia w postaci tekstu do pliku (jedna linia, bez znaku końca linii), w tej samej składni, którą czyta parse_suspensions. +// punkty z dokładnością do milimetra +std::string write_suspension( suspension const &Entry ); +// wpis łańcucha w postaci tekstu do pliku (jedna linia) +std::string write_chain( chain const &Entry ); +// wpis bramki w postaci tekstu do pliku (jedna linia) +std::string write_gantry( gantry const &Entry ); +// wpis kotwienia w postaci tekstu do pliku (jedna linia) +std::string write_anchor( anchor const &Entry ); + +// dane konstrukcji wsporczej z rejestru +struct support_data { + std::string type; // mast, gantry_mast, beam, drop + double height { 0.0 }; // m, wysokość nad punktem zerowym modelu (główka szyny) + double width_base { 0.0 }; // m, szerokość w kierunku toru u podstawy + double width_top { 0.0 }; // m, to samo na wysokości height + // połowa szerokości na podanej wysokości: odległość czoła od osi konstrukcji + double half_width( double Height ) const; +}; + +// rejestr konstrukcji wsporczych: dane czytane z pliku, szukane po ścieżce modelu +class support_registry { +public: + // wczytuje plik; błędy trafiają do Errors. zwraca false, gdy pliku nie da się otworzyć + bool load( std::string const &Filename, std::vector &Errors ); + // dane konstrukcji o podanej ścieżce modelu (wielkość liter i rodzaj ukośników bez znaczenia); nullptr gdy brak + support_data const *find( std::string const &Model ) const; + std::size_t size() const { return m_supports.size(); } + +private: + std::map m_supports; +}; + +} // namespace catenary diff --git a/world/catenary_tensioner.cpp b/world/catenary_tensioner.cpp new file mode 100644 index 00000000..a695cf0d --- /dev/null +++ b/world/catenary_tensioner.cpp @@ -0,0 +1,619 @@ +/* +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 "world/catenary_tensioner.h" + +#include +#include +#include +#include + +namespace catenary { + +namespace { + +double const kPi { 3.14159265358979323846 }; + +std::vector words_of( std::string Line ) { + if( auto const comment { Line.find( "//" ) }; comment != std::string::npos ) { Line.erase( comment ); } + std::istringstream stream { Line }; + std::vector words; + std::string word; + while( stream >> word ) { words.push_back( word ); } + return words; +} + +bool number_of( std::string const &Word, double &Value ) { + try { + std::size_t used { 0 }; + Value = std::stod( Word, &used ); + return used == Word.size(); + } + catch( ... ) { return false; } +} + +// położenie z interpolacją liniową w tablicy rosnącej; Index i Fraction między Index a Index + 1. false poza zakresem +bool bracket( std::vector const &Values, double const Value, std::size_t &Index, double &Fraction ) { + if( Values.size() < 2 ) { Index = 0; Fraction = 0.0; return Values.size() == 1 && Value == Values.front(); } + auto inside { true }; + auto value { Value }; + if( value < Values.front() ) { value = Values.front(); inside = false; } + if( value > Values.back() ) { value = Values.back(); inside = false; } + Index = 0; + while( Index + 2 < Values.size() && value > Values[ Index + 1 ] ) { ++Index; } + auto const span { Values[ Index + 1 ] - Values[ Index ] }; + Fraction = span > 0.0 ? ( value - Values[ Index ] ) / span : 0.0; + return inside; +} + +struct vec { double x { 0.0 }, y { 0.0 }, z { 0.0 }; }; +vec operator+( vec const &A, vec const &B ) { return { A.x + B.x, A.y + B.y, A.z + B.z }; } +vec operator-( vec const &A, vec const &B ) { return { A.x - B.x, A.y - B.y, A.z - B.z }; } +vec operator*( vec const &A, double const B ) { return { A.x * B, A.y * B, A.z * B }; } +double dot( vec const &A, vec const &B ) { return A.x * B.x + A.y * B.y + A.z * B.z; } +vec cross( vec const &A, vec const &B ) { return { A.y * B.z - A.z * B.y, A.z * B.x - A.x * B.z, A.x * B.y - A.y * B.x }; } +double length( vec const &A ) { return std::sqrt( dot( A, A ) ); } +vec unit( vec const &A ) { auto const l { length( A ) }; return l > 1e-12 ? A * ( 1.0 / l ) : vec { 1.0, 0.0, 0.0 }; } + +// obrót: kolumny to osie części w układzie urządzenia +void set_rotation( tensioner_placement &Placement, vec const &X, vec const &Y, vec const &Z ) { + vec const axes[ 3 ] { X, Y, Z }; + for( int row = 0; row < 3; ++row ) { + for( int column = 0; column < 3; ++column ) { + auto const &axis { axes[ column ] }; + Placement.rotation[ row * 3 + column ] = ( row == 0 ? axis.x : row == 1 ? axis.y : axis.z ); + } + } +} + +// oś Y części możliwie wzdłuż Y urządzenia, prostopadła do Direction +vec side_of( vec const &Direction ) { + vec const y { 0.0, 1.0, 0.0 }; + auto const side { y - Direction * dot( y, Direction ) }; + return length( side ) > 1e-9 ? unit( side ) : vec { 0.0, 0.0, 1.0 }; +} + +tensioner_placement placed( std::string const &Role, std::string const &Part, vec const &At ) { + tensioner_placement placement; + placement.role = Role; + placement.part = Part; + placement.position[ 0 ] = At.x; + placement.position[ 1 ] = At.y; + placement.position[ 2 ] = At.z; + return placement; +} + +// część liniowa: +X części w kierunku Direction +tensioner_placement placed_along( std::string const &Role, std::string const &Part, vec const &At, vec const &Direction ) { + auto placement { placed( Role, Part, At ) }; + auto const x { unit( Direction ) }; + auto const y { side_of( x ) }; + set_rotation( placement, x, y, cross( x, y ) ); + return placement; +} + +// łańcuch części liniowych od From w kierunku Direction; zwraca koniec +vec place_chain( std::vector const &Links, std::string const &Role, vec From, vec const &Direction, tensioner_result &Result ) { + auto const step { unit( Direction ) }; + int index { 0 }; + for( auto const &link : Links ) { + Result.parts.push_back( placed_along( Role + "." + std::to_string( ++index ), link.name, From, step ) ); + From = From + step * link.length; + } + return From; +} + +} // namespace + +void tensioner_table::formula_values( double const Temperature, double const Distance, double &B, double &S ) const { + auto const shift { Distance * alpha * ( Temperature - tn ) }; + B = bn - shift; + S = sn - 4.0 * shift; +} + +int tensioner_color( std::string const &Name ) { + if( Name == "zolty" || Name == "1" ) { return 1; } + if( Name == "szary" || Name == "2" ) { return 2; } + if( Name == "zolty_pedzel" || Name == "3" ) { return 3; } + if( Name == "szary_brzydki" || Name == "4" ) { return 4; } + return 0; +} + +bool tensioner_table::lookup( double const Temperature, double const Distance, double &B, double &S ) const { + if( formula ) { + formula_values( Temperature, Distance, B, S ); + return true; + } + B = 0.0; + S = 0.0; + if( temperatures.empty() || distances.empty() ) { return false; } + std::size_t row { 0 }, column { 0 }; + double down { 0.0 }, across { 0.0 }; + auto const inside_d { bracket( distances, Distance, row, down ) }; + auto const inside_t { bracket( temperatures, Temperature, column, across ) }; + auto const value = [ & ]( std::vector> const &Table ) { + auto const at = [ & ]( std::size_t const R, std::size_t const C ) { + return Table[ std::min( R, Table.size() - 1 ) ][ std::min( C, Table[ 0 ].size() - 1 ) ]; + }; + auto const top { at( row, column ) * ( 1.0 - across ) + at( row, column + 1 ) * across }; + auto const bottom { at( row + 1, column ) * ( 1.0 - across ) + at( row + 1, column + 1 ) * across }; + return top * ( 1.0 - down ) + bottom * down; + }; + B = value( b ); + S = value( s ); + return inside_d && inside_t; +} + +tensioner_type const *tensioner_device::type( int const Code ) const { + for( auto const &entry : types ) { if( entry.code == Code ) { return &entry; } } + return nullptr; +} + +bool tensioner_catalog::load( std::string const &Filename, std::vector &Errors ) { + std::ifstream file { Filename }; + if( false == file.is_open() ) { return false; } + std::vector lines; + std::string line; + while( std::getline( file, line ) ) { + if( false == line.empty() && line.back() == '\r' ) { line.pop_back(); } + lines.push_back( line ); + } + parse( lines, Filename, Errors ); + return true; +} + +void tensioner_catalog::parse( std::vector const &Lines, std::string const &Source, std::vector &Errors ) { + tensioner_table *table { nullptr }; + tensioner_device *device { nullptr }; + int number { 0 }; + auto const report = [ & ]( std::string const &Text ) { Errors.push_back( Source + ":" + std::to_string( number ) + ": " + Text ); }; + // pary "część długość" od pozycji From + auto const links_of = [ & ]( std::vector const &Words, std::size_t const From, std::vector &Links ) { + for( auto k { From }; k + 1 < Words.size(); k += 2 ) { + tensioner_link link; + link.name = Words[ k ]; + if( false == number_of( Words[ k + 1 ], link.length ) ) { report( "bad length \"" + Words[ k + 1 ] + "\"" ); return; } + Links.push_back( link ); + } + if( ( Words.size() - From ) % 2 != 0 ) { report( "\"" + Words[ 0 ] + "\": a part without a length" ); } + }; + for( auto const &text : Lines ) { + ++number; + auto const words { words_of( text ) }; + if( words.empty() ) { continue; } + auto const &keyword { words[ 0 ] }; + // tablica montażowa + if( table != nullptr ) { + if( keyword == "endtable" ) { + for( auto const &row : table->b ) { + if( row.size() != table->temperatures.size() ) { report( "table " + table->name + ": a row of b has a wrong number of values" ); } + } + for( auto const &row : table->s ) { + if( row.size() != table->temperatures.size() ) { report( "table " + table->name + ": a row of s has a wrong number of values" ); } + } + table = nullptr; + continue; + } + if( keyword == "temperatures" ) { + table->temperatures.clear(); + for( std::size_t k = 1; k < words.size(); ++k ) { + double value; + if( number_of( words[ k ], value ) ) { table->temperatures.push_back( value ); } + else { report( "bad temperature \"" + words[ k ] + "\"" ); } + } + continue; + } + if( keyword == "row" && words.size() > 2 ) { + double distance; + if( false == number_of( words[ 1 ], distance ) ) { report( "bad distance \"" + words[ 1 ] + "\"" ); continue; } + std::vector *target { nullptr }; + table->distances.push_back( distance ); + table->b.emplace_back(); + table->s.emplace_back(); + for( std::size_t k = 2; k < words.size(); ++k ) { + if( words[ k ] == "b" ) { target = &table->b.back(); continue; } + if( words[ k ] == "s" ) { target = &table->s.back(); continue; } + double value; + if( target == nullptr || false == number_of( words[ k ], value ) ) { report( "bad value \"" + words[ k ] + "\"" ); continue; } + target->push_back( value ); + } + continue; + } + report( "unknown keyword \"" + keyword + "\" in a table" ); + continue; + } + // urządzenie + if( device != nullptr ) { + if( keyword == "enddevice" ) { device = nullptr; continue; } + if( keyword == "mount" ) { links_of( words, 1, device->mount ); continue; } + if( keyword == "holder" ) { links_of( words, 1, device->holder ); continue; } + if( keyword == "hanger" ) { links_of( words, 1, device->hanger ); continue; } + if( keyword == "messenger" ) { links_of( words, 1, device->messenger ); continue; } + if( keyword == "contact" ) { links_of( words, 1, device->contact ); continue; } + if( ( keyword == "messenger_clamp" || keyword == "contact_clamp" || keyword == "equalizer" ) && words.size() >= 3 ) { + auto &link { keyword == "messenger_clamp" ? device->messenger_clamp : keyword == "contact_clamp" ? device->contact_clamp : device->equalizer }; + link.name = words[ 1 ]; + if( false == number_of( words[ 2 ], link.length ) ) { report( "bad length \"" + words[ 2 ] + "\"" ); } + if( keyword == "equalizer" && words.size() >= 5 && words[ 3 ] == "spread" ) { number_of( words[ 4 ], device->equalizer_spread ); } + continue; + } + if( keyword == "type" && words.size() >= 2 ) { + tensioner_type type; + double code; + if( false == number_of( words[ 1 ], code ) ) { report( "bad type \"" + words[ 1 ] + "\"" ); continue; } + type.code = static_cast( code ); + for( std::size_t k = 2; k < words.size(); ) { + auto const &key { words[ k++ ] }; + if( key == "stack" ) { + while( k + 1 < words.size() && m_weights.end() != std::find_if( m_weights.begin(), m_weights.end(), [ & ]( tensioner_weight const &W ) { return W.name == words[ k ]; } ) ) { + double count; + if( false == number_of( words[ k + 1 ], count ) ) { report( "bad count \"" + words[ k + 1 ] + "\"" ); break; } + type.stack.emplace_back( words[ k ], static_cast( count ) ); + k += 2; + } + } + else if( key == "lever" && k + 2 < words.size() ) { + type.messenger_hole = words[ k++ ]; + type.contact_hole = words[ k++ ]; + type.axis_hole = words[ k++ ]; + } + else if( key == "rod" && k < words.size() ) { type.rod = words[ k++ ]; } + else if( key == "clamp" && k < words.size() ) { type.messenger_clamp = words[ k++ ]; } + else { report( "type " + words[ 1 ] + ": unknown keyword \"" + key + "\" (a weight has to be defined before the device)" ); break; } + } + device->types.push_back( type ); + continue; + } + report( "unknown keyword \"" + keyword + "\" in a device" ); + continue; + } + if( keyword == "table" && words.size() >= 2 ) { + // "table base shift_b shift_s ..." - jednowierszowa, wartości innej tablicy przesunięte + // o stałą (tablice 0011-6/-7/-8 różnią się od 0011-1 tylko położeniem nominalnym) + tensioner_table entry; + entry.name = words[ 1 ]; + double shiftb { 0.0 }, shifts { 0.0 }; + auto derived { false }; + for( std::size_t k = 2; k + 1 < words.size(); k += 2 ) { + if( words[ k ] == "height" ) { number_of( words[ k + 1 ], entry.height ); } + else if( words[ k ] == "rod" ) { number_of( words[ k + 1 ], entry.rod ); } + else if( words[ k ] == "shift_b" ) { number_of( words[ k + 1 ], shiftb ); } + else if( words[ k ] == "shift_s" ) { number_of( words[ k + 1 ], shifts ); } + else if( words[ k ] == "formula" ) { + // "formula ": cztery liczby + if( k + 4 < words.size() && number_of( words[ k + 1 ], entry.bn ) && number_of( words[ k + 2 ], entry.sn ) + && number_of( words[ k + 3 ], entry.tn ) && number_of( words[ k + 4 ], entry.alpha ) ) { entry.formula = true; } + else { report( "table " + entry.name + ": formula needs bn sn tn alpha" ); } + k += 3; // pętla doda jeszcze 2 + } + else if( words[ k ] == "base" ) { + derived = true; + auto const *base { this->table( words[ k + 1 ] ) }; + if( base == nullptr ) { report( "table " + entry.name + ": no base table " + words[ k + 1 ] + " before it" ); continue; } + entry.temperatures = base->temperatures; + entry.distances = base->distances; + entry.b = base->b; + entry.s = base->s; + entry.formula = base->formula; + entry.bn = base->bn; + entry.sn = base->sn; + entry.tn = base->tn; + entry.alpha = base->alpha; + } + else { report( "table " + entry.name + ": unknown keyword \"" + words[ k ] + "\"" ); } + } + if( derived ) { + for( auto &row : entry.b ) { for( auto &value : row ) { value += shiftb; } } + for( auto &row : entry.s ) { for( auto &value : row ) { value += shifts; } } + entry.bn += shiftb; + entry.sn += shifts; + } + m_tables.push_back( entry ); + if( false == derived ) { table = &m_tables.back(); } + continue; + } + if( keyword == "weight" && words.size() >= 2 ) { + tensioner_weight weight; + weight.name = words[ 1 ]; + for( std::size_t k = 2; k + 1 < words.size(); k += 2 ) { + if( words[ k ] == "height" ) { number_of( words[ k + 1 ], weight.height ); } + else if( words[ k ] == "mass" ) { number_of( words[ k + 1 ], weight.mass ); } + else if( words[ k ] == "turn" ) { number_of( words[ k + 1 ], weight.turn ); } + else if( words[ k ] == "model" ) { weight.model = words[ k + 1 ]; } + else if( words[ k ] == "offset" ) { double value { 0.0 }; number_of( words[ k + 1 ], value ); weight.offset = static_cast( value ); } + else { report( "weight " + weight.name + ": unknown keyword \"" + words[ k ] + "\"" ); } + } + m_weights.push_back( weight ); + continue; + } + if( keyword == "pulley" && words.size() >= 2 ) { + tensioner_pulley pulley; + pulley.name = words[ 1 ]; + for( std::size_t k = 2; k + 1 < words.size(); k += 2 ) { + double value { 0.0 }; + number_of( words[ k + 1 ], value ); + if( words[ k ] == "diameter" ) { pulley.diameter = value; } + else if( words[ k ] == "width" ) { pulley.width = value; } + else if( words[ k ] == "grooves" ) { pulley.grooves = static_cast( value ); } + else { report( "pulley " + pulley.name + ": unknown keyword \"" + words[ k ] + "\"" ); } + } + m_pulleys.push_back( pulley ); + continue; + } + if( keyword == "lever" && words.size() >= 2 ) { + tensioner_lever lever; + lever.name = words[ 1 ]; + for( std::size_t k = 2; k < words.size(); ) { + if( words[ k ] == "hole" && k + 3 < words.size() ) { + double along, aside; + if( number_of( words[ k + 2 ], along ) && number_of( words[ k + 3 ], aside ) ) { lever.holes[ words[ k + 1 ] ] = { along, aside }; } + else { report( "lever " + lever.name + ": bad hole \"" + words[ k + 1 ] + "\"" ); } + k += 4; + } + else { report( "lever " + lever.name + ": unknown keyword \"" + words[ k ] + "\"" ); break; } + } + m_levers.push_back( lever ); + continue; + } + if( keyword == "part" && words.size() >= 4 && words[ 2 ] == "length" ) { + double value; + if( number_of( words[ 3 ], value ) ) { m_lengths[ words[ 1 ] ] = value; } + else { report( "part " + words[ 1 ] + ": bad length" ); } + continue; + } + if( keyword == "device" && words.size() >= 2 ) { + m_devices.emplace_back(); + device = &m_devices.back(); + device->name = words[ 1 ]; + for( std::size_t k = 2; k + 1 < words.size(); k += 2 ) { + auto const &key { words[ k ] }; + auto const &value { words[ k + 1 ] }; + if( key == "table" ) { device->table = value; } + else if( key == "pulley" ) { device->pulley = value; } + else if( key == "lever" ) { device->lever = value; } + else if( key == "rod" ) { device->rod = value; } + else if( key == "rope" ) { device->rope = value; } + else if( key == "rope_diameter" ) { number_of( value, device->rope_diameter ); } + else { report( "device " + device->name + ": unknown keyword \"" + key + "\"" ); } + } + continue; + } + report( "unknown keyword \"" + keyword + "\"" ); + } + if( table != nullptr ) { report( "table " + table->name + " without endtable" ); } + if( device != nullptr ) { report( "device " + device->name + " without enddevice" ); } +} + +tensioner_table const *tensioner_catalog::table( std::string const &Name ) const { + for( auto const &entry : m_tables ) { if( entry.name == Name ) { return &entry; } } + return nullptr; +} +tensioner_weight const *tensioner_catalog::weight( std::string const &Name ) const { + for( auto const &entry : m_weights ) { if( entry.name == Name ) { return &entry; } } + return nullptr; +} +tensioner_pulley const *tensioner_catalog::pulley( std::string const &Name ) const { + for( auto const &entry : m_pulleys ) { if( entry.name == Name ) { return &entry; } } + return nullptr; +} +tensioner_lever const *tensioner_catalog::lever( std::string const &Name ) const { + for( auto const &entry : m_levers ) { if( entry.name == Name ) { return &entry; } } + return nullptr; +} +tensioner_device const *tensioner_catalog::device( std::string const &Name ) const { + for( auto const &entry : m_devices ) { if( entry.name == Name ) { return &entry; } } + return nullptr; +} + +bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const &Device, tensioner_input const &Input, tensioner_result &Result ) { + Result = tensioner_result(); + auto const fail = [ & ]( std::string const &Text ) { Result.warnings.push_back( Text ); return false; }; + auto const *type { Device.type( Input.type ) }; + if( type == nullptr ) { return fail( "device " + Device.name + " has no data for network type " + std::to_string( Input.type ) ); } + auto const *table { Catalog.table( Device.table ) }; + if( table == nullptr ) { return fail( "no table " + Device.table ); } + auto const *pulley { Catalog.pulley( Device.pulley ) }; + if( pulley == nullptr ) { return fail( "no pulley " + Device.pulley ); } + auto const *lever { Catalog.lever( Device.lever ) }; + if( lever == nullptr ) { return fail( "no lever " + Device.lever ); } + auto const hole = [ & ]( std::string const &Name ) -> std::pair const * { + auto const found { lever->holes.find( Name ) }; + return found == lever->holes.end() ? nullptr : &found->second; + }; + auto const *messengerhole { hole( type->messenger_hole ) }; + auto const *contacthole { hole( type->contact_hole ) }; + auto const *axishole { hole( type->axis_hole ) }; + if( messengerhole == nullptr || contacthole == nullptr || axishole == nullptr ) { + return fail( "lever " + lever->name + " has no hole " + type->messenger_hole + "/" + type->contact_hole + "/" + type->axis_hole ); + } + auto const rodname { type->rod.empty() ? Device.rod : type->rod }; + auto const rodlength { [ & ] { auto const found { Catalog.part_lengths().find( rodname ) }; return found != Catalog.part_lengths().end() ? found->second : table->rod; }() }; + + // położenie z tablicy, trwałe wydłużenie sieci przesuwa rolkę do konstrukcji (ciężary 4 razy więcej) + double b, s; + if( false == table->lookup( Input.temperature, Input.distance, b, s ) ) { + Result.warnings.push_back( "temperature or distance outside table " + table->name + ", taken from its edge" ); + } + b -= Input.elongation; + s -= 4.0 * Input.elongation; + // pręt innej długości niż w tablicy: spód ciężarów niżej o różnicę (uwaga 3 na karcie 0011-1), góra pręta na tej samej linie + s -= rodlength - table->rod; + + // stos ciężarów od dołu, brakujące z góry + std::vector stack; + for( auto const &[ name, count ] : type->stack ) { + auto const *weight { Catalog.weight( name ) }; + if( weight == nullptr ) { return fail( "no weight " + name ); } + for( int k = 0; k < count; ++k ) { stack.push_back( weight ); } + } + for( int k = 0; k < Input.missing && false == stack.empty(); ++k ) { stack.pop_back(); } + double mass { 0.0 }, stackheight { 0.0 }; + for( auto const *weight : stack ) { mass += weight->mass; stackheight += weight->height; } + Result.weights = static_cast( stack.size() ); + Result.stack_height = stackheight; + Result.tension = 4.0 * mass * Input.gravity; + + // ciężary nie zejdą niżej niż grunt: lina nie puści rolki bliżej konstrukcji, sieć traci naciąg + if( s < Input.ground ) { + b += ( Input.ground - s ) / 4.0; + s = Input.ground; + Result.grounded = true; + Result.tension = 0.0; + Result.warnings.push_back( "the weights rest on the ground: the network has no tension from the device" ); + } + Result.b = b; + Result.s = s; + + auto const height { table->height }; + vec const out { 1.0, 0.0, 0.0 }, down { 0.0, 0.0, -1.0 }; + + // rolka stała na uchwytach od czoła konstrukcji, rolka ruchoma w b + auto const fixedaxle { place_chain( Device.mount, "mount", vec { 0.0, 0.0, height }, out, Result ) }; + vec const movingaxle { b, 0.0, height }; + auto const radius { 0.5 * pulley->diameter + 0.5 * Device.rope_diameter }; + if( movingaxle.x - fixedaxle.x < 2.0 * radius ) { Result.warnings.push_back( "the pulleys overlap: the network pulled the moving one to the structure" ); } + { + auto fixed { placed( "pulley_fixed", pulley->name, fixedaxle ) }; + Result.parts.push_back( fixed ); + Result.parts.push_back( placed( "pulley_moving", pulley->name, movingaxle ) ); + } + auto const axispoint { place_chain( Device.holder, "holder", movingaxle, out, Result ) }; + + // dźwignia: otwór osi na axispoint; przy otworze liny niżej niż otwór przewodów odwrócona + auto const flipped { messengerhole->first < contacthole->first }; + auto const sign { flipped ? -1.0 : 1.0 }; + auto const holepoint = [ & ]( std::pair const &Hole ) { + return axispoint + vec { sign * ( Hole.second - axishole->second ), 0.0, sign * ( Hole.first - axishole->first ) }; + }; + { + auto placement { placed( "lever", lever->name, holepoint( { 0.0, 0.0 } ) ) }; + set_rotation( placement, vec { sign, 0.0, 0.0 }, vec { 0.0, 1.0, 0.0 }, vec { 0.0, 0.0, sign } ); + Result.parts.push_back( placement ); + } + // podział naciągu dźwignią: momenty względem osi + auto const armmessenger { std::abs( messengerhole->first - axishole->first ) }; + auto const armcontact { std::abs( contacthole->first - axishole->first ) }; + if( armmessenger + armcontact > 1e-9 ) { + Result.messenger_tension = Result.tension * armcontact / ( armmessenger + armcontact ); + Result.contact_tension = Result.tension - Result.messenger_tension; + } + + // łańcuchy do drutów + { + auto end { place_chain( Device.messenger, "messenger", holepoint( *messengerhole ), out, Result ) }; + auto const clamp { type->messenger_clamp.empty() ? Device.messenger_clamp.name : type->messenger_clamp }; + if( false == clamp.empty() ) { + Result.parts.push_back( placed_along( "messenger_clamp", clamp, end, out ) ); + end = end + out * Device.messenger_clamp.length; + } + Result.messenger_end[ 0 ] = end.x; + Result.messenger_end[ 1 ] = end.y; + Result.messenger_end[ 2 ] = end.z; + } + { + auto end { place_chain( Device.contact, "contact", holepoint( *contacthole ), out, Result ) }; + std::vector ends; + if( Input.contacts >= 2 && false == Device.equalizer.name.empty() ) { + Result.parts.push_back( placed_along( "equalizer", Device.equalizer.name, end, out ) ); + end = end + out * Device.equalizer.length; + for( int k = 0; k < Input.contacts; ++k ) { + auto const offset { ( k - 0.5 * ( Input.contacts - 1 ) ) * Device.equalizer_spread }; + ends.push_back( end + vec { 0.0, offset, 0.0 } ); + } + } + else { + ends.push_back( end ); + } + for( std::size_t k = 0; k < ends.size(); ++k ) { + auto at { ends[ k ] }; + if( false == Device.contact_clamp.name.empty() ) { + Result.parts.push_back( placed_along( "contact_clamp." + std::to_string( k + 1 ), Device.contact_clamp.name, at, out ) ); + at = at + out * Device.contact_clamp.length; + } + Result.contact_ends.push_back( { at.x, at.y, at.z } ); + } + } + + // lina: koniec przy rolce stałej (dołem), żyły między rowkami A i B, wolny koniec z rolki stałej w dół + auto const groove { pulley->grooves >= 2 ? 0.25 * pulley->width : 0.0 }; + double const ya { -groove }, yb { groove }; + auto &rope { Result.rope }; + auto const point = [ & ]( vec const &P ) { rope.push_back( { P.x, P.y, P.z } ); }; + auto const arc = [ & ]( vec const &Center, double const Y, double const From, double const To ) { + // kąt w płaszczyźnie X-Z od +X w stronę +Z; punkty co najwyżej co 15 stopni, bez pierwszego + auto const steps { std::max( 1, static_cast( std::ceil( std::abs( To - From ) / ( kPi / 12.0 ) ) ) ) }; + for( int k = 1; k <= steps; ++k ) { + auto const angle { From + ( To - From ) * k / steps }; + point( vec { Center.x + radius * std::cos( angle ), Y, Center.z + radius * std::sin( angle ) } ); + } + }; + point( vec { fixedaxle.x, 0.0, height - radius } ); + point( vec { movingaxle.x, ya, height - radius } ); + arc( movingaxle, ya, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedaxle.x, ya, height + radius } ); + arc( fixedaxle, ya, 0.5 * kPi, 1.5 * kPi ); + point( vec { movingaxle.x, yb, height - radius } ); + arc( movingaxle, yb, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedaxle.x, yb, height + radius } ); + arc( fixedaxle, yb, 0.5 * kPi, kPi ); + + // pręt z ciężarami pod liną, łączniki między liną a prętem + double hangerlength { 0.0 }; + for( auto const &link : Device.hanger ) { hangerlength += link.length; } + auto const columnx { fixedaxle.x - radius }; + vec const ropeend { columnx, yb, s + rodlength + hangerlength }; + if( ropeend.z > height - 0.05 ) { Result.warnings.push_back( "the weights would hang above the pulley: rod or hangers too long for the table" ); } + point( ropeend ); + place_chain( Device.hanger, "hanger", ropeend, down, Result ); + { + auto placement { placed( "rod", rodname, vec { columnx, yb, s } ) }; + Result.parts.push_back( placement ); + } + auto z { s }; + int index { 0 }; + int turns { 0 }; // ciężary już leżące na pręcie (każdy kolejny obrócony o "turn" swojego rodzaju) + for( auto const *weight : stack ) { + ++index; + auto const top { index == static_cast( stack.size() ) }; + auto const &color { top && false == Input.top_color.empty() ? Input.top_color : Input.color }; + auto model { weight->name }; + if( false == weight->model.empty() ) { + auto number { tensioner_color( color ) }; + if( number == 0 ) { number = 2; } + number += weight->offset; + model = weight->model + "-" + ( number < 10 ? "0" : "" ) + std::to_string( number ); + } + auto placement { placed( "weight." + std::to_string( index ), model, vec { columnx, yb, z } ) }; + auto const angle { weight->turn * ( turns++ ) * kPi / 180.0 }; + set_rotation( placement, vec { std::cos( angle ), std::sin( angle ), 0.0 }, vec { -std::sin( angle ), std::cos( angle ), 0.0 }, vec { 0.0, 0.0, 1.0 } ); + Result.parts.push_back( placement ); + z += weight->height; + } + if( z > s + rodlength ) { Result.warnings.push_back( "the stack is higher than the rod" ); } + return true; +} + +void tensioner_rope_parts( tensioner_result &Result, std::string const &Cord, double const Length, double const Girth ) { + for( std::size_t k = 1; k < Result.rope.size(); ++k ) { + vec const from { Result.rope[ k - 1 ][ 0 ], Result.rope[ k - 1 ][ 1 ], Result.rope[ k - 1 ][ 2 ] }; + vec const to { Result.rope[ k ][ 0 ], Result.rope[ k ][ 1 ], Result.rope[ k ][ 2 ] }; + auto const span { length( to - from ) }; + if( span < 1e-6 ) { continue; } + auto const z { unit( to - from ) }; + auto const y { side_of( z ) }; + auto placement { placed( "rope." + std::to_string( k ), Cord, from ) }; + set_rotation( placement, cross( y, z ), y, z ); + placement.scale[ 0 ] = Girth; + placement.scale[ 1 ] = Girth; + placement.scale[ 2 ] = Length > 1e-9 ? span / Length : 1.0; + Result.parts.push_back( placement ); + } +} + +} // namespace catenary diff --git a/world/catenary_tensioner.h b/world/catenary_tensioner.h new file mode 100644 index 00000000..0cd5fcef --- /dev/null +++ b/world/catenary_tensioner.h @@ -0,0 +1,180 @@ +/* +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/. +*/ + +// Sieć trakcyjna: urządzenie naprężające (kotwienie ciężarowe), Katalog ESN tom 1 dział 0400 (karta 23-0461 z Uzupełnienia +// nr 2, dawniej 63-0401) i tablica montażowa 33-0011-1. Dane w pliku traction_tensioners.txt. +// Kod niezależny od reszty symulatora (C++17, tylko biblioteka standardowa). +// +// Układ urządzenia: zero na czole konstrukcji wsporczej (od strony kotwionego przęsła), na wysokości główki szyny; +// X poziomo od konstrukcji w stronę kotwionego przęsła (wzdłuż kierunku drutów), Z w górę, Y = Z x X (w lewo patrząc +// wzdłuż X). Wymiary w metrach. +// +// Budowa (rolki równoległe, przekładnia 1:4): na konstrukcji uchwyt przegubowy 4450 i uchwyt 4440 trzymają rolkę stałą +// (podwójną 4460-1), w odległości b od czoła wisi rolka ruchoma (też podwójna), od niej uchwyt 4440 do osi dźwigni 6406. +// Do otworów dźwigni idą łańcuchy części do liny nośnej i do przewodów jezdnych (łącznik, izolator, łącznik, śruba +// rzymska, wyrównywacz, uchwyty krańcowe). Lina stalowa (cięgło 4490): koniec zaczepiony przy rolce stałej, cztery żyły +// między rolkami (rowek A, potem B), wolny koniec z rolki stałej pionowo w dół do pręta ciężarów; na pręcie stos +// ciężarów, którego spód wisi s nad główką szyny. b i s z tablicy montażowej (temperatura, odległość od kotwienia +// stałego albo środkowego). ZAŁOŻENIE przebiegu liny (Jan 2026-10-23: do sprawdzenia na zdjęciach). +// +// Ułożenie modeli (w układzie części): +// rolka 4460 zero w osi, koło w płaszczyźnie X-Z, oś wzdłuż Y +// części liniowe (uchwyty 4440/4450, łączniki, izolatory, śruby, wyrównywacz, uchwyty krańcowe, łączniki na linie) +// zero w sworzniu od strony konstrukcji/dźwigni, biegną w +X do drugiego sworznia (długość z danych) +// dźwignia 6406 zero w otworze I, otwory w +Z (I na dole), otwór III odsunięty w +X; przy otworze liny poniżej +// otworu przewodów dźwignia odwrócona (obrót o 180 stopni wokół Y) +// pręt 6401/6402 zero na dole (pod stosem), w +Z do zaczepu na górze (długość z danych) +// ciężar 63xx zero w środku spodu, w +Z; kolejne obracane o "turn" wokół Z; kolor numerem wykonania modelu +// lina linka rozciągana (część typu cord, zero w zaczepie, w +Z, skalowana wzdłuż Z) +#pragma once +#include +#include +#include + +namespace catenary { + +// tablica montażowa urządzenia naprężającego: b (odległość osi rolki ruchomej od czoła konstrukcji) i s (wysokość spodu +// ciężarów nad główką szyny) w funkcji temperatury i odległości od kotwienia stałego/środkowego +struct tensioner_table { + std::string name; + double height { 6.70 }; // oś rolek nad główką szyny + double rod { 2.00 }; // długość pręta ciężarów, dla której liczono tablicę + std::vector temperatures; // °C, rosnąco + std::vector distances; // m, rosnąco + std::vector> b, s; // [odległość][temperatura] + // wzór z karty 13-0011 (stary katalog): b = bn - as·α·(t - tn), s = sn - 4·as·α·(t - tn); gdy podany, liczy się z niego + // (wiersze tablicy zostają do porównania), bez przycinania do zakresu + bool formula { false }; + double bn { 1.75 }, sn { 2.25 }, tn { 10.0 }, alpha { 17e-6 }; + // ze wzoru, niezależnie od tego, czy tablica go używa + void formula_values( double Temperature, double Distance, double &B, double &S ) const; + // wartości dla temperatury i odległości (interpolacja dwuliniowa, poza zakresem przycięte do brzegu); + // zwraca false, gdy którakolwiek wartość była poza zakresem tablicy + bool lookup( double Temperature, double Distance, double &B, double &S ) const; +}; + +struct tensioner_weight { + std::string name; + double height { 0.055 }; + double mass { 27.0 }; // kg + double turn { 0.0 }; // stopnie: ciężar na n-tym miejscu stosu (od 0) obrócony o n·turn wokół pionu (wszystkie 90) + std::string model; // rdzeń nazwy modelu, np. 6321: model "-", NN = offset + numer koloru + int offset { 0 }; // 6322: 0 niskie (56 mm), 4 wysokie (92 mm) - dwie wysokości produkowane (Jan) +}; + +// numer koloru ciężaru w modelach (wykonania 1-4, Jan 2026-10-23): 1 żółty, 2 szary ładny, 3 żółty pędzlowany, 4 szary brzydki; +// przyjmuje nazwę (zolty, szary, zolty_pedzel, szary_brzydki) albo cyfrę; 0 gdy nieznany +int tensioner_color( std::string const &Name ); + +struct tensioner_pulley { + std::string name; + double diameter { 0.220 }; // po dnie rowka + double width { 0.080 }; + int grooves { 2 }; +}; + +struct tensioner_lever { + std::string name; + std::map> holes; // nazwa otworu -> (wzdłuż osi od otworu I, odsunięcie w bok) +}; + +// część liniowa w łańcuchu +struct tensioner_link { + std::string name; + double length { 0.0 }; +}; + +// stos ciężarów i otwory dźwigni dla typu sieci (numer typu z karty 0002 = traction_code) +struct tensioner_type { + int code { -1 }; + std::vector> stack; // od dołu: ciężar, liczba + std::string messenger_hole, contact_hole, axis_hole; + std::string rod; // pręt, gdy inny niż urządzenia + std::string messenger_clamp; // uchwyt krańcowy liny nośnej, gdy inny niż urządzenia +}; + +struct tensioner_device { + std::string name; // karta, np. 0461 + std::string table, pulley, lever, rod, rope; + double rope_diameter { 0.010 }; + std::vector mount; // od czoła konstrukcji do osi rolki stałej + std::vector holder; // od osi rolki ruchomej do osi dźwigni + std::vector hanger; // od rolki stałej w dół do pręta (bez liny) + std::vector messenger; // od otworu dźwigni do uchwytu krańcowego liny nośnej (bez uchwytu) + std::vector contact; // od otworu dźwigni do wyrównywacza albo uchwytu krańcowego przewodu + tensioner_link messenger_clamp, contact_clamp; + tensioner_link equalizer; // wyrównywacz przewodów jezdnych (tylko przy dwóch przewodach) + double equalizer_spread { 0.08 }; // rozstaw przewodów za wyrównywaczem + std::vector types; + tensioner_type const *type( int Code ) const; +}; + +class tensioner_catalog { +public: + bool load( std::string const &Filename, std::vector &Errors ); + void parse( std::vector const &Lines, std::string const &Source, std::vector &Errors ); + tensioner_table const *table( std::string const &Name ) const; + tensioner_weight const *weight( std::string const &Name ) const; + tensioner_pulley const *pulley( std::string const &Name ) const; + tensioner_lever const *lever( std::string const &Name ) const; + tensioner_device const *device( std::string const &Name ) const; + std::vector const &devices() const { return m_devices; } + std::map const &part_lengths() const { return m_lengths; } // długości części liniowych (pręt) + +private: + std::vector m_tables; + std::vector m_weights; + std::vector m_pulleys; + std::vector m_levers; + std::vector m_devices; + std::map m_lengths; +}; + +struct tensioner_input { + int type { 3 }; // numer typu sieci (traction_code) + int contacts { 2 }; // liczba przewodów jezdnych + double temperature { 10.0 }; // °C + double distance { 700.0 }; // m, od kotwienia stałego albo środkowego + double elongation { 0.0 }; // m, trwałe wydłużenie sieci od ostatniej regulacji (rolka bliżej konstrukcji) + int missing { 0 }; // brakujące ciężary (z góry stosu) + std::string color; // kolor ciężarów (tensioner_color); pusty = szary + std::string top_color; // kolor najwyższego ciężaru; pusty = jak reszta + double ground { -0.50 }; // najniższe położenie spodu ciężarów względem główki szyny (grunt, fundament); ZAŁOŻENIE + double gravity { 0.981 }; // daN/kg +}; + +struct tensioner_placement { + std::string role; + std::string part; + double position[ 3 ] { 0.0, 0.0, 0.0 }; + double rotation[ 9 ] { 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 }; // wierszami, kolumny to osie części + double scale[ 3 ] { 1.0, 1.0, 1.0 }; +}; + +struct tensioner_result { + std::vector parts; + std::vector> rope; // punkty liny (x, y, z) po kolei, od zaczepu przy rolce stałej do pręta + double b { 0.0 }, s { 0.0 }; // położenie rolki ruchomej i spodu ciężarów + double stack_height { 0.0 }; + int weights { 0 }; // zawieszone ciężary + double tension { 0.0 }; // daN, całkowity naciąg sieci od ciężarów (zero, gdy ciężary stoją na gruncie) + double messenger_tension { 0.0 }, contact_tension { 0.0 }; + double messenger_end[ 3 ] { 0.0, 0.0, 0.0 }; // gdzie kończy się lina nośna (uchwyt krańcowy) + std::vector> contact_ends; // gdzie kończą się przewody jezdne + bool grounded { false }; // ciężary oparte o grunt: sieć bez naciągu od urządzenia + std::vector warnings; +}; + +// składa urządzenie. false, gdy brakuje danych (urządzenia, typu, tablicy, rolki, dźwigni, otworów) +bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const &Device, tensioner_input const &Input, tensioner_result &Result ); + +// lina jako odcinki linki rozciąganej (część Cord o długości spoczynkowej Length, skala w poprzek Girth) +void tensioner_rope_parts( tensioner_result &Result, std::string const &Cord, double Length, double Girth ); + +} // namespace catenary