17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-29 11:37:29 +01:00
Files
maszyna/world/catenary_scene.cpp
Claude f30fefd91c Sieć trakcyjna: przęsła wg typów sieci, wysięgniki z katalogu, bramki, kotwienia i edytor sieci
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Tqn7T8D1Gx3ptKtZgNtX5
2026-10-23 23:15:00 +02:00

1603 lines
75 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://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 <algorithm>
#include <limits>
#include "utilities/Logs.h"
#include "utilities/parser.h"
#include <cmath>
#include <fstream>
#include <locale>
#include <set>
#include <sstream>
// 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<TTraction *> messenger; // przęsła, których linę nośną trzyma
std::vector<TTraction *> 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<TAnimModel *> parts; // części wysięgnika wstawione do scenerii
bool removed { false }; // usunięty w edytorze
};
std::vector<scene_suspension> 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<scene_file> Files;
// łańcuch sieci z pliku: przęsła od podwieszenia do podwieszenia
struct scene_chain {
catenary::chain data;
std::string file;
std::vector<TTraction *> spans; // przęsło k: od podwieszenia k do k+1; nullptr gdy nie dało się go zrobić
bool removed { false };
};
std::vector<scene_chain> Chains;
// bramka z pliku: słupy i dźwigar wstawione jako modele
struct scene_gantry {
catenary::gantry data;
std::string file;
std::vector<TAnimModel *> parts;
bool removed { false };
};
std::vector<scene_gantry> 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<TAnimModel *> 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<scene_anchor> 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<std::size_t>
gantries_at( double const *Point, std::size_t const Except ) {
std::vector<std::size_t> 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<std::string> 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<std::string> 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<std::string> 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-<zestaw>, 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<std::string, std::string> 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<std::pair<std::string, std::vector<std::string>>> 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<std::string, std::string> 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<std::pair<double, std::string>> 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<std::string> 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<std::size_t>::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<std::size_t> 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<std::size_t> 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<catenary::cantilever_placement> 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<std::size_t>( &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<std::pair<catenary::cantilever_placement, double>> 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<std::string> 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<std::size_t>( 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<scene_anchor const *>( 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<int>( 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<std::size_t>
umbrella_members( std::string const &Name ) {
std::vector<std::size_t> 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<std::string>( 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<std::string>();
} while( false == token.empty() && token != "endcatenary" );
}
if( false == Global.bLoadTraction ) { return; }
std::vector<std::string> tokens;
{
cParser parser( filename, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction );
auto token { parser.getToken<std::string>( false ) };
while( false == token.empty() ) {
tokens.emplace_back( token );
token = parser.getToken<std::string>( false );
}
}
if( tokens.empty() ) {
ErrorLog( "Bad scenario: catenary file \"" + filename + "\" is missing or empty" );
return;
}
catenary::suspension_file file;
std::vector<std::string> 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<std::string, std::vector<std::size_t>> 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<std::string> const &Names, std::vector<TTraction *> &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<std::string> 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<std::string>
catenary_scene_files() {
std::vector<std::string> 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<std::string>
catenary_scene_new_files() {
std::vector<std::string> 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<std::string> 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<std::size_t>
catenary_chain_members( std::size_t const Index ) {
std::vector<std::size_t> 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<std::size_t>( found - chain.data.suspensions.begin() );
return static_cast<int>( 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<std::size_t> 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 );
}
}