From b4b1e6c32f453c56c98dbbf8cd7e58e6af3655f5 Mon Sep 17 00:00:00 2001 From: IwanXXI Date: Sat, 10 Oct 2026 21:22:39 +0200 Subject: [PATCH 1/3] Broadcast radio receiver in the cab Plays internet radio streams (http, mp3) through a positional emitter in the cab, with analog tuning, inter-station noise and two speaker profiles. Stations are listed in fmradio.txt, cab controls are defined with fmradio entries in the .mmd file. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 2 + audio/audiorenderer.cpp | 7 + audio/audiorenderer.h | 4 + audio/fmradio.cpp | 640 +++++ audio/fmradio.h | 141 + audio/radiostream.cpp | 910 ++++++ audio/radiostream.h | 108 + audio/sound.cpp | 12 + audio/sound.h | 3 + input/command.cpp | 14 + input/command.h | 7 + input/drivermouseinput.cpp | 9 + ref/dr_libs/dr_mp3.h | 5430 ++++++++++++++++++++++++++++++++++++ utilities/Globals.cpp | 21 + utilities/Globals.h | 4 + vehicle/Train.cpp | 178 ++ vehicle/Train.h | 19 + 17 files changed, 7509 insertions(+) create mode 100644 audio/fmradio.cpp create mode 100644 audio/fmradio.h create mode 100644 audio/radiostream.cpp create mode 100644 audio/radiostream.h create mode 100644 ref/dr_libs/dr_mp3.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 477f584e..72d3f05c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,6 +154,8 @@ set(SOURCES "model/vertex.cpp" "audio/audio.cpp" "audio/audiorenderer.cpp" +"audio/radiostream.cpp" +"audio/fmradio.cpp" "utilities/motiontelemetry.cpp" "application/uitranscripts.cpp" "world/station.cpp" diff --git a/audio/audiorenderer.cpp b/audio/audiorenderer.cpp index 080ab66f..0d4c94ff 100644 --- a/audio/audiorenderer.cpp +++ b/audio/audiorenderer.cpp @@ -274,6 +274,8 @@ openal_renderer::~openal_renderer() { if( m_alcEventCallbackSOFT != nullptr ) { m_alcEventCallbackSOFT( nullptr, nullptr ); } // stop callbacks before teardown + if( m_context != nullptr ) { m_fmradio.release(); } // the receiver owns implementation-side resources, which have to go before the context does + ::alcMakeContextCurrent( nullptr ); if( m_context != nullptr ) { ::alcDestroyContext( m_context ); } @@ -487,6 +489,11 @@ openal_renderer::update( double const Deltatime ) { } } + // update streamed sound + if( m_ready ) { + m_fmradio.update( Deltatime, cached_camerapos ); + } + // reset potentially used volume change flag audio::event_volume_change = false; diff --git a/audio/audiorenderer.h b/audio/audiorenderer.h index 3f462eb4..b592696c 100644 --- a/audio/audiorenderer.h +++ b/audio/audiorenderer.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "audio/audio.h" +#include "audio/fmradio.h" #include "model/ResourceManager.h" #include "application/uitranscripts.h" @@ -121,6 +122,8 @@ public: void erase( sound_source const *Controller ); void update( double const Deltatime ); + // provides access to the broadcast radio receiver + audio::fm_radio &fmradio() { return m_fmradio; } glm::dvec3 cached_camerapos; @@ -142,6 +145,7 @@ private: buffer_manager m_buffers; source_list m_sources; source_sequence m_sourcespares; + audio::fm_radio m_fmradio; // streamed sound, maintained separately from regular emitters // OpenAL Soft extension entry points, resolved at runtime via al(c)GetProcAddress LPALDEFERUPDATESSOFT m_alDeferUpdatesSOFT { nullptr }; diff --git a/audio/fmradio.cpp b/audio/fmradio.cpp new file mode 100644 index 00000000..f1fcfaa4 --- /dev/null +++ b/audio/fmradio.cpp @@ -0,0 +1,640 @@ +/* +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 "audio/fmradio.h" + +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +namespace audio +{ + +namespace +{ + +auto const EU07_FMRADIO_BUFFERCOUNT{8}; // number of sound buffers owned by the receiver +auto const EU07_FMRADIO_QUEUELENGTH{6}; // max number of sound buffers waiting for playback. kept short, so the sound follows the knobs without noticeable delay +auto const EU07_FMRADIO_BUFFERSPERSECOND{40}; // inverse of the length of a single sound buffer +// sample rate of generated audio. it has to stay the same for all buffers queued in the emitter, so station audio is converted to it if needed +unsigned int const EU07_FMRADIO_SAMPLERATE{44100}; +auto const EU07_FMRADIO_PREBUFFERLENGTH{1.0}; // seconds of station audio required to let it through +auto const EU07_FMRADIO_UNATTENDEDLIMIT{1.0}; // seconds without contact from the owner after which the receiver turns itself off +auto const EU07_FMRADIO_CAPTURERANGE{0.15f}; // MHz, max distance from the station frequency at which the station can be heard +auto const EU07_FMRADIO_CLEANRANGE{0.03f}; // MHz, max distance from the station frequency at which the reception is undisturbed +auto const EU07_FMRADIO_SETTLETIME{0.1}; // seconds the tuning knob has to spend at a station before the receiver connects with its stream +// seconds the connection with a station is kept after the tuning knob moves away from it. +// allows to pass a station, hear where it was and come back to it without waiting for another connection +auto const EU07_FMRADIO_LINGERTIME{8.0}; +auto const EU07_FMRADIO_CARRIERNOISE{0.02f}; // fraction of the noise left when the receiver is locked on a station +// the receiver makes a short electric crackle when it locks on a station or lets go of it +auto const EU07_FMRADIO_CRACKLELEVEL{0.25f}; +auto const EU07_FMRADIO_CRACKLEDENSITY{0.12f}; // fraction of samples which carry a spike +auto const EU07_FMRADIO_CRACKLEDECAY{0.008f}; // seconds +auto const EU07_FMRADIO_CRACKLELENGTH{0.04f}; // seconds +auto const EU07_FMRADIO_NOISELEVEL{0.3f}; // amplitude of the noise heard between the stations +// level of the receiver signal at the input of the simulated amplifier. modern stations broadcast at full scale most of the time, +// so without some headroom the amplifier and the filters boosting parts of the spectrum would be driven into heavy distortion +auto const EU07_FMRADIO_HEADROOM{0.5f}; +auto const EU07_FMRADIO_LIMITERKNEE{0.9f}; // output level above which the peaks are rounded off +// gain of the emitter with the volume knob turned all the way up. regular cab sounds are short and loud, +// while a broadcast played at nominal gain gets lost in the noise of a running vehicle +auto const EU07_FMRADIO_LOUDNESS{3.f}; +auto const EU07_FMRADIO_PI{3.14159265358979f}; + +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); +} + +} // namespace + +// filter designs follow the audio eq cookbook by Robert Bristow-Johnson +fm_radio::biquad fm_radio::biquad::highpass(float const Rate, float const Frequency, float const Q) +{ + auto const omega{2.f * EU07_FMRADIO_PI * std::min(Frequency, Rate * 0.45f) / Rate}; + auto const cosine{std::cos(omega)}; + auto const alpha{std::sin(omega) / (2.f * Q)}; + auto const a0{1.f + alpha}; + biquad filter; + filter.b0 = (1.f + cosine) * 0.5f / a0; + filter.b1 = -(1.f + cosine) / a0; + filter.b2 = filter.b0; + filter.a1 = -2.f * cosine / a0; + filter.a2 = (1.f - alpha) / a0; + return filter; +} + +fm_radio::biquad fm_radio::biquad::lowpass(float const Rate, float const Frequency, float const Q) +{ + auto const omega{2.f * EU07_FMRADIO_PI * std::min(Frequency, Rate * 0.45f) / Rate}; + auto const cosine{std::cos(omega)}; + auto const alpha{std::sin(omega) / (2.f * Q)}; + auto const a0{1.f + alpha}; + biquad filter; + filter.b0 = (1.f - cosine) * 0.5f / a0; + filter.b1 = (1.f - cosine) / a0; + filter.b2 = filter.b0; + filter.a1 = -2.f * cosine / a0; + filter.a2 = (1.f - alpha) / a0; + return filter; +} + +// Gain is in decibels +fm_radio::biquad fm_radio::biquad::peak(float const Rate, float const Frequency, float const Q, float const Gain) +{ + auto const amplitude{std::pow(10.f, Gain / 40.f)}; + auto const omega{2.f * EU07_FMRADIO_PI * std::min(Frequency, Rate * 0.45f) / Rate}; + auto const cosine{std::cos(omega)}; + auto const alpha{std::sin(omega) / (2.f * Q)}; + auto const a0{1.f + alpha / amplitude}; + biquad filter; + filter.b0 = (1.f + alpha * amplitude) / a0; + filter.b1 = -2.f * cosine / a0; + filter.b2 = (1.f - alpha * amplitude) / a0; + filter.a1 = filter.b1; + filter.a2 = (1.f - alpha / amplitude) / a0; + return filter; +} + +void fm_radio::power(bool const State) +{ + std::scoped_lock lock{m_controlmutex}; + m_control.power = State; + m_control.unattended = 0.0; +} + +// sets frequency the receiver is tuned to, in MHz +void fm_radio::tune(float const Frequency) +{ + std::scoped_lock lock{m_controlmutex}; + m_control.frequency = std::clamp(Frequency, EU07_FMRADIO_BANDSTART, EU07_FMRADIO_BANDEND); +} + +// sets position of the volume knob, in range 0-1 +void fm_radio::volume(float const Volume) +{ + std::scoped_lock lock{m_controlmutex}; + m_control.volume = std::clamp(Volume, 0.f, 1.f); +} + +void fm_radio::type(fmradio_type const Type) +{ + std::scoped_lock lock{m_controlmutex}; + m_control.type = Type; +} + +// sets location of the receiver in the simulation region space, and audibility parameters at the current position of the listener +void fm_radio::placement(glm::dvec3 const &Location, float const Soundproofing, float const Range) +{ + std::scoped_lock lock{m_controlmutex}; + m_control.location = Location; + m_control.soundproofing = Soundproofing; + m_control.range = std::max(1.f, Range); +} + +// returns frequency of the first station on the list, or middle of the band if there's none +float fm_radio::default_frequency() +{ + load_stations(); + return m_stations.empty() ? (EU07_FMRADIO_BANDSTART + EU07_FMRADIO_BANDEND) * 0.5f : m_stations.front().frequency; +} + +// imports the list of stations. each line of the file holds name of a station, address of its stream and its frequency, separated with semicolons +void fm_radio::load_stations() +{ + std::scoped_lock lock{m_stationsmutex}; + if (m_stationsloaded) + { + return; + } + m_stationsloaded = true; + + std::ifstream input{Global.fmradio_stations}; + if (false == input.is_open()) + { + // NOTE: the list isn't a part of the simulator and it's up to the user to provide one, so its absence isn't an error + WriteLog("fm radio: no station list found at \"" + Global.fmradio_stations + "\", the receiver will pick up only noise", logtype::sound); + return; + } + std::string line; + auto linenumber{0}; + while (std::getline(input, line)) + { + ++linenumber; + if (linenumber == 1 && line.rfind("\xEF\xBB\xBF", 0) == 0) + { + line.erase(0, 3); // byte order mark + } + line = trim(line); + if (line.empty() || line.front() == '#' || line.rfind("//", 0) == 0) + { + continue; + } + // NOTE: the address can potentially contain semicolons, so we take the outermost ones as separators + auto const nameend{line.find(';')}; + auto const frequencystart{line.rfind(';')}; + if (nameend == std::string::npos || frequencystart == nameend) + { + ErrorLog("fm radio: malformed entry in line " + std::to_string(linenumber) + " of the station list, expected \"name; address; frequency\"", logtype::sound); + continue; + } + station entry; + entry.name = trim(line.substr(0, nameend)); + entry.url = trim(line.substr(nameend + 1, frequencystart - nameend - 1)); + auto frequency{trim(line.substr(frequencystart + 1))}; + std::replace(std::begin(frequency), std::end(frequency), ',', '.'); + entry.frequency = static_cast(std::atof(frequency.c_str())); + if (entry.url.empty() || entry.frequency < EU07_FMRADIO_BANDSTART || entry.frequency > EU07_FMRADIO_BANDEND) + { + ErrorLog("fm radio: station \"" + entry.name + "\" in line " + std::to_string(linenumber) + " of the station list has no address or its frequency is outside of 87.5-108 MHz range", logtype::sound); + continue; + } + m_stations.emplace_back(entry); + } + WriteLog(m_stations.empty() ? "fm radio: the station list is empty, the receiver will pick up only noise" : "fm radio: " + std::to_string(m_stations.size()) + " station(s) on the list", logtype::sound); +} + +// returns index of the station which can be received at specified frequency, or -1 +int fm_radio::find_station(float const Frequency) const +{ + auto result{-1}; + auto distance{EU07_FMRADIO_CAPTURERANGE}; + for (std::size_t index{0}; index < m_stations.size(); ++index) + { + auto const stationdistance{std::abs(m_stations[index].frequency - Frequency)}; + if (stationdistance < distance) + { + distance = stationdistance; + result = static_cast(index); + } + } + return result; +} + +// selects the station to receive, taking into account movement of the tuning knob +void fm_radio::update_tuning(control_block const &Control, double const Deltatime) +{ + auto const candidate{find_station(Control.frequency)}; + if (candidate != m_candidate) + { + m_candidate = candidate; + m_candidatetime = 0.0; + } + else + { + m_candidatetime += Deltatime; + } + // NOTE: connection is made after the knob stays at the station for a moment, to skip stations passed at full speed + if (candidate != -1 && candidate != m_station && m_candidatetime >= EU07_FMRADIO_SETTLETIME) + { + m_station = candidate; + WriteLog("fm radio: tuned to \"" + m_stations[m_station].name + "\", " + std::to_string(m_stations[m_station].frequency) + " MHz", logtype::sound); + m_stream.open(m_stations[m_station].url); + m_buffering = true; + m_inputphase = 0.0; + m_inputsamples = {0, 0}; + } + if (m_station != -1) + { + if (m_station == candidate) + { + m_lingertime = 0.0; + } + else + { + // the knob has left the station we were connected with. the connection is kept for a while in case the knob comes back + m_lingertime += Deltatime; + if (m_lingertime > EU07_FMRADIO_LINGERTIME) + { + m_stream.close(); + m_station = -1; + } + } + } +} + +// prepares speaker simulation for specified receiver +void fm_radio::setup_speaker(fmradio_type const Type) +{ + m_speakertype = Type; + m_speaker.clear(); + auto const rate{static_cast(EU07_FMRADIO_SAMPLERATE)}; + switch (Type) + { + case fmradio_type::cheap: + { + // tiny speaker in a thin plastic case: no bass to speak of, harsh midrange, overdriven amplifier + m_speaker.emplace_back(biquad::highpass(rate, 420.f, 0.8f)); + m_speaker.emplace_back(biquad::highpass(rate, 300.f, 0.6f)); + m_speaker.emplace_back(biquad::peak(rate, 1900.f, 1.4f, 7.f)); + m_speaker.emplace_back(biquad::lowpass(rate, 3400.f, 0.9f)); + m_speakerdrive = 2.2f; + m_speakergain = 0.75f; + break; + } + case fmradio_type::wooden: + { + // large speaker in a wooden cabinet: full, slightly boomy lows and soft, rolled off highs + m_speaker.emplace_back(biquad::highpass(rate, 65.f, 0.7f)); + m_speaker.emplace_back(biquad::peak(rate, 130.f, 0.9f, 5.f)); + m_speaker.emplace_back(biquad::peak(rate, 2400.f, 0.8f, -2.5f)); + m_speaker.emplace_back(biquad::lowpass(rate, 4200.f, 0.6f)); + m_speakerdrive = 1.f; + m_speakergain = 1.1f; + break; + } + } +} + +// fills the staging area with next piece of the receiver output +void fm_radio::generate_chunk(control_block const &Control, std::size_t const Samplecount) +{ + m_chunk.assign(Samplecount, 0); + m_mix.resize(Samplecount); + + // strength of the station signal at the current position of the tuning knob. it drops with the distance from the station frequency + auto carrier{0.f}; + if (m_candidate != -1) + { + auto const distance{std::abs(m_stations[m_candidate].frequency - Control.frequency)}; + auto const detune{std::clamp((distance - EU07_FMRADIO_CLEANRANGE) / (EU07_FMRADIO_CAPTURERANGE - EU07_FMRADIO_CLEANRANGE), 0.f, 1.f)}; + carrier = 1.f - detune * detune * (3.f - 2.f * detune); + } + // the moment the receiver locks on a station or loses it is marked with a crackle. + // together with the drop of the noise it tells there's a station, even if it's passed too quickly to make out anything else + // NOTE: the thresholds are set apart, so the receiver doesn't keep thumping when the knob is left right at the edge + auto const locked{m_locked ? carrier > 0.4f : carrier >= 0.6f}; + if (locked != m_locked) + { + m_locked = locked; + m_thumpposition = 0; + } + + // station audio + // NOTE: it's available only if we're connected with the station at the knob, the connection may be still in progress or kept with another station + auto reception{0.f}; + if (m_station != -1 && m_station == m_candidate) + { + auto const streamrate{m_stream.rate()}; + if (m_buffering && streamrate > 0 && m_stream.available() >= static_cast(streamrate * EU07_FMRADIO_PREBUFFERLENGTH)) + { + m_buffering = false; + } + if (false == m_buffering) + { + // station audio is converted to our sample rate with linear interpolation, which is plenty for a radio speaker + auto const step{static_cast(streamrate) / EU07_FMRADIO_SAMPLERATE}; + m_input.assign(static_cast(m_inputphase + step * Samplecount), 0); + if (m_stream.read(m_input.data(), m_input.size()) < m_input.size()) + { + // we ran out of data. the missing part of the chunk is already silent, and the station is put on hold until the reserve is rebuilt + m_buffering = true; + } + std::size_t inputindex{0}; + for (std::size_t index{0}; index < Samplecount; ++index) + { + m_inputphase += step; + while (m_inputphase >= 1.0) + { + m_inputsamples[0] = m_inputsamples[1]; + m_inputsamples[1] = (inputindex < m_input.size() ? m_input[inputindex++] : 0); + m_inputphase -= 1.0; + } + m_chunk[index] = static_cast(m_inputsamples[0] + (m_inputsamples[1] - m_inputsamples[0]) * m_inputphase); + } + reception = carrier; + } + } + + // mix of the station, the noise and the crackle + auto const receptionstep{(reception - m_reception) / static_cast(Samplecount)}; + auto const carrierstep{(carrier - m_carrier) / static_cast(Samplecount)}; + auto const cracklelength{static_cast(EU07_FMRADIO_CRACKLELENGTH * EU07_FMRADIO_SAMPLERATE)}; + for (std::size_t index{0}; index < Samplecount; ++index) + { + // NOTE: the clamps matter, accumulated rounding errors can push the values slightly below zero and the square root further down wouldn't like it + m_reception = std::clamp(m_reception + receptionstep, 0.f, 1.f); + m_carrier = std::clamp(m_carrier + carrierstep, 0.f, 1.f); + // white noise from a xorshift generator + m_noisestate ^= m_noisestate << 13; + m_noisestate ^= m_noisestate >> 17; + m_noisestate ^= m_noisestate << 5; + auto const noise{static_cast(m_noisestate) * (2.f / 4294967296.f) - 1.f}; + auto signal{m_chunk[index] * (1.f / 32768.f)}; + if (m_reception < 0.999f) + { + // an fm receiver which is off the station frequency cuts off peaks of the signal, the further the stronger + auto const ceiling{0.05f + 0.95f * m_reception}; + signal = std::clamp(signal * (1.f + 2.f * (1.f - m_reception)), -ceiling, ceiling); + } + // the noise is pushed down by the station signal itself, whether there's anything to hear on it or not + auto const noisegain{std::max((1.f - m_carrier) * (1.f - m_carrier), EU07_FMRADIO_CARRIERNOISE)}; + auto sample{signal * std::sqrt(m_reception) + noise * noisegain * EU07_FMRADIO_NOISELEVEL}; + if (m_thumpposition >= 0) + { + // sparse spikes of random polarity, fading out quickly + auto const time{static_cast(m_thumpposition) / EU07_FMRADIO_SAMPLERATE}; + if (std::abs(noise) > 1.f - EU07_FMRADIO_CRACKLEDENSITY) + { + sample += (noise < 0.f ? -EU07_FMRADIO_CRACKLELEVEL : EU07_FMRADIO_CRACKLELEVEL) * std::exp(-time / EU07_FMRADIO_CRACKLEDECAY); + } + if (++m_thumpposition >= cracklelength) + { + m_thumpposition = -1; + } + } + m_mix[index] = sample; + } + m_reception = reception; + m_carrier = carrier; + + // speaker + if (false == Global.fmradio_cleansound) + { + if (m_speaker.empty() || m_speakertype != Control.type) + { + setup_speaker(Control.type); + } + for (auto &sample : m_mix) + { + // amplifier, with soft saturation + sample = std::tanh(sample * EU07_FMRADIO_HEADROOM * m_speakerdrive); + for (auto &filter : m_speaker) + { + sample = filter.process(sample); + } + sample *= m_speakergain; + } + // safety net, an invalid value stuck in a filter would silence the receiver for good + for (auto &filter : m_speaker) + { + if (false == std::isfinite(filter.z1) || false == std::isfinite(filter.z2)) + { + filter.z1 = 0.f; + filter.z2 = 0.f; + std::fill(std::begin(m_mix), std::end(m_mix), 0.f); + } + } + } + + for (std::size_t index{0}; index < Samplecount; ++index) + { + // peaks which still go over the top are rounded off, as hard clipping sounds much worse + auto sample{m_mix[index]}; + auto const level{std::abs(sample)}; + if (level > EU07_FMRADIO_LIMITERKNEE) + { + auto const limited{EU07_FMRADIO_LIMITERKNEE + (1.f - EU07_FMRADIO_LIMITERKNEE) * std::tanh((level - EU07_FMRADIO_LIMITERKNEE) / (1.f - EU07_FMRADIO_LIMITERKNEE))}; + sample = (sample < 0.f ? -limited : limited); + } + m_chunk[index] = static_cast(sample * 32767.f); + } +} + +// updates state of the receiver. expects active audio context +void fm_radio::update(double const Deltatime, glm::dvec3 const &Listener) +{ + control_block control; + { + std::scoped_lock lock{m_controlmutex}; + if (m_control.unattended > EU07_FMRADIO_UNATTENDEDLIMIT) + { + // the owner is gone, or has more important things to do + m_control.power = false; + } + // NOTE: the time is added after the check, so a single long frame doesn't count as a lack of contact + m_control.unattended += Deltatime; + control = m_control; + } + + // pass messages from the stream worker to the log + radio_stream::message message; + while (m_stream.fetch_message(message)) + { + if (message.second) + { + ErrorLog(message.first, logtype::sound); + } + else + { + WriteLog(message.first, logtype::sound); + } + } + + if (false == control.power) + { + if (m_active) + { + m_stream.close(); + reset_emitter(); + m_station = -1; + m_candidate = -1; + m_active = false; + } + return; + } + if (false == m_active) + { + // switched on + load_stations(); + m_reception = 0.f; + m_carrier = 0.f; + m_locked = false; + m_thumpposition = -1; + m_active = true; + } + update_tuning(control, Deltatime); + + if (m_source == audio::null_resource && false == create_emitter()) + { + return; + } + + // feed the emitter with generated audio + auto queuedcount{reclaim_buffers()}; + auto const chunksize{static_cast(EU07_FMRADIO_SAMPLERATE / EU07_FMRADIO_BUFFERSPERSECOND)}; + while (queuedcount < EU07_FMRADIO_QUEUELENGTH && false == m_sparebuffers.empty()) + { + generate_chunk(control, chunksize); + auto const buffer{m_sparebuffers.back()}; + m_sparebuffers.pop_back(); + ::alBufferData(buffer, AL_FORMAT_MONO16, m_chunk.data(), static_cast(m_chunk.size() * sizeof(std::int16_t)), static_cast(EU07_FMRADIO_SAMPLERATE)); + ::alSourceQueueBuffers(m_source, 1, &buffer); + ++queuedcount; + } + + // location + auto const offset{glm::vec3{control.location - Listener}}; + ::alSourcefv(m_source, AL_POSITION, glm::value_ptr(offset)); + // gain + // NOTE: the knob position is squared to approximate the response of an audio taper potentiometer + auto gain{EU07_FMRADIO_LOUDNESS * control.volume * control.volume * control.soundproofing * Global.VehicleVolume}; + auto const rangesquared{control.range * control.range}; + auto const distancesquared{glm::length2(offset)}; + if (distancesquared > rangesquared) + { + // outside of nominal hearing range the volume fades out, same way as it does for regular sounds + auto const fadedistance{control.range * 0.75f}; + gain *= std::lerp(1.f, 0.f, std::clamp((distancesquared - rangesquared) / (fadedistance * fadedistance), 0.f, 1.f)); + } + ::alSourcef(m_source, AL_GAIN, gain); + ::alSourcef(m_source, AL_REFERENCE_DISTANCE, control.range * (1.f / 16.f) * control.soundproofing); + + // playback. the emitter stops on its own when it runs out of queued data, so it may need a restart + ALint state{AL_INITIAL}; + ::alGetSourcei(m_source, AL_SOURCE_STATE, &state); + if (state != AL_PLAYING && queuedcount > 0) + { + ::alSourcePlay(m_source); + } +} + +// stops the reception and frees implementation-side resources. expects active audio context +void fm_radio::release() +{ + m_stream.close(); + m_station = -1; + m_candidate = -1; + m_active = false; + if (m_source != audio::null_resource) + { + reset_emitter(); + ::alDeleteSources(1, &m_source); + m_source = audio::null_resource; + } + if (false == m_buffers.empty()) + { + ::alDeleteBuffers(static_cast(m_buffers.size()), m_buffers.data()); + m_buffers.clear(); + m_sparebuffers.clear(); + } +} + +bool fm_radio::create_emitter() +{ + ::alGetError(); // reset the error state, so we don't get blamed for past sins of others + + ::alGenSources(1, &m_source); + if (::alGetError() != AL_NO_ERROR) + { + m_source = audio::null_resource; + if (false == m_emitterfailed) + { + ErrorLog("fm radio: failed to create sound emitter", logtype::sound); + m_emitterfailed = true; + } + return false; + } + if (m_buffers.empty()) + { + m_buffers.resize(EU07_FMRADIO_BUFFERCOUNT); + ::alGenBuffers(static_cast(m_buffers.size()), m_buffers.data()); + if (::alGetError() != AL_NO_ERROR) + { + m_buffers.clear(); + ::alDeleteSources(1, &m_source); + m_source = audio::null_resource; + if (false == m_emitterfailed) + { + ErrorLog("fm radio: failed to create sound buffers", logtype::sound); + m_emitterfailed = true; + } + return false; + } + } + m_sparebuffers = m_buffers; + m_emitterfailed = false; + + ::alSourcef(m_source, AL_PITCH, 1.f); + ::alSourcef(m_source, AL_MAX_GAIN, EU07_FMRADIO_LOUDNESS); // by default the gain is capped at 1 + ::alSourcef(m_source, AL_ROLLOFF_FACTOR, 1.75f); // matches the setup of regular sounds + ::alSourcei(m_source, AL_LOOPING, AL_FALSE); + glm::vec3 const zero{0.f}; + ::alSourcefv(m_source, AL_VELOCITY, glm::value_ptr(zero)); + + return true; +} + +// stops the playback and takes back all sound buffers +void fm_radio::reset_emitter() +{ + if (m_source == audio::null_resource) + { + return; + } + ::alSourceStop(m_source); + ::alSourcei(m_source, AL_BUFFER, 0); // detaches all queued buffers + m_sparebuffers = m_buffers; +} + +// collects buffers which were played back. returns: number of buffers still waiting in the queue +int fm_radio::reclaim_buffers() +{ + ALint processedcount{0}; + ::alGetSourcei(m_source, AL_BUFFERS_PROCESSED, &processedcount); + while (processedcount-- > 0) + { + ALuint buffer{0}; + ::alSourceUnqueueBuffers(m_source, 1, &buffer); + if (buffer != 0 && m_sparebuffers.size() < m_buffers.size()) + { + m_sparebuffers.emplace_back(buffer); + } + } + return static_cast(m_buffers.size() - m_sparebuffers.size()); +} + +} // namespace audio diff --git a/audio/fmradio.h b/audio/fmradio.h new file mode 100644 index 00000000..5c4caf50 --- /dev/null +++ b/audio/fmradio.h @@ -0,0 +1,141 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "audio/audio.h" +#include "audio/radiostream.h" + +namespace audio +{ + +float const EU07_FMRADIO_BANDSTART{87.5f}; // MHz +float const EU07_FMRADIO_BANDEND{108.f}; // MHz + +// kind of the receiver, determines character of its sound +enum class fmradio_type +{ + cheap, // pocket-sized plastic receiver with a tiny speaker + wooden // old table receiver with a large speaker in a wooden cabinet +}; + +// broadcast radio receiver installed in a vehicle cab. +// the stations are internet radio streams assigned to frequencies of the fm band in a text file. +// the sound is generated piece by piece from the stream and synthesized noise, then played through a positional emitter +// owned by the receiver, as this doesn't fit the regular buffer-based sound pipeline +class fm_radio +{ + + public: + // constructors + fm_radio() = default; + // methods + // owner-side interface, to be called by the vehicle the receiver is installed in. + // NOTE: the owner is expected to call power() on regular basis, a receiver left without supervision turns itself off + void power(bool const State); + // sets frequency the receiver is tuned to, in MHz + void tune(float const Frequency); + // sets position of the volume knob, in range 0-1 + void volume(float const Volume); + void type(fmradio_type const Type); + // sets location of the receiver in the simulation region space, and audibility parameters at the current position of the listener + void placement(glm::dvec3 const &Location, float const Soundproofing, float const Range); + // returns frequency of the first station on the list, or middle of the band if there's none + float default_frequency(); + // renderer-side interface + // updates state of the receiver. expects active audio context + void update(double const Deltatime, glm::dvec3 const &Listener); + // stops the reception and frees implementation-side resources. expects active audio context + void release(); + + private: + // types + struct control_block + { + bool power{false}; + float frequency{EU07_FMRADIO_BANDSTART}; + float volume{0.f}; + fmradio_type type{fmradio_type::cheap}; + glm::dvec3 location{0.0}; + float soundproofing{1.f}; + float range{15.f}; + double unattended{0.0}; // time since the last call from the owner + }; + struct station + { + std::string name; + std::string url; + float frequency{0.f}; // MHz + }; + // second order filter section + struct biquad + { + float b0{1.f}, b1{0.f}, b2{0.f}, a1{0.f}, a2{0.f}; + float z1{0.f}, z2{0.f}; + + float process(float const Input) + { + auto const output{b0 * Input + z1}; + z1 = b1 * Input - a1 * output + z2; + z2 = b2 * Input - a2 * output; + return output; + } + static biquad highpass(float const Rate, float const Frequency, float const Q); + static biquad lowpass(float const Rate, float const Frequency, float const Q); + static biquad peak(float const Rate, float const Frequency, float const Q, float const Gain); + }; + // methods + void load_stations(); + // returns index of the station which can be received at specified frequency, or -1 + int find_station(float const Frequency) const; + // selects the station to receive, taking into account movement of the tuning knob + void update_tuning(control_block const &Control, double const Deltatime); + // prepares speaker simulation for specified receiver + void setup_speaker(fmradio_type const Type); + // fills the staging area with next piece of the receiver output + void generate_chunk(control_block const &Control, std::size_t const Samplecount); + bool create_emitter(); + // stops the playback and takes back all sound buffers + void reset_emitter(); + // collects buffers which were played back. returns: number of buffers still waiting in the queue + int reclaim_buffers(); + // members + std::mutex m_controlmutex; + control_block m_control; // settings requested by the owner + std::mutex m_stationsmutex; + std::vector m_stations; + bool m_stationsloaded{false}; + radio_stream m_stream; + bool m_active{false}; // the receiver is switched on + int m_station{-1}; // station the stream is connected with + int m_candidate{-1}; // station at the current position of the tuning knob + double m_candidatetime{0.0}; // time the knob has spent at the candidate station + double m_lingertime{0.0}; // time since the knob has left the station the stream is connected with + bool m_buffering{true}; // station audio is held until there's enough data to keep it going + float m_reception{0.f}; // current quality of the station audio, 0-1 + float m_carrier{0.f}; // current strength of the station signal, 0-1 + bool m_locked{false}; // the receiver is locked on a station signal + int m_thumpposition{-1}; // progress of the sound made when the receiver locks on a station or loses it, in samples. negative if the sound isn't playing + double m_inputphase{0.0}; // sample rate conversion, position between two most recent samples of station audio + std::array m_inputsamples{0, 0}; // sample rate conversion, two most recent samples of station audio + fmradio_type m_speakertype{fmradio_type::cheap}; + std::vector m_speaker; // filters simulating the speaker and the case + float m_speakerdrive{1.f}; // signal level at the input of the simulated amplifier + float m_speakergain{1.f}; // make-up gain + std::uint32_t m_noisestate{0x2545f491}; // state of the noise generator + bool m_emitterfailed{false}; // helper, prevents flooding the log with the same error + ALuint m_source{audio::null_resource}; + std::vector m_buffers; // all sound buffers owned by the receiver + std::vector m_sparebuffers; // sound buffers which aren't queued for playback + std::vector m_chunk; // helper, staging area for audio data + std::vector m_input; // helper, staging area for audio data + std::vector m_mix; // helper, staging area for audio data +}; + +} // namespace audio diff --git a/audio/radiostream.cpp b/audio/radiostream.cpp new file mode 100644 index 00000000..ead876d2 --- /dev/null +++ b/audio/radiostream.cpp @@ -0,0 +1,910 @@ +/* +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 "audio/radiostream.h" + +#ifdef DBG_NEW +#undef new +#endif + +#define ASIO_DISABLE_VISIBILITY +#include + +#define DR_MP3_IMPLEMENTATION +#define DR_MP3_NO_STDIO +#include "ref/dr_libs/dr_mp3.h" + +namespace audio +{ + +namespace +{ + +auto const EU07_RADIOSTREAM_BUFFERLENGTH{8}; // seconds of decoded audio kept for the owner +auto const EU07_RADIOSTREAM_CONNECTTIMEOUT{std::chrono::seconds{10}}; +auto const EU07_RADIOSTREAM_STALLTIMEOUT{std::chrono::seconds{10}}; // max time without any incoming data +auto const EU07_RADIOSTREAM_MAXREDIRECTS{5}; +std::size_t const EU07_RADIOSTREAM_MAXHEADERSIZE{32 * 1024}; +std::size_t const EU07_RADIOSTREAM_MAXPLAYLISTSIZE{64 * 1024}; +// the decoder needs to see the beginning of the next frame to stay in sync, so it's only fed when there's more data than a frame can take. +// we start with the size safe for the largest frames, then scale it down to the frames of the stream, +// otherwise audio of low bitrate streams would be delivered in pieces up to a second long +std::size_t const EU07_RADIOSTREAM_DECODETHRESHOLD{4096}; +std::size_t const EU07_RADIOSTREAM_MINDECODETHRESHOLD{512}; +// amount of data without a single valid frame after which the stream is deemed not to be mp3 +std::size_t const EU07_RADIOSTREAM_MAXJUNKSIZE{512 * 1024}; + +std::string lowercase(std::string Text) +{ + std::transform(std::begin(Text), std::end(Text), std::begin(Text), [](unsigned char const c) { return static_cast(std::tolower(c)); }); + return Text; +} + +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); +} + +bool starts_with_nocase(std::string const &Text, std::string const &Prefix) +{ + return Text.size() >= Prefix.size() && lowercase(Text.substr(0, Prefix.size())) == Prefix; +} + +struct stream_address +{ + std::string host; + std::string port{"80"}; + std::string target{"/"}; + + // host with optional port, as expected by http host header + std::string authority() const + { + auto const name{host.find(':') != std::string::npos ? "[" + host + "]" : host}; + return port == "80" ? name : name + ":" + port; + } +}; + +// splits provided address into components. returns: empty string on success, or description of the problem +std::string parse_address(std::string const &Url, stream_address &Address) +{ + auto url{trim(Url)}; + if (starts_with_nocase(url, "https://")) + { + return "encrypted (https) streams aren't supported"; + } + if (starts_with_nocase(url, "http://")) + { + url.erase(0, 7); + } + else if (url.find("://") != std::string::npos) + { + return "unsupported protocol"; + } + + Address = stream_address{}; + auto const hostend{url.find_first_of("/?")}; + auto authority{url.substr(0, hostend)}; + if (hostend != std::string::npos) + { + Address.target = url.substr(hostend); + if (Address.target.front() == '?') + { + Address.target.insert(0, "/"); + } + } + // credentials aren't supported, but shouldn't prevent the connection + auto const credentialsend{authority.rfind('@')}; + if (credentialsend != std::string::npos) + { + authority.erase(0, credentialsend + 1); + } + if (false == authority.empty() && authority.front() == '[') + { + // ipv6 literal + auto const literalend{authority.find(']')}; + if (literalend == std::string::npos) + { + return "malformed address"; + } + Address.host = authority.substr(1, literalend - 1); + if (literalend + 2 < authority.size() && authority[literalend + 1] == ':') + { + Address.port = authority.substr(literalend + 2); + } + } + else + { + auto const portstart{authority.rfind(':')}; + Address.host = authority.substr(0, portstart); + if (portstart != std::string::npos && portstart + 1 < authority.size()) + { + Address.port = authority.substr(portstart + 1); + } + } + if (Address.host.empty()) + { + return "malformed address"; + } + return {}; +} + +// combines address received in a redirect with the address of the redirecting resource +std::string resolve_redirect(std::string const &Location, stream_address const &Origin) +{ + if (Location.find("://") != std::string::npos) + { + return Location; + } + if (Location.rfind("//", 0) == 0) + { + return "http:" + Location; + } + auto const root{"http://" + Origin.authority()}; + if (false == Location.empty() && Location.front() == '/') + { + return root + Location; + } + auto directory{Origin.target.substr(0, Origin.target.find('?'))}; + directory.erase(directory.rfind('/') + 1); + return root + directory + Location; +} + +// extracts address of the first stream listed in provided m3u or pls playlist. returns: the address, or empty string +std::string parse_playlist(std::string const &Playlist) +{ + std::istringstream lines{Playlist}; + std::string line; + while (std::getline(lines, line)) + { + line = trim(line); + if (line.empty() || line.front() == '#' || line.front() == '[') + { + continue; + } + // pls entries have form of FileN=address, m3u ones are plain addresses + if (starts_with_nocase(line, "file")) + { + auto const separator{line.find('=')}; + if (separator != std::string::npos) + { + line = trim(line.substr(separator + 1)); + } + } + if (starts_with_nocase(line, "http://") || starts_with_nocase(line, "https://")) + { + return line; + } + } + return {}; +} + +struct http_response +{ + int status{0}; + std::map headers; // names are stored in lower case + std::string body; // part of the content which arrived together with the header + + std::string header(std::string const &Name) const + { + auto const lookup{headers.find(Name)}; + return lookup != std::end(headers) ? lookup->second : std::string{}; + } +}; + +// parses provided response header. returns: true on success +bool parse_response(std::string const &Header, http_response &Response) +{ + std::istringstream lines{Header}; + std::string line; + if (false == static_cast(std::getline(lines, line))) + { + return false; + } + // status line, either regular 'HTTP/1.x 200 OK' or 'ICY 200 OK' sent by shoutcast servers + std::istringstream statusline{line}; + std::string protocol; + statusline >> protocol >> Response.status; + if (statusline.fail() || (false == starts_with_nocase(protocol, "http/") && lowercase(protocol) != "icy")) + { + return false; + } + while (std::getline(lines, line)) + { + auto const separator{line.find(':')}; + if (separator == std::string::npos) + { + continue; + } + Response.headers[lowercase(trim(line.substr(0, separator)))] = trim(line.substr(separator + 1)); + } + return true; +} + +// removes http chunked transfer framing from received data, if there's any +class content_decoder +{ + + public: + explicit content_decoder(bool const Chunked) : m_chunked{Chunked} {} + // appends payload of provided data to the output. returns: false if the data is malformed + bool feed(std::uint8_t const *Data, std::size_t const Size, std::vector &Output) + { + if (false == m_chunked) + { + Output.insert(std::end(Output), Data, Data + Size); + return true; + } + std::size_t index{0}; + while (index < Size) + { + switch (m_stage) + { + case stage::size: + { + auto const character{static_cast(Data[index++])}; + if (character == '\n') + { + // chunk size is a hex number, optionally followed by extensions we don't care about + char *numberend{nullptr}; + m_remaining = std::strtoul(m_line.c_str(), &numberend, 16); + if (numberend == m_line.c_str()) + { + return false; + } + m_line.clear(); + m_stage = (m_remaining > 0 ? stage::payload : stage::done); + } + else if (character != '\r') + { + m_line += character; + if (m_line.size() > 256) + { + return false; + } + } + break; + } + case stage::payload: + { + auto const count{std::min(m_remaining, Size - index)}; + Output.insert(std::end(Output), Data + index, Data + index + count); + index += count; + m_remaining -= count; + if (m_remaining == 0) + { + m_stage = stage::separator; + } + break; + } + case stage::separator: + { + // payload of each chunk is followed by a line break + if (Data[index++] == '\n') + { + m_stage = stage::size; + } + break; + } + case stage::done: + { + return true; + } + } + } + return true; + } + bool done() const + { + return m_stage == stage::done; + } + + private: + enum class stage + { + size, + payload, + separator, + done + }; + bool m_chunked; + stage m_stage{stage::size}; + std::string m_line; + unsigned long m_remaining{0}; +}; + +} // namespace + +// single connection attempt of the radio stream worker +class radio_session +{ + + public: + enum class result + { + stopped, // the owner lost interest in the stream + failed, // connection didn't work out or was lost, it's worth to try again + unsupported // there's nothing we can do with this stream + }; + radio_session(radio_stream &Owner, std::stop_token Stoptoken, std::uint32_t const Generation) : m_owner{Owner}, m_stoptoken{std::move(Stoptoken)}, m_generation{Generation}, m_resolver{m_io}, m_socket{m_io} {} + ~radio_session() + { + asio::error_code ignored; + m_socket.close(ignored); + } + // connects with specified stream and feeds the owner with decoded audio until the connection ends + result play(std::string Url); + // returns true if the session delivered any audio + bool delivered() const + { + return m_delivered; + } + + private: + // runs pending network operation until specified flag is set by its completion handler. returns: false if the operation was aborted + bool wait(bool const &Done, std::chrono::seconds const Timeout); + bool connect(stream_address const &Address); + bool send(std::string const &Data); + // reads some data from the socket. returns: number of received bytes, or 0 if the connection is done for + std::size_t receive(std::uint8_t *Data, std::size_t const Size); + bool receive_response(http_response &Response); + // retrieves the playlist and replaces provided address with the one found in it. returns: false if it wasn't possible + bool receive_playlist(http_response const &Response, std::string &Url, result &Failure); + result receive_audio(http_response const &Response); + result fail(std::string const &Reason, result const Result = result::failed); + + radio_stream &m_owner; + std::stop_token m_stoptoken; + std::uint32_t m_generation; + asio::io_context m_io; + asio::ip::tcp::resolver m_resolver; + asio::ip::tcp::socket m_socket; + std::string m_error; // description of the most recent network problem + bool m_delivered{false}; +}; + +bool radio_session::wait(bool const &Done, std::chrono::seconds const Timeout) +{ + auto const deadline{std::chrono::steady_clock::now() + Timeout}; + while (false == Done) + { + auto const timedout{std::chrono::steady_clock::now() >= deadline}; + if (timedout || m_stoptoken.stop_requested()) + { + m_error = (timedout ? "operation timed out" : ""); + // abort the operation, then let its completion handler run as it refers to data of our caller + asio::error_code ignored; + m_resolver.cancel(); + m_socket.close(ignored); + m_io.restart(); + m_io.run(); + return false; + } + if (m_io.stopped()) + { + m_io.restart(); + } + m_io.run_one_for(std::chrono::milliseconds{100}); + } + return true; +} + +bool radio_session::connect(stream_address const &Address) +{ + asio::error_code error; + asio::ip::tcp::resolver::results_type endpoints; + bool done{false}; + + m_resolver.async_resolve(Address.host, Address.port, [&](asio::error_code const &Error, asio::ip::tcp::resolver::results_type Results) { + error = Error; + endpoints = std::move(Results); + done = true; + }); + if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT)) + { + return false; + } + if (error) + { + m_error = "can't find the server (" + error.message() + ")"; + return false; + } + + done = false; + asio::async_connect(m_socket, endpoints, [&](asio::error_code const &Error, asio::ip::tcp::endpoint const &) { + error = Error; + done = true; + }); + if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT)) + { + return false; + } + if (error) + { + m_error = "can't connect with the server (" + error.message() + ")"; + return false; + } + return true; +} + +bool radio_session::send(std::string const &Data) +{ + asio::error_code error; + bool done{false}; + + asio::async_write(m_socket, asio::buffer(Data), [&](asio::error_code const &Error, std::size_t) { + error = Error; + done = true; + }); + if (false == wait(done, EU07_RADIOSTREAM_CONNECTTIMEOUT)) + { + return false; + } + if (error) + { + m_error = error.message(); + return false; + } + return true; +} + +std::size_t radio_session::receive(std::uint8_t *Data, std::size_t const Size) +{ + asio::error_code error; + std::size_t received{0}; + bool done{false}; + + m_socket.async_read_some(asio::buffer(Data, Size), [&](asio::error_code const &Error, std::size_t const Count) { + error = Error; + received = Count; + done = true; + }); + if (false == wait(done, EU07_RADIOSTREAM_STALLTIMEOUT)) + { + return 0; + } + if (error && received == 0) + { + m_error = (error == asio::error::eof ? "connection closed by the server" : error.message()); + } + return received; +} + +bool radio_session::receive_response(http_response &Response) +{ + std::string data; + std::array chunk; + while (true) + { + // some servers end lines with a bare line feed, so we look for both variants + auto headerend{data.find("\r\n\r\n")}; + auto headerendsize{std::size_t{4}}; + if (headerend == std::string::npos) + { + headerend = data.find("\n\n"); + headerendsize = 2; + } + if (headerend != std::string::npos) + { + Response = http_response{}; + if (false == parse_response(data.substr(0, headerend), Response)) + { + m_error = "unrecognized reply from the server"; + return false; + } + Response.body = data.substr(headerend + headerendsize); + return true; + } + if (data.size() > EU07_RADIOSTREAM_MAXHEADERSIZE) + { + m_error = "unrecognized reply from the server"; + return false; + } + auto const received{receive(chunk.data(), chunk.size())}; + if (received == 0) + { + return false; + } + data.append(reinterpret_cast(chunk.data()), received); + } +} + +radio_session::result radio_session::fail(std::string const &Reason, result const Result) +{ + if (m_stoptoken.stop_requested()) + { + return result::stopped; + } + if (false == Reason.empty()) + { + m_owner.post("radio stream: " + Reason, true); + } + return Result; +} + +radio_session::result radio_session::play(std::string Url) +{ + for (auto hop{0}; hop <= EU07_RADIOSTREAM_MAXREDIRECTS; ++hop) + { + stream_address address; + auto const addresserror{parse_address(Url, address)}; + if (false == addresserror.empty()) + { + return fail(addresserror + " \"" + Url + "\"", result::unsupported); + } + + m_owner.status(radio_stream::state::connecting, m_generation); + m_owner.post("radio stream: connecting with \"" + Url + "\""); + { + // each hop needs a fresh connection + asio::error_code ignored; + m_socket.close(ignored); + } + if (false == connect(address)) + { + return fail(m_error); + } + // NOTE: we don't ask for the in-band metadata, so the content is pure audio + auto const request{"GET " + address.target + " HTTP/1.1\r\nHost: " + address.authority() + "\r\nUser-Agent: MaSzyna\r\nAccept: */*\r\nConnection: close\r\n\r\n"}; + http_response response; + if (false == send(request) || false == receive_response(response)) + { + return fail(m_error); + } + + if (response.status >= 300 && response.status < 400) + { + auto const location{response.header("location")}; + if (location.empty()) + { + return fail("server sent a redirect without the target"); + } + Url = resolve_redirect(location, address); + continue; + } + if (response.status != 200) + { + return fail("server replied with status " + std::to_string(response.status)); + } + + auto const contenttype{lowercase(response.header("content-type"))}; + auto const path{lowercase(address.target.substr(0, address.target.find('?')))}; + auto const isplaylist{contenttype.find("mpegurl") != std::string::npos || contenttype.find("scpls") != std::string::npos || path.ends_with(".m3u") || path.ends_with(".pls")}; + if (isplaylist) + { + auto failure{result::failed}; + if (false == receive_playlist(response, Url, failure)) + { + return failure; + } + continue; + } + for (auto const *format : {"aac", "ogg", "opus", "flac"}) + { + if (contenttype.find(format) != std::string::npos) + { + return fail("unsupported audio format \"" + contenttype + "\", only mp3 streams can be played", result::unsupported); + } + } + + auto const name{response.header("icy-name")}; + auto const bitrate{response.header("icy-br")}; + m_owner.post("radio stream: connected" + (name.empty() ? "" : ", station \"" + name + "\"") + (bitrate.empty() ? "" : ", " + bitrate + " kbps")); + return receive_audio(response); + } + return fail("too many redirects"); +} + +bool radio_session::receive_playlist(http_response const &Response, std::string &Url, result &Failure) +{ + content_decoder content{lowercase(Response.header("transfer-encoding")).find("chunked") != std::string::npos}; + std::vector playlist; + if (false == content.feed(reinterpret_cast(Response.body.data()), Response.body.size(), playlist)) + { + Failure = fail("malformed reply from the server"); + return false; + } + std::array chunk; + while (false == content.done() && playlist.size() < EU07_RADIOSTREAM_MAXPLAYLISTSIZE) + { + // the playlist is complete when the server closes the connection + auto const received{receive(chunk.data(), chunk.size())}; + if (received == 0) + { + break; + } + if (false == content.feed(chunk.data(), received, playlist)) + { + Failure = fail("malformed reply from the server"); + return false; + } + } + if (m_stoptoken.stop_requested()) + { + Failure = result::stopped; + return false; + } + auto const address{parse_playlist(std::string{std::begin(playlist), std::end(playlist)})}; + if (address.empty()) + { + Failure = fail("playlist \"" + Url + "\" doesn't contain any stream address", result::unsupported); + return false; + } + Url = address; + return true; +} + +radio_session::result radio_session::receive_audio(http_response const &Response) +{ + content_decoder content{lowercase(Response.header("transfer-encoding")).find("chunked") != std::string::npos}; + std::vector input; + input.reserve(4 * EU07_RADIOSTREAM_DECODETHRESHOLD); + if (false == content.feed(reinterpret_cast(Response.body.data()), Response.body.size(), input)) + { + return fail("malformed reply from the server"); + } + + auto decoder{std::make_unique()}; + ::drmp3dec_init(decoder.get()); + std::vector frame(DRMP3_MAX_SAMPLES_PER_FRAME); + std::array chunk; + std::size_t junksize{0}; + auto threshold{EU07_RADIOSTREAM_DECODETHRESHOLD}; + + m_owner.status(radio_stream::state::receiving, m_generation); + + while (true) + { + // decode what we have... + std::size_t position{0}; + while (input.size() - position >= threshold) + { + drmp3dec_frame_info info{}; + auto const samplecount{::drmp3dec_decode_frame(decoder.get(), input.data() + position, static_cast(input.size() - position), frame.data(), &info)}; + if (samplecount > 0) + { + if (info.channels > 1) + { + // mix the channels down, the sound is going to be emitted from a single point in the cab + for (auto index{0}; index < samplecount; ++index) + { + auto sum{0}; + for (auto channel{0}; channel < info.channels; ++channel) + { + sum += frame[index * info.channels + channel]; + } + frame[index] = static_cast(sum / info.channels); + } + } + m_owner.store(frame.data(), static_cast(samplecount), static_cast(info.sample_rate), m_generation); + m_delivered = true; + junksize = 0; + threshold = std::clamp(static_cast(info.frame_bytes) * 3, EU07_RADIOSTREAM_MINDECODETHRESHOLD, EU07_RADIOSTREAM_DECODETHRESHOLD); + } + else if (info.frame_bytes > 0) + { + // data which isn't part of any frame. the decoder is looking for the stream again, which takes more data + junksize += static_cast(info.frame_bytes); + threshold = EU07_RADIOSTREAM_DECODETHRESHOLD; + } + else + { + // the decoder wants more data + break; + } + position += static_cast(info.frame_bytes); + } + input.erase(std::begin(input), std::begin(input) + static_cast(position)); + if (junksize > EU07_RADIOSTREAM_MAXJUNKSIZE || input.size() > EU07_RADIOSTREAM_MAXJUNKSIZE) + { + return fail(m_delivered ? "lost track of audio data in the stream" : "no mp3 audio found in the stream, only mp3 streams can be played", m_delivered ? result::failed : result::unsupported); + } + // ...then get some more + if (content.done()) + { + return fail("stream ended"); + } + auto const received{receive(chunk.data(), chunk.size())}; + if (received == 0) + { + return fail(m_error); + } + if (false == content.feed(chunk.data(), received, input)) + { + return fail("malformed data from the server"); + } + } +} + +radio_stream::~radio_stream() +{ + close(); + // NOTE: destructors of retired workers wait for their threads to finish +} + +// starts reception of specified stream, dropping current one if there's any +void radio_stream::open(std::string const &Url) +{ + close(); + if (Url.empty()) + { + return; + } + m_url = Url; + std::uint32_t generation; + { + std::scoped_lock lock{m_mutex}; + generation = m_generation; + m_state = state::connecting; + } + m_worker.finished = std::make_shared>(false); + m_worker.thread = std::jthread{[this](std::stop_token Stoptoken, std::string Address, std::uint32_t const Workergeneration, std::shared_ptr> Finished) { run(std::move(Stoptoken), std::move(Address), Workergeneration, std::move(Finished)); }, + Url, generation, m_worker.finished}; +} + +// terminates reception of the current stream +void radio_stream::close() +{ + { + std::scoped_lock lock{m_mutex}; + // from now on the current worker can't deliver anything + ++m_generation; + m_samplesstart = 0; + m_samplescount = 0; + m_state = state::idle; + m_rate = 0; + } + m_url.clear(); + if (m_worker.thread.joinable()) + { + // the worker may need a moment to shut down its connection. we don't wait for it here, as we're likely called from the main thread + m_worker.thread.request_stop(); + m_retired.emplace_back(std::move(m_worker)); + m_worker = worker{}; + } + reap(); +} + +// disposes of retired workers which are done with their cleanup +void radio_stream::reap() +{ + std::erase_if(m_retired, [](worker const &Worker) { return Worker.finished->load(); }); +} + +// returns number of decoded samples waiting to be collected +std::size_t radio_stream::available() const +{ + std::scoped_lock lock{m_mutex}; + return m_samplescount; +} + +// moves up to specified number of decoded samples to provided buffer. returns: number of samples moved +std::size_t radio_stream::read(std::int16_t *Output, std::size_t const Count) +{ + std::scoped_lock lock{m_mutex}; + auto const count{std::min(Count, m_samplescount)}; + for (std::size_t index{0}; index < count; ++index) + { + Output[index] = m_samples[(m_samplesstart + index) % m_samples.size()]; + } + if (count > 0) + { + m_samplesstart = (m_samplesstart + count) % m_samples.size(); + m_samplescount -= count; + } + return count; +} + +// retrieves next diagnostic message posted by the worker. returns: false if there's no message waiting +bool radio_stream::fetch_message(message &Message) +{ + std::scoped_lock lock{m_messagesmutex}; + if (m_messages.empty()) + { + return false; + } + Message = std::move(m_messages.front()); + m_messages.pop_front(); + return true; +} + +void radio_stream::run(std::stop_token Stoptoken, std::string Url, std::uint32_t const Generation, std::shared_ptr> Finished) +{ + auto failures{0}; + while (false == Stoptoken.stop_requested()) + { + auto result{radio_session::result::failed}; + auto delivered{false}; + { + radio_session session{*this, Stoptoken, Generation}; + result = session.play(Url); + delivered = session.delivered(); + } + if (result == radio_session::result::stopped || Stoptoken.stop_requested()) + { + break; + } + if (result == radio_session::result::unsupported) + { + status(state::failed, Generation); + break; + } + // try again after a pause, which gets longer if the server keeps refusing to cooperate + failures = (delivered ? 0 : failures + 1); + auto const pause{std::chrono::seconds{std::min(15, 1 << std::min(failures, 4))}}; + status(state::waiting, Generation); + post("radio stream: next connection attempt in " + std::to_string(pause.count()) + " s"); + auto const deadline{std::chrono::steady_clock::now() + pause}; + while (false == Stoptoken.stop_requested() && std::chrono::steady_clock::now() < deadline) + { + std::this_thread::sleep_for(std::chrono::milliseconds{50}); + } + } + Finished->store(true); +} + +// stores decoded samples, discarding oldest data if there's no room left. data from outdated workers is ignored +void radio_stream::store(std::int16_t const *Samples, std::size_t const Count, unsigned int const Rate, std::uint32_t const Generation) +{ + std::scoped_lock lock{m_mutex}; + if (Generation != m_generation || Rate == 0) + { + return; + } + if (Rate != m_rate) + { + // (re)initialize the buffer for the new format + m_samples.assign(static_cast(Rate) * EU07_RADIOSTREAM_BUFFERLENGTH, 0); + m_samplesstart = 0; + m_samplescount = 0; + m_rate = Rate; + } + auto const capacity{m_samples.size()}; + for (std::size_t index{0}; index < Count; ++index) + { + m_samples[(m_samplesstart + m_samplescount) % capacity] = Samples[index]; + if (m_samplescount < capacity) + { + ++m_samplescount; + } + else + { + // the buffer is full, the oldest sample was just overwritten + m_samplesstart = (m_samplesstart + 1) % capacity; + } + } +} + +void radio_stream::status(state const State, std::uint32_t const Generation) +{ + std::scoped_lock lock{m_mutex}; + if (Generation == m_generation) + { + m_state = State; + } +} + +void radio_stream::post(std::string Text, bool const Error) +{ + std::scoped_lock lock{m_messagesmutex}; + m_messages.emplace_back(std::move(Text), Error); + // if nobody collects the messages there's no point in hoarding them + while (m_messages.size() > 64) + { + m_messages.pop_front(); + } +} + +} // namespace audio diff --git a/audio/radiostream.h b/audio/radiostream.h new file mode 100644 index 00000000..5088f1fc --- /dev/null +++ b/audio/radiostream.h @@ -0,0 +1,108 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace audio +{ + +// receiver of an internet radio stream (plain http, mp3 payload). +// owns a worker thread which maintains the connection, decodes received data +// and stores the result as 16-bit mono samples for the owner to collect. +// the worker doesn't touch anything outside of the object, and the owner is never made to wait for the network +class radio_stream +{ + + friend class radio_session; + + public: + // types + enum class state + { + idle, // no stream requested + connecting, // establishing connection with the server + receiving, // audio data is coming in + waiting, // connection was lost or refused, waiting to make another attempt + failed // stream can't be played, no further attempts will be made + }; + using message = std::pair; // text, is error + // constructors + radio_stream() = default; + radio_stream(radio_stream const &) = delete; + radio_stream &operator=(radio_stream const &) = delete; + // destructor + ~radio_stream(); + // methods + // starts reception of specified stream, dropping current one if there's any + void open(std::string const &Url); + // terminates reception of the current stream + void close(); + // returns address of currently requested stream, or empty string + std::string const &url() const + { + return m_url; + } + state status() const + { + return m_state.load(); + } + // returns number of decoded samples waiting to be collected + std::size_t available() const; + // returns sample rate of decoded audio, or 0 if nothing was decoded yet + unsigned int rate() const + { + return m_rate.load(); + } + // moves up to specified number of decoded samples to provided buffer. returns: number of samples moved + std::size_t read(std::int16_t *Output, std::size_t const Count); + // retrieves next diagnostic message posted by the worker. returns: false if there's no message waiting + bool fetch_message(message &Message); + + private: + // types + struct worker + { + std::jthread thread; + std::shared_ptr> finished; + }; + // methods + void run(std::stop_token Stoptoken, std::string Url, std::uint32_t const Generation, std::shared_ptr> Finished); + // stores decoded samples, discarding oldest data if there's no room left. data from outdated workers is ignored + void store(std::int16_t const *Samples, std::size_t const Count, unsigned int const Rate, std::uint32_t const Generation); + void status(state const State, std::uint32_t const Generation); + void post(std::string Text, bool const Error = false); + // disposes of retired workers which are done with their cleanup + void reap(); + // members + std::string m_url; + worker m_worker; + std::vector m_retired; // workers which were told to stop but may still be busy closing their connection + std::atomic m_state{state::idle}; + std::atomic m_rate{0}; + mutable std::mutex m_mutex; // guards the sample buffer and the generation counter + std::uint32_t m_generation{0}; // identifies the worker allowed to deliver data + std::vector m_samples; // ring buffer + std::size_t m_samplesstart{0}; + std::size_t m_samplescount{0}; + std::mutex m_messagesmutex; + std::deque m_messages; +}; + +} // namespace audio diff --git a/audio/sound.cpp b/audio/sound.cpp index 785b976f..0cd17e0f 100644 --- a/audio/sound.cpp +++ b/audio/sound.cpp @@ -914,6 +914,18 @@ sound_source::location() const { + m_owner->VectorFront() * (double)m_offset.z }; } +// returns location-based gain factor of the emitter for the current position of the listener +float +sound_source::soundproofing_factor() { + + // NOTE: the calculation needs the listener to be assigned to a vehicle or in one of the outside views. + // this may not be the case yet when we're called from vehicle update, as the global camera data is refreshed later in the frame + if( FreeFlyModeFlag || Global.CabWindowOpen || Global.pCamera.m_owner != nullptr ) { + update_soundproofing(); + } + return m_properties.soundproofing; +} + void sound_source::range( float const Range ) { diff --git a/audio/sound.h b/audio/sound.h index e9957be0..5948115f 100644 --- a/audio/sound.h +++ b/audio/sound.h @@ -139,6 +139,9 @@ public: // returns location of the sound source in simulation region space glm::dvec3 location() const; + // returns location-based gain factor of the emitter for the current position of the listener + float + soundproofing_factor(); // returns defined range of the sound void range( float const Range ); diff --git a/input/command.cpp b/input/command.cpp index d7966f37..4cb9153a 100644 --- a/input/command.cpp +++ b/input/command.cpp @@ -405,6 +405,13 @@ commanddescription_sequence Commands_descriptions = { {"wiperswitchincrease", command_target::vehicle, command_mode::oneoff}, {"wiperswitchdecrease", command_target::vehicle, command_mode::oneoff}, {"lightsset", command_target::vehicle, command_mode::oneoff}, + {"fmradiotoggle", command_target::vehicle, command_mode::oneoff}, + {"fmradiovolumeincrease", command_target::vehicle, command_mode::oneoff}, + {"fmradiovolumedecrease", command_target::vehicle, command_mode::oneoff}, + {"fmradiovolumeset", command_target::vehicle, command_mode::oneoff}, + {"fmradiotuneincrease", command_target::vehicle, command_mode::oneoff}, + {"fmradiotunedecrease", command_target::vehicle, command_mode::oneoff}, + {"fmradiotuneset", command_target::vehicle, command_mode::oneoff}, }; // Maps of command and coresponding strings @@ -789,6 +796,13 @@ std::unordered_map const commandMap = { {"wiperswitchincrease", user_command::wiperswitchincrease}, {"wiperswitchdecrease", user_command::wiperswitchdecrease}, {"lightsset", user_command::lightsset}, + {"fmradiotoggle", user_command::fmradiotoggle}, + {"fmradiovolumeincrease", user_command::fmradiovolumeincrease}, + {"fmradiovolumedecrease", user_command::fmradiovolumedecrease}, + {"fmradiovolumeset", user_command::fmradiovolumeset}, + {"fmradiotuneincrease", user_command::fmradiotuneincrease}, + {"fmradiotunedecrease", user_command::fmradiotunedecrease}, + {"fmradiotuneset", user_command::fmradiotuneset}, {"none", user_command::none}}; } // namespace simulation diff --git a/input/command.h b/input/command.h index e03622a9..e9c25595 100644 --- a/input/command.h +++ b/input/command.h @@ -403,6 +403,13 @@ enum class user_command wiperswitchincrease, wiperswitchdecrease, lightsset, + fmradiotoggle, + fmradiovolumeincrease, + fmradiovolumedecrease, + fmradiovolumeset, + fmradiotuneincrease, + fmradiotunedecrease, + fmradiotuneset, none = -1 }; diff --git a/input/drivermouseinput.cpp b/input/drivermouseinput.cpp index ab3d0aaf..e4c007e5 100644 --- a/input/drivermouseinput.cpp +++ b/input/drivermouseinput.cpp @@ -816,6 +816,15 @@ drivermouse_input::default_bindings() { { "radiovolumenext_sw:",{ radiovolumeincrease, none } }, + { "fmradio_sw:",{ + fmradiotoggle, + none } }, + { "fmradiovolume_sw:",{ + fmradiovolumeincrease, + fmradiovolumedecrease } }, + { "fmradiotune_sw:",{ + fmradiotuneincrease, + fmradiotunedecrease } }, { "pantfront_sw:", { pantographtogglefront, none } }, diff --git a/ref/dr_libs/dr_mp3.h b/ref/dr_libs/dr_mp3.h new file mode 100644 index 00000000..a631ae40 --- /dev/null +++ b/ref/dr_libs/dr_mp3.h @@ -0,0 +1,5430 @@ +/* +MP3 audio decoder. Choice of public domain or MIT-0. See license statements at the end of this file. +dr_mp3 - v0.7.4 - TBD + +David Reid - mackron@gmail.com + +GitHub: https://github.com/mackron/dr_libs + +Based on minimp3 (https://github.com/lieff/minimp3) which is where the real work was done. See the bottom of this file for differences between minimp3 and dr_mp3. +*/ + +/* +Introduction +============= +dr_mp3 is a single file library. To use it, do something like the following in one .c file. + + ```c + #define DR_MP3_IMPLEMENTATION + #include "dr_mp3.h" + ``` + +You can then #include this file in other parts of the program as you would with any other header file. To decode audio data, do something like the following: + + ```c + drmp3 mp3; + if (!drmp3_init_file(&mp3, "MySong.mp3", NULL)) { + // Failed to open file + } + + ... + + drmp3_uint64 framesRead = drmp3_read_pcm_frames_f32(pMP3, framesToRead, pFrames); + ``` + +The drmp3 object is transparent so you can get access to the channel count and sample rate like so: + + ``` + drmp3_uint32 channels = mp3.channels; + drmp3_uint32 sampleRate = mp3.sampleRate; + ``` + +The example above initializes a decoder from a file, but you can also initialize it from a block of memory and read and seek callbacks with +`drmp3_init_memory()` and `drmp3_init()` respectively. + +You do not need to do any annoying memory management when reading PCM frames - this is all managed internally. You can request any number of PCM frames in each +call to `drmp3_read_pcm_frames_f32()` and it will return as many PCM frames as it can, up to the requested amount. + +You can also decode an entire file in one go with `drmp3_open_and_read_pcm_frames_f32()`, `drmp3_open_memory_and_read_pcm_frames_f32()` and +`drmp3_open_file_and_read_pcm_frames_f32()`. + + +Build Options +============= +#define these options before including this file. + +#define DR_MP3_NO_STDIO + Disable drmp3_init_file(), etc. + +#define DR_MP3_NO_SIMD + Disable SIMD optimizations. +*/ + +#ifndef dr_mp3_h +#define dr_mp3_h + +#ifdef __cplusplus +extern "C" { +#endif + +#define DRMP3_STRINGIFY(x) #x +#define DRMP3_XSTRINGIFY(x) DRMP3_STRINGIFY(x) + +#define DRMP3_VERSION_MAJOR 0 +#define DRMP3_VERSION_MINOR 7 +#define DRMP3_VERSION_REVISION 4 +#define DRMP3_VERSION_STRING DRMP3_XSTRINGIFY(DRMP3_VERSION_MAJOR) "." DRMP3_XSTRINGIFY(DRMP3_VERSION_MINOR) "." DRMP3_XSTRINGIFY(DRMP3_VERSION_REVISION) + +#include /* For size_t. */ + +/* Sized Types */ +typedef signed char drmp3_int8; +typedef unsigned char drmp3_uint8; +typedef signed short drmp3_int16; +typedef unsigned short drmp3_uint16; +typedef signed int drmp3_int32; +typedef unsigned int drmp3_uint32; +#if defined(_MSC_VER) && !defined(__clang__) + typedef signed __int64 drmp3_int64; + typedef unsigned __int64 drmp3_uint64; +#else + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wlong-long" + #if defined(__clang__) + #pragma GCC diagnostic ignored "-Wc++11-long-long" + #endif + #endif + typedef signed long long drmp3_int64; + typedef unsigned long long drmp3_uint64; + #if defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))) + #pragma GCC diagnostic pop + #endif +#endif +#if defined(__LP64__) || defined(_WIN64) || (defined(__x86_64__) && !defined(__ILP32__)) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) || defined(__powerpc64__) + typedef drmp3_uint64 drmp3_uintptr; +#else + typedef drmp3_uint32 drmp3_uintptr; +#endif +typedef drmp3_uint8 drmp3_bool8; +typedef drmp3_uint32 drmp3_bool32; +#define DRMP3_TRUE 1 +#define DRMP3_FALSE 0 + +/* Weird shifting syntax is for VC6 compatibility. */ +#define DRMP3_UINT64_MAX (((drmp3_uint64)0xFFFFFFFF << 32) | (drmp3_uint64)0xFFFFFFFF) +/* End Sized Types */ + +/* Decorations */ +#if !defined(DRMP3_API) + #if defined(DRMP3_DLL) + #if defined(_WIN32) + #define DRMP3_DLL_IMPORT __declspec(dllimport) + #define DRMP3_DLL_EXPORT __declspec(dllexport) + #define DRMP3_DLL_PRIVATE static + #else + #if defined(__GNUC__) && __GNUC__ >= 4 + #define DRMP3_DLL_IMPORT __attribute__((visibility("default"))) + #define DRMP3_DLL_EXPORT __attribute__((visibility("default"))) + #define DRMP3_DLL_PRIVATE __attribute__((visibility("hidden"))) + #else + #define DRMP3_DLL_IMPORT + #define DRMP3_DLL_EXPORT + #define DRMP3_DLL_PRIVATE static + #endif + #endif + + #if defined(DR_MP3_IMPLEMENTATION) + #define DRMP3_API DRMP3_DLL_EXPORT + #else + #define DRMP3_API DRMP3_DLL_IMPORT + #endif + #define DRMP3_PRIVATE DRMP3_DLL_PRIVATE + #else + #define DRMP3_API extern + #define DRMP3_PRIVATE static + #endif +#endif +/* End Decorations */ + +/* Result Codes */ +typedef drmp3_int32 drmp3_result; +#define DRMP3_SUCCESS 0 +#define DRMP3_ERROR -1 /* A generic error. */ +#define DRMP3_INVALID_ARGS -2 +#define DRMP3_INVALID_OPERATION -3 +#define DRMP3_OUT_OF_MEMORY -4 +#define DRMP3_OUT_OF_RANGE -5 +#define DRMP3_ACCESS_DENIED -6 +#define DRMP3_DOES_NOT_EXIST -7 +#define DRMP3_ALREADY_EXISTS -8 +#define DRMP3_TOO_MANY_OPEN_FILES -9 +#define DRMP3_INVALID_FILE -10 +#define DRMP3_TOO_BIG -11 +#define DRMP3_PATH_TOO_LONG -12 +#define DRMP3_NAME_TOO_LONG -13 +#define DRMP3_NOT_DIRECTORY -14 +#define DRMP3_IS_DIRECTORY -15 +#define DRMP3_DIRECTORY_NOT_EMPTY -16 +#define DRMP3_END_OF_FILE -17 +#define DRMP3_NO_SPACE -18 +#define DRMP3_BUSY -19 +#define DRMP3_IO_ERROR -20 +#define DRMP3_INTERRUPT -21 +#define DRMP3_UNAVAILABLE -22 +#define DRMP3_ALREADY_IN_USE -23 +#define DRMP3_BAD_ADDRESS -24 +#define DRMP3_BAD_SEEK -25 +#define DRMP3_BAD_PIPE -26 +#define DRMP3_DEADLOCK -27 +#define DRMP3_TOO_MANY_LINKS -28 +#define DRMP3_NOT_IMPLEMENTED -29 +#define DRMP3_NO_MESSAGE -30 +#define DRMP3_BAD_MESSAGE -31 +#define DRMP3_NO_DATA_AVAILABLE -32 +#define DRMP3_INVALID_DATA -33 +#define DRMP3_TIMEOUT -34 +#define DRMP3_NO_NETWORK -35 +#define DRMP3_NOT_UNIQUE -36 +#define DRMP3_NOT_SOCKET -37 +#define DRMP3_NO_ADDRESS -38 +#define DRMP3_BAD_PROTOCOL -39 +#define DRMP3_PROTOCOL_UNAVAILABLE -40 +#define DRMP3_PROTOCOL_NOT_SUPPORTED -41 +#define DRMP3_PROTOCOL_FAMILY_NOT_SUPPORTED -42 +#define DRMP3_ADDRESS_FAMILY_NOT_SUPPORTED -43 +#define DRMP3_SOCKET_NOT_SUPPORTED -44 +#define DRMP3_CONNECTION_RESET -45 +#define DRMP3_ALREADY_CONNECTED -46 +#define DRMP3_NOT_CONNECTED -47 +#define DRMP3_CONNECTION_REFUSED -48 +#define DRMP3_NO_HOST -49 +#define DRMP3_IN_PROGRESS -50 +#define DRMP3_CANCELLED -51 +#define DRMP3_MEMORY_ALREADY_MAPPED -52 +#define DRMP3_AT_END -53 +/* End Result Codes */ + +#define DRMP3_MAX_PCM_FRAMES_PER_MP3_FRAME 1152 +#define DRMP3_MAX_SAMPLES_PER_FRAME (DRMP3_MAX_PCM_FRAMES_PER_MP3_FRAME*2) + +/* Inline */ +#ifdef _MSC_VER + #define DRMP3_INLINE __forceinline +#elif defined(__GNUC__) + /* + I've had a bug report where GCC is emitting warnings about functions possibly not being inlineable. This warning happens when + the __attribute__((always_inline)) attribute is defined without an "inline" statement. I think therefore there must be some + case where "__inline__" is not always defined, thus the compiler emitting these warnings. When using -std=c89 or -ansi on the + command line, we cannot use the "inline" keyword and instead need to use "__inline__". In an attempt to work around this issue + I am using "__inline__" only when we're compiling in strict ANSI mode. + */ + #if defined(__STRICT_ANSI__) + #define DRMP3_GNUC_INLINE_HINT __inline__ + #else + #define DRMP3_GNUC_INLINE_HINT inline + #endif + + #if (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 2)) || defined(__clang__) + #define DRMP3_INLINE DRMP3_GNUC_INLINE_HINT __attribute__((always_inline)) + #else + #define DRMP3_INLINE DRMP3_GNUC_INLINE_HINT + #endif +#elif defined(__WATCOMC__) + #define DRMP3_INLINE __inline +#else + #define DRMP3_INLINE +#endif +/* End Inline */ + + +DRMP3_API void drmp3_version(drmp3_uint32* pMajor, drmp3_uint32* pMinor, drmp3_uint32* pRevision); +DRMP3_API const char* drmp3_version_string(void); + + +/* Allocation Callbacks */ +typedef struct +{ + void* pUserData; + void* (* onMalloc)(size_t sz, void* pUserData); + void* (* onRealloc)(void* p, size_t sz, void* pUserData); + void (* onFree)(void* p, void* pUserData); +} drmp3_allocation_callbacks; +/* End Allocation Callbacks */ + + +/* +Low Level Push API +================== +*/ +#define DRMP3_MAX_BITRESERVOIR_BYTES 511 +#define DRMP3_MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */ +#define DRMP3_MAX_L3_FRAME_PAYLOAD_BYTES DRMP3_MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */ + +typedef struct +{ + int frame_bytes, channels, sample_rate, layer, bitrate_kbps; +} drmp3dec_frame_info; + +typedef struct +{ + const drmp3_uint8 *buf; + int pos, limit; +} drmp3_bs; + +typedef struct +{ + const drmp3_uint8 *sfbtab; + drmp3_uint16 part_23_length, big_values, scalefac_compress; + drmp3_uint8 global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb; + drmp3_uint8 table_select[3], region_count[3], subblock_gain[3]; + drmp3_uint8 preflag, scalefac_scale, count1_table, scfsi; +} drmp3_L3_gr_info; + +typedef struct +{ + drmp3_bs bs; + drmp3_uint8 maindata[DRMP3_MAX_BITRESERVOIR_BYTES + DRMP3_MAX_L3_FRAME_PAYLOAD_BYTES]; + drmp3_L3_gr_info gr_info[4]; + float grbuf[2][576], scf[40], syn[18 + 15][2*32]; + drmp3_uint8 ist_pos[2][39]; +} drmp3dec_scratch; + +typedef struct +{ + float mdct_overlap[2][9*32], qmf_state[15*2*32]; + int reserv, free_format_bytes; + drmp3_uint8 header[4], reserv_buf[511]; + drmp3dec_scratch scratch; +} drmp3dec; + +/* Initializes a low level decoder. */ +DRMP3_API void drmp3dec_init(drmp3dec *dec); + +/* Reads a frame from a low level decoder. */ +DRMP3_API int drmp3dec_decode_frame(drmp3dec *dec, const drmp3_uint8 *mp3, int mp3_bytes, void *pcm, drmp3dec_frame_info *info); + +/* Helper for converting between f32 and s16. */ +DRMP3_API void drmp3dec_f32_to_s16(const float *in, drmp3_int16 *out, size_t num_samples); + + + +/* +Main API (Pull API) +=================== +*/ +typedef enum +{ + DRMP3_SEEK_SET, + DRMP3_SEEK_CUR, + DRMP3_SEEK_END +} drmp3_seek_origin; + +typedef struct +{ + drmp3_uint64 seekPosInBytes; /* Points to the first byte of an MP3 frame. */ + drmp3_uint64 pcmFrameIndex; /* The index of the PCM frame this seek point targets. */ + drmp3_uint16 mp3FramesToDiscard; /* The number of whole MP3 frames to be discarded before pcmFramesToDiscard. */ + drmp3_uint16 pcmFramesToDiscard; /* The number of leading samples to read and discard. These are discarded after mp3FramesToDiscard. */ +} drmp3_seek_point; + +typedef enum +{ + DRMP3_METADATA_TYPE_ID3V1, + DRMP3_METADATA_TYPE_ID3V2, + DRMP3_METADATA_TYPE_APE, + DRMP3_METADATA_TYPE_XING, + DRMP3_METADATA_TYPE_VBRI +} drmp3_metadata_type; + +typedef struct +{ + drmp3_metadata_type type; + const void* pRawData; /* A pointer to the raw data. */ + size_t rawDataSize; +} drmp3_metadata; + + +/* +Callback for when data is read. Return value is the number of bytes actually read. + +pUserData [in] The user data that was passed to drmp3_init(), and family. +pBufferOut [out] The output buffer. +bytesToRead [in] The number of bytes to read. + +Returns the number of bytes actually read. + +A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback until +either the entire bytesToRead is filled or you have reached the end of the stream. +*/ +typedef size_t (* drmp3_read_proc)(void* pUserData, void* pBufferOut, size_t bytesToRead); + +/* +Callback for when data needs to be seeked. + +pUserData [in] The user data that was passed to drmp3_init(), and family. +offset [in] The number of bytes to move, relative to the origin. Can be negative. +origin [in] The origin of the seek. + +Returns whether or not the seek was successful. +*/ +typedef drmp3_bool32 (* drmp3_seek_proc)(void* pUserData, int offset, drmp3_seek_origin origin); + +/* +Callback for retrieving the current cursor position. + +pUserData [in] The user data that was passed to drmp3_init(), and family. +pCursor [out] The cursor position in bytes from the start of the stream. + +Returns whether or not the cursor position was successfully retrieved. +*/ +typedef drmp3_bool32 (* drmp3_tell_proc)(void* pUserData, drmp3_int64* pCursor); + + +/* +Callback for when metadata is read. + +Only the raw data is provided. The client is responsible for parsing the contents of the data themsevles. +*/ +typedef void (* drmp3_meta_proc)(void* pUserData, const drmp3_metadata* pMetadata); + + +typedef struct +{ + drmp3_uint32 channels; + drmp3_uint32 sampleRate; +} drmp3_config; + +typedef struct +{ + drmp3dec decoder; + drmp3_uint32 channels; + drmp3_uint32 sampleRate; + drmp3_read_proc onRead; + drmp3_seek_proc onSeek; + drmp3_meta_proc onMeta; + void* pUserData; + void* pUserDataMeta; + drmp3_allocation_callbacks allocationCallbacks; + drmp3_uint32 mp3FrameChannels; /* The number of channels in the currently loaded MP3 frame. Internal use only. */ + drmp3_uint32 mp3FrameSampleRate; /* The sample rate of the currently loaded MP3 frame. Internal use only. */ + drmp3_uint32 pcmFramesConsumedInMP3Frame; + drmp3_uint32 pcmFramesRemainingInMP3Frame; + drmp3_uint8 pcmFrames[sizeof(float)*DRMP3_MAX_SAMPLES_PER_FRAME]; /* <-- Multipled by sizeof(float) to ensure there's enough room for DR_MP3_FLOAT_OUTPUT. */ + drmp3_uint64 currentPCMFrame; /* The current PCM frame, globally. */ + drmp3_uint64 streamCursor; /* The current byte the decoder is sitting on in the raw stream. */ + drmp3_uint64 streamLength; /* The length of the stream in bytes. dr_mp3 will not read beyond this. If a ID3v1 or APE tag is present, this will be set to the first byte of the tag. */ + drmp3_uint64 streamStartOffset; /* The offset of the start of the MP3 data. This is used for skipping ID3v2 and VBR tags. */ + drmp3_seek_point* pSeekPoints; /* NULL by default. Set with drmp3_bind_seek_table(). Memory is owned by the client. dr_mp3 will never attempt to free this pointer. */ + drmp3_uint32 seekPointCount; /* The number of items in pSeekPoints. When set to 0 assumes to no seek table. Defaults to zero. */ + drmp3_uint32 delayInPCMFrames; + drmp3_uint32 paddingInPCMFrames; + drmp3_uint64 totalPCMFrameCount; /* Set to DRMP3_UINT64_MAX if the length is unknown. Includes delay and padding. */ + drmp3_bool32 isVBR; + drmp3_bool32 isCBR; + size_t dataSize; + size_t dataCapacity; + size_t dataConsumed; + drmp3_uint8* pData; + drmp3_bool32 atEnd; + struct + { + const drmp3_uint8* pData; + size_t dataSize; + size_t currentReadPos; + } memory; /* Only used for decoders that were opened against a block of memory. */ +} drmp3; + +/* +Initializes an MP3 decoder. + +onRead [in] The function to call when data needs to be read from the client. +onSeek [in] The function to call when the read position of the client data needs to move. +onTell [in] The function to call when the read position of the client data needs to be retrieved. +pUserData [in, optional] A pointer to application defined data that will be passed to onRead and onSeek. + +Returns true if successful; false otherwise. + +Close the loader with drmp3_uninit(). + +See also: drmp3_init_file(), drmp3_init_memory(), drmp3_uninit() +*/ +DRMP3_API drmp3_bool32 drmp3_init(drmp3* pMP3, drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, drmp3_meta_proc onMeta, void* pUserData, const drmp3_allocation_callbacks* pAllocationCallbacks); + +/* +Initializes an MP3 decoder from a block of memory. + +This does not create a copy of the data. It is up to the application to ensure the buffer remains valid for +the lifetime of the drmp3 object. + +The buffer should contain the contents of the entire MP3 file. +*/ +DRMP3_API drmp3_bool32 drmp3_init_memory_with_metadata(drmp3* pMP3, const void* pData, size_t dataSize, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_bool32 drmp3_init_memory(drmp3* pMP3, const void* pData, size_t dataSize, const drmp3_allocation_callbacks* pAllocationCallbacks); + +#ifndef DR_MP3_NO_STDIO +/* +Initializes an MP3 decoder from a file. + +This holds the internal FILE object until drmp3_uninit() is called. Keep this in mind if you're caching drmp3 +objects because the operating system may restrict the number of file handles an application can have open at +any given time. +*/ +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata(drmp3* pMP3, const char* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata_w(drmp3* pMP3, const wchar_t* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks); + +DRMP3_API drmp3_bool32 drmp3_init_file(drmp3* pMP3, const char* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_bool32 drmp3_init_file_w(drmp3* pMP3, const wchar_t* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks); +#endif + +/* +Uninitializes an MP3 decoder. +*/ +DRMP3_API void drmp3_uninit(drmp3* pMP3); + +/* +Reads PCM frames as interleaved 32-bit IEEE floating point PCM. + +Note that framesToRead specifies the number of PCM frames to read, _not_ the number of MP3 frames. +*/ +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut); + +/* +Reads PCM frames as interleaved signed 16-bit integer PCM. + +Note that framesToRead specifies the number of PCM frames to read, _not_ the number of MP3 frames. +*/ +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_s16(drmp3* pMP3, drmp3_uint64 framesToRead, drmp3_int16* pBufferOut); + +/* +Seeks to a specific frame. + +Note that this is _not_ an MP3 frame, but rather a PCM frame. +*/ +DRMP3_API drmp3_bool32 drmp3_seek_to_pcm_frame(drmp3* pMP3, drmp3_uint64 frameIndex); + +/* +Calculates the total number of PCM frames in the MP3 stream. Cannot be used for infinite streams such as internet +radio. Runs in linear time. Returns 0 on error. +*/ +DRMP3_API drmp3_uint64 drmp3_get_pcm_frame_count(drmp3* pMP3); + +/* +Calculates the total number of MP3 frames in the MP3 stream. Cannot be used for infinite streams such as internet +radio. Runs in linear time. Returns 0 on error. +*/ +DRMP3_API drmp3_uint64 drmp3_get_mp3_frame_count(drmp3* pMP3); + +/* +Calculates the total number of MP3 and PCM frames in the MP3 stream. Cannot be used for infinite streams such as internet +radio. Runs in linear time. Returns 0 on error. + +This is equivalent to calling drmp3_get_mp3_frame_count() and drmp3_get_pcm_frame_count() except that it's more efficient. +*/ +DRMP3_API drmp3_bool32 drmp3_get_mp3_and_pcm_frame_count(drmp3* pMP3, drmp3_uint64* pMP3FrameCount, drmp3_uint64* pPCMFrameCount); + +/* +Calculates the seekpoints based on PCM frames. This is slow. + +pSeekpoint count is a pointer to a uint32 containing the seekpoint count. On input it contains the desired count. +On output it contains the actual count. The reason for this design is that the client may request too many +seekpoints, in which case dr_mp3 will return a corrected count. + +Note that seektable seeking is not quite sample exact when the MP3 stream contains inconsistent sample rates. +*/ +DRMP3_API drmp3_bool32 drmp3_calculate_seek_points(drmp3* pMP3, drmp3_uint32* pSeekPointCount, drmp3_seek_point* pSeekPoints); + +/* +Binds a seek table to the decoder. + +This does _not_ make a copy of pSeekPoints - it only references it. It is up to the application to ensure this +remains valid while it is bound to the decoder. + +Use drmp3_calculate_seek_points() to calculate the seek points. +*/ +DRMP3_API drmp3_bool32 drmp3_bind_seek_table(drmp3* pMP3, drmp3_uint32 seekPointCount, drmp3_seek_point* pSeekPoints); + + +/* +Opens an decodes an entire MP3 stream as a single operation. + +On output pConfig will receive the channel count and sample rate of the stream. + +Free the returned pointer with drmp3_free(). +*/ +DRMP3_API float* drmp3_open_and_read_pcm_frames_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_int16* drmp3_open_and_read_pcm_frames_s16(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); + +DRMP3_API float* drmp3_open_memory_and_read_pcm_frames_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_int16* drmp3_open_memory_and_read_pcm_frames_s16(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); + +#ifndef DR_MP3_NO_STDIO +DRMP3_API float* drmp3_open_file_and_read_pcm_frames_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +DRMP3_API drmp3_int16* drmp3_open_file_and_read_pcm_frames_s16(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks); +#endif + +/* +Allocates a block of memory on the heap. +*/ +DRMP3_API void* drmp3_malloc(size_t sz, const drmp3_allocation_callbacks* pAllocationCallbacks); + +/* +Frees any memory that was allocated by a public drmp3 API. +*/ +DRMP3_API void drmp3_free(void* p, const drmp3_allocation_callbacks* pAllocationCallbacks); + +#ifdef __cplusplus +} +#endif +#endif /* dr_mp3_h */ + + +/************************************************************************************************************************************************************ + ************************************************************************************************************************************************************ + + IMPLEMENTATION + + ************************************************************************************************************************************************************ + ************************************************************************************************************************************************************/ +#if defined(DR_MP3_IMPLEMENTATION) +#ifndef dr_mp3_c +#define dr_mp3_c + +#include +#include +#include /* For INT_MAX */ + +DRMP3_API void drmp3_version(drmp3_uint32* pMajor, drmp3_uint32* pMinor, drmp3_uint32* pRevision) +{ + if (pMajor) { + *pMajor = DRMP3_VERSION_MAJOR; + } + + if (pMinor) { + *pMinor = DRMP3_VERSION_MINOR; + } + + if (pRevision) { + *pRevision = DRMP3_VERSION_REVISION; + } +} + +DRMP3_API const char* drmp3_version_string(void) +{ + return DRMP3_VERSION_STRING; +} + +/* Disable SIMD when compiling with TCC for now. */ +#if defined(__TINYC__) +#define DR_MP3_NO_SIMD +#endif + +#define DRMP3_OFFSET_PTR(p, offset) ((void*)((drmp3_uint8*)(p) + (offset))) + +#ifndef DRMP3_MAX_FRAME_SYNC_MATCHES +#define DRMP3_MAX_FRAME_SYNC_MATCHES 10 +#endif + +#define DRMP3_SHORT_BLOCK_TYPE 2 +#define DRMP3_STOP_BLOCK_TYPE 3 +#define DRMP3_MODE_MONO 3 +#define DRMP3_MODE_JOINT_STEREO 1 +#define DRMP3_HDR_SIZE 4 +#define DRMP3_HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0) +#define DRMP3_HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60) +#define DRMP3_HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0) +#define DRMP3_HDR_IS_CRC(h) (!((h[1]) & 1)) +#define DRMP3_HDR_TEST_PADDING(h) ((h[2]) & 0x2) +#define DRMP3_HDR_TEST_MPEG1(h) ((h[1]) & 0x8) +#define DRMP3_HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10) +#define DRMP3_HDR_TEST_I_STEREO(h) ((h[3]) & 0x10) +#define DRMP3_HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20) +#define DRMP3_HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3) +#define DRMP3_HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3) +#define DRMP3_HDR_GET_LAYER(h) (((h[1]) >> 1) & 3) +#define DRMP3_HDR_GET_BITRATE(h) ((h[2]) >> 4) +#define DRMP3_HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3) +#define DRMP3_HDR_GET_MY_SAMPLE_RATE(h) (DRMP3_HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1))*3) +#define DRMP3_HDR_IS_FRAME_576(h) ((h[1] & 14) == 2) +#define DRMP3_HDR_IS_LAYER_1(h) ((h[1] & 6) == 6) + +#define DRMP3_BITS_DEQUANTIZER_OUT -1 +#define DRMP3_MAX_SCF (255 + DRMP3_BITS_DEQUANTIZER_OUT*4 - 210) +#define DRMP3_MAX_SCFI ((DRMP3_MAX_SCF + 3) & ~3) + +#define DRMP3_MIN(a, b) ((a) > (b) ? (b) : (a)) +#define DRMP3_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#if !defined(DR_MP3_NO_SIMD) + +#if !defined(DR_MP3_ONLY_SIMD) && ((defined(_MSC_VER) && _MSC_VER >= 1400) && defined(_M_X64)) || ((defined(__i386) || defined(_M_IX86) || defined(__i386__) || defined(__x86_64__)) && ((defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE2__))) +#define DR_MP3_ONLY_SIMD +#endif +#if !defined(DR_MP3_ONLY_SIMD) && (defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC)) +#define DR_MP3_ONLY_SIMD +#endif + +#if ((defined(_MSC_VER) && _MSC_VER >= 1400) && defined(_M_X64)) || ((defined(__i386) || defined(_M_IX86) || defined(__i386__) || defined(__x86_64__)) && ((defined(_M_IX86_FP) && _M_IX86_FP == 2) || defined(__SSE2__))) +#if defined(_MSC_VER) +#include +#endif +#include +#define DRMP3_HAVE_SSE 1 +#define DRMP3_HAVE_SIMD 1 +#define DRMP3_VSTORE _mm_storeu_ps +#define DRMP3_VLD _mm_loadu_ps +#define DRMP3_VSET _mm_set1_ps +#define DRMP3_VADD _mm_add_ps +#define DRMP3_VSUB _mm_sub_ps +#define DRMP3_VMUL _mm_mul_ps +#define DRMP3_VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y)) +#define DRMP3_VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y)) +#define DRMP3_VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s)) +#define DRMP3_VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3)) +typedef __m128 drmp3_f4; +#if (defined(_MSC_VER) || defined(DR_MP3_ONLY_SIMD)) && !defined(__clang__) +#define drmp3_cpuid __cpuid +#else +static __inline__ __attribute__((always_inline)) void drmp3_cpuid(int CPUInfo[], const int InfoType) +{ +#if defined(__PIC__) + __asm__ __volatile__( +#if defined(__x86_64__) + "push %%rbx\n" + "cpuid\n" + "xchgl %%ebx, %1\n" + "pop %%rbx\n" +#else + "xchgl %%ebx, %1\n" + "cpuid\n" + "xchgl %%ebx, %1\n" +#endif + : "=a" (CPUInfo[0]), "=r" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#else + __asm__ __volatile__( + "cpuid" + : "=a" (CPUInfo[0]), "=b" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#endif +} +#endif +static int drmp3_have_simd(void) +{ +#ifdef DR_MP3_ONLY_SIMD + return 1; +#else + static int g_have_simd; + int CPUInfo[4]; +#ifdef MINIMP3_TEST + static int g_counter; + if (g_counter++ > 100) + return 0; +#endif + if (g_have_simd) + goto end; + drmp3_cpuid(CPUInfo, 0); + if (CPUInfo[0] > 0) + { + drmp3_cpuid(CPUInfo, 1); + g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */ + return g_have_simd - 1; + } + +end: + return g_have_simd - 1; +#endif +} +#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC) +#include +#define DRMP3_HAVE_SSE 0 +#define DRMP3_HAVE_SIMD 1 +#define DRMP3_VSTORE vst1q_f32 +#define DRMP3_VLD vld1q_f32 +#define DRMP3_VSET vmovq_n_f32 +#define DRMP3_VADD vaddq_f32 +#define DRMP3_VSUB vsubq_f32 +#define DRMP3_VMUL vmulq_f32 +#define DRMP3_VMAC(a, x, y) vmlaq_f32(a, x, y) +#define DRMP3_VMSB(a, x, y) vmlsq_f32(a, x, y) +#define DRMP3_VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s)) +#define DRMP3_VREV(x) vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x))) +typedef float32x4_t drmp3_f4; +static int drmp3_have_simd(void) +{ /* TODO: detect neon for !DR_MP3_ONLY_SIMD */ + return 1; +} +#else +#define DRMP3_HAVE_SSE 0 +#define DRMP3_HAVE_SIMD 0 +#ifdef DR_MP3_ONLY_SIMD +#error DR_MP3_ONLY_SIMD used, but SSE/NEON not enabled +#endif +#endif + +#else + +#define DRMP3_HAVE_SIMD 0 + +#endif + +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) && !defined(_M_ARM64EC) && !defined(__ARM_ARCH_6M__) +#define DRMP3_HAVE_ARMV6 1 +static __inline__ __attribute__((always_inline)) drmp3_int32 drmp3_clip_int16_arm(drmp3_int32 a) +{ + drmp3_int32 x = 0; + __asm__ ("ssat %0, #16, %1" : "=r"(x) : "r"(a)); + return x; +} +#else +#define DRMP3_HAVE_ARMV6 0 +#endif + + +/* Standard library stuff. */ +#ifndef DRMP3_ASSERT +#include +#define DRMP3_ASSERT(expression) assert(expression) +#endif +#ifndef DRMP3_COPY_MEMORY +#define DRMP3_COPY_MEMORY(dst, src, sz) memcpy((dst), (src), (sz)) +#endif +#ifndef DRMP3_MOVE_MEMORY +#define DRMP3_MOVE_MEMORY(dst, src, sz) memmove((dst), (src), (sz)) +#endif +#ifndef DRMP3_ZERO_MEMORY +#define DRMP3_ZERO_MEMORY(p, sz) memset((p), 0, (sz)) +#endif +#define DRMP3_ZERO_OBJECT(p) DRMP3_ZERO_MEMORY((p), sizeof(*(p))) +#ifndef DRMP3_MALLOC +#define DRMP3_MALLOC(sz) malloc((sz)) +#endif +#ifndef DRMP3_REALLOC +#define DRMP3_REALLOC(p, sz) realloc((p), (sz)) +#endif +#ifndef DRMP3_FREE +#define DRMP3_FREE(p) free((p)) +#endif + + + +typedef struct +{ + float scf[3*64]; + drmp3_uint8 total_bands, stereo_bands, bitalloc[64], scfcod[64]; +} drmp3_L12_scale_info; + +typedef struct +{ + drmp3_uint8 tab_offset, code_tab_width, band_count; +} drmp3_L12_subband_alloc; + +static void drmp3_bs_init(drmp3_bs *bs, const drmp3_uint8 *data, int bytes) +{ + bs->buf = data; + bs->pos = 0; + bs->limit = bytes*8; +} + +static drmp3_uint32 drmp3_bs_get_bits(drmp3_bs *bs, int n) +{ + drmp3_uint32 next, cache = 0, s = bs->pos & 7; + int shl = n + s; + const drmp3_uint8 *p = bs->buf + (bs->pos >> 3); + if ((bs->pos += n) > bs->limit) + return 0; + next = *p++ & (255 >> s); + while ((shl -= 8) > 0) + { + cache |= next << shl; + next = *p++; + } + return cache | (next >> -shl); +} + +static int drmp3_hdr_valid(const drmp3_uint8 *h) +{ + return h[0] == 0xff && + ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) && + (DRMP3_HDR_GET_LAYER(h) != 0) && + (DRMP3_HDR_GET_BITRATE(h) != 15) && + (DRMP3_HDR_GET_SAMPLE_RATE(h) != 3); +} + +static int drmp3_hdr_compare(const drmp3_uint8 *h1, const drmp3_uint8 *h2) +{ + return drmp3_hdr_valid(h2) && + ((h1[1] ^ h2[1]) & 0xFE) == 0 && + ((h1[2] ^ h2[2]) & 0x0C) == 0 && + !(DRMP3_HDR_IS_FREE_FORMAT(h1) ^ DRMP3_HDR_IS_FREE_FORMAT(h2)); +} + +static unsigned drmp3_hdr_bitrate_kbps(const drmp3_uint8 *h) +{ + static const drmp3_uint8 halfrate[2][3][15] = { + { { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,16,24,28,32,40,48,56,64,72,80,88,96,112,128 } }, + { { 0,16,20,24,28,32,40,48,56,64,80,96,112,128,160 }, { 0,16,24,28,32,40,48,56,64,80,96,112,128,160,192 }, { 0,16,32,48,64,80,96,112,128,144,160,176,192,208,224 } }, + }; + return 2*halfrate[!!DRMP3_HDR_TEST_MPEG1(h)][DRMP3_HDR_GET_LAYER(h) - 1][DRMP3_HDR_GET_BITRATE(h)]; +} + +static unsigned drmp3_hdr_sample_rate_hz(const drmp3_uint8 *h) +{ + static const unsigned g_hz[3] = { 44100, 48000, 32000 }; + return g_hz[DRMP3_HDR_GET_SAMPLE_RATE(h)] >> (int)!DRMP3_HDR_TEST_MPEG1(h) >> (int)!DRMP3_HDR_TEST_NOT_MPEG25(h); +} + +static unsigned drmp3_hdr_frame_samples(const drmp3_uint8 *h) +{ + return DRMP3_HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)DRMP3_HDR_IS_FRAME_576(h)); +} + +static int drmp3_hdr_frame_bytes(const drmp3_uint8 *h, int free_format_size) +{ + int frame_bytes = drmp3_hdr_frame_samples(h)*drmp3_hdr_bitrate_kbps(h)*125/drmp3_hdr_sample_rate_hz(h); + if (DRMP3_HDR_IS_LAYER_1(h)) + { + frame_bytes &= ~3; /* slot align */ + } + return frame_bytes ? frame_bytes : free_format_size; +} + +static int drmp3_hdr_padding(const drmp3_uint8 *h) +{ + return DRMP3_HDR_TEST_PADDING(h) ? (DRMP3_HDR_IS_LAYER_1(h) ? 4 : 1) : 0; +} + +#ifndef DR_MP3_ONLY_MP3 +static const drmp3_L12_subband_alloc *drmp3_L12_subband_alloc_table(const drmp3_uint8 *hdr, drmp3_L12_scale_info *sci) +{ + const drmp3_L12_subband_alloc *alloc; + int mode = DRMP3_HDR_GET_STEREO_MODE(hdr); + int nbands, stereo_bands = (mode == DRMP3_MODE_MONO) ? 0 : (mode == DRMP3_MODE_JOINT_STEREO) ? (DRMP3_HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4 : 32; + + if (DRMP3_HDR_IS_LAYER_1(hdr)) + { + static const drmp3_L12_subband_alloc g_alloc_L1[] = { { 76, 4, 32 } }; + alloc = g_alloc_L1; + nbands = 32; + } else if (!DRMP3_HDR_TEST_MPEG1(hdr)) + { + static const drmp3_L12_subband_alloc g_alloc_L2M2[] = { { 60, 4, 4 }, { 44, 3, 7 }, { 44, 2, 19 } }; + alloc = g_alloc_L2M2; + nbands = 30; + } else + { + static const drmp3_L12_subband_alloc g_alloc_L2M1[] = { { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 } }; + int sample_rate_idx = DRMP3_HDR_GET_SAMPLE_RATE(hdr); + unsigned kbps = drmp3_hdr_bitrate_kbps(hdr) >> (int)(mode != DRMP3_MODE_MONO); + if (!kbps) /* free-format */ + { + kbps = 192; + } + + alloc = g_alloc_L2M1; + nbands = 27; + if (kbps < 56) + { + static const drmp3_L12_subband_alloc g_alloc_L2M1_lowrate[] = { { 44, 4, 2 }, { 44, 3, 10 } }; + alloc = g_alloc_L2M1_lowrate; + nbands = sample_rate_idx == 2 ? 12 : 8; + } else if (kbps >= 96 && sample_rate_idx != 1) + { + nbands = 30; + } + } + + sci->total_bands = (drmp3_uint8)nbands; + sci->stereo_bands = (drmp3_uint8)DRMP3_MIN(stereo_bands, nbands); + + return alloc; +} + +static void drmp3_L12_read_scalefactors(drmp3_bs *bs, drmp3_uint8 *pba, drmp3_uint8 *scfcod, int bands, float *scf) +{ + static const float g_deq_L12[18*3] = { +#define DRMP3_DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x + DRMP3_DQ(3),DRMP3_DQ(7),DRMP3_DQ(15),DRMP3_DQ(31),DRMP3_DQ(63),DRMP3_DQ(127),DRMP3_DQ(255),DRMP3_DQ(511),DRMP3_DQ(1023),DRMP3_DQ(2047),DRMP3_DQ(4095),DRMP3_DQ(8191),DRMP3_DQ(16383),DRMP3_DQ(32767),DRMP3_DQ(65535),DRMP3_DQ(3),DRMP3_DQ(5),DRMP3_DQ(9) + }; + int i, m; + for (i = 0; i < bands; i++) + { + float s = 0; + int ba = *pba++; + int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0; + for (m = 4; m; m >>= 1) + { + if (mask & m) + { + int b = drmp3_bs_get_bits(bs, 6); + s = g_deq_L12[ba*3 - 6 + b % 3]*(int)(1 << 21 >> b/3); + } + *scf++ = s; + } + } +} + +static void drmp3_L12_read_scale_info(const drmp3_uint8 *hdr, drmp3_bs *bs, drmp3_L12_scale_info *sci) +{ + static const drmp3_uint8 g_bitalloc_code_tab[] = { + 0,17, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,16, + 0,17,18, 3,19,4,5,16, + 0,17,18,16, + 0,17,18,19, 4,5,6, 7,8, 9,10,11,12,13,14,15, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,14, + 0, 2, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16 + }; + const drmp3_L12_subband_alloc *subband_alloc = drmp3_L12_subband_alloc_table(hdr, sci); + + int i, k = 0, ba_bits = 0; + const drmp3_uint8 *ba_code_tab = g_bitalloc_code_tab; + + for (i = 0; i < sci->total_bands; i++) + { + drmp3_uint8 ba; + if (i == k) + { + k += subband_alloc->band_count; + ba_bits = subband_alloc->code_tab_width; + ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset; + subband_alloc++; + } + ba = ba_code_tab[drmp3_bs_get_bits(bs, ba_bits)]; + sci->bitalloc[2*i] = ba; + if (i < sci->stereo_bands) + { + ba = ba_code_tab[drmp3_bs_get_bits(bs, ba_bits)]; + } + sci->bitalloc[2*i + 1] = sci->stereo_bands ? ba : 0; + } + + for (i = 0; i < 2*sci->total_bands; i++) + { + sci->scfcod[i] = (drmp3_uint8)(sci->bitalloc[i] ? DRMP3_HDR_IS_LAYER_1(hdr) ? 2 : drmp3_bs_get_bits(bs, 2) : 6); + } + + drmp3_L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands*2, sci->scf); + + for (i = sci->stereo_bands; i < sci->total_bands; i++) + { + sci->bitalloc[2*i + 1] = 0; + } +} + +static int drmp3_L12_dequantize_granule(float *grbuf, drmp3_bs *bs, drmp3_L12_scale_info *sci, int group_size) +{ + int i, j, k, choff = 576; + for (j = 0; j < 4; j++) + { + float *dst = grbuf + group_size*j; + for (i = 0; i < 2*sci->total_bands; i++) + { + int ba = sci->bitalloc[i]; + if (ba != 0) + { + if (ba < 17) + { + int half = (1 << (ba - 1)) - 1; + for (k = 0; k < group_size; k++) + { + dst[k] = (float)((int)drmp3_bs_get_bits(bs, ba) - half); + } + } else + { + unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */ + unsigned code = drmp3_bs_get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */ + for (k = 0; k < group_size; k++, code /= mod) + { + dst[k] = (float)((int)(code % mod - mod/2)); + } + } + } + dst += choff; + choff = 18 - choff; + } + } + return group_size*4; +} + +static void drmp3_L12_apply_scf_384(drmp3_L12_scale_info *sci, const float *scf, float *dst) +{ + int i, k; + DRMP3_COPY_MEMORY(dst + 576 + sci->stereo_bands*18, dst + sci->stereo_bands*18, (sci->total_bands - sci->stereo_bands)*18*sizeof(float)); + for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) + { + for (k = 0; k < 12; k++) + { + dst[k + 0] *= scf[0]; + dst[k + 576] *= scf[3]; + } + } +} +#endif + +static int drmp3_L3_read_side_info(drmp3_bs *bs, drmp3_L3_gr_info *gr, const drmp3_uint8 *hdr) +{ + static const drmp3_uint8 g_scf_long[8][23] = { + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 12,12,12,12,12,12,16,20,24,28,32,40,48,56,64,76,90,2,2,2,2,2,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,18,22,26,32,38,46,54,62,70,76,36,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 4,4,4,4,4,4,6,6,8,8,10,12,16,20,24,28,34,42,50,54,76,158,0 }, + { 4,4,4,4,4,4,6,6,6,8,10,12,16,18,22,28,34,40,46,54,54,192,0 }, + { 4,4,4,4,4,4,6,6,8,10,12,16,20,24,30,38,46,56,68,84,102,26,0 } + }; + static const drmp3_uint8 g_scf_short[8][40] = { + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 8,8,8,8,8,8,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + static const drmp3_uint8 g_scf_mixed[8][40] = { + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 12,12,12,4,4,4,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 6,6,6,6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + + unsigned tables, scfsi = 0; + int main_data_begin, part_23_sum = 0; + int gr_count = DRMP3_HDR_IS_MONO(hdr) ? 1 : 2; + int sr_idx = DRMP3_HDR_GET_MY_SAMPLE_RATE(hdr); sr_idx -= (sr_idx != 0); + + if (DRMP3_HDR_TEST_MPEG1(hdr)) + { + gr_count *= 2; + main_data_begin = drmp3_bs_get_bits(bs, 9); + scfsi = drmp3_bs_get_bits(bs, 7 + gr_count); + } else + { + main_data_begin = drmp3_bs_get_bits(bs, 8 + gr_count) >> gr_count; + } + + do + { + if (DRMP3_HDR_IS_MONO(hdr)) + { + scfsi <<= 4; + } + gr->part_23_length = (drmp3_uint16)drmp3_bs_get_bits(bs, 12); + part_23_sum += gr->part_23_length; + gr->big_values = (drmp3_uint16)drmp3_bs_get_bits(bs, 9); + if (gr->big_values > 288) + { + return -1; + } + gr->global_gain = (drmp3_uint8)drmp3_bs_get_bits(bs, 8); + gr->scalefac_compress = (drmp3_uint16)drmp3_bs_get_bits(bs, DRMP3_HDR_TEST_MPEG1(hdr) ? 4 : 9); + gr->sfbtab = g_scf_long[sr_idx]; + gr->n_long_sfb = 22; + gr->n_short_sfb = 0; + if (drmp3_bs_get_bits(bs, 1)) + { + gr->block_type = (drmp3_uint8)drmp3_bs_get_bits(bs, 2); + if (!gr->block_type) + { + return -1; + } + gr->mixed_block_flag = (drmp3_uint8)drmp3_bs_get_bits(bs, 1); + gr->region_count[0] = 7; + gr->region_count[1] = 255; + if (gr->block_type == DRMP3_SHORT_BLOCK_TYPE) + { + scfsi &= 0x0F0F; + if (!gr->mixed_block_flag) + { + gr->region_count[0] = 8; + gr->sfbtab = g_scf_short[sr_idx]; + gr->n_long_sfb = 0; + gr->n_short_sfb = 39; + } else + { + gr->sfbtab = g_scf_mixed[sr_idx]; + gr->n_long_sfb = DRMP3_HDR_TEST_MPEG1(hdr) ? 8 : 6; + gr->n_short_sfb = 30; + } + } + tables = drmp3_bs_get_bits(bs, 10); + tables <<= 5; + gr->subblock_gain[0] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + gr->subblock_gain[1] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + gr->subblock_gain[2] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + } else + { + gr->block_type = 0; + gr->mixed_block_flag = 0; + tables = drmp3_bs_get_bits(bs, 15); + gr->region_count[0] = (drmp3_uint8)drmp3_bs_get_bits(bs, 4); + gr->region_count[1] = (drmp3_uint8)drmp3_bs_get_bits(bs, 3); + gr->region_count[2] = 255; + } + gr->table_select[0] = (drmp3_uint8)(tables >> 10); + gr->table_select[1] = (drmp3_uint8)((tables >> 5) & 31); + gr->table_select[2] = (drmp3_uint8)((tables) & 31); + gr->preflag = (drmp3_uint8)(DRMP3_HDR_TEST_MPEG1(hdr) ? drmp3_bs_get_bits(bs, 1) : (gr->scalefac_compress >= 500)); + gr->scalefac_scale = (drmp3_uint8)drmp3_bs_get_bits(bs, 1); + gr->count1_table = (drmp3_uint8)drmp3_bs_get_bits(bs, 1); + gr->scfsi = (drmp3_uint8)((scfsi >> 12) & 15); + scfsi <<= 4; + gr++; + } while(--gr_count); + + if (part_23_sum + bs->pos > bs->limit + main_data_begin*8) + { + return -1; + } + + return main_data_begin; +} + +static void drmp3_L3_read_scalefactors(drmp3_uint8 *scf, drmp3_uint8 *ist_pos, const drmp3_uint8 *scf_size, const drmp3_uint8 *scf_count, drmp3_bs *bitbuf, int scfsi) +{ + int i, k; + for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) + { + int cnt = scf_count[i]; + if (scfsi & 8) + { + DRMP3_COPY_MEMORY(scf, ist_pos, cnt); + } else + { + int bits = scf_size[i]; + if (!bits) + { + DRMP3_ZERO_MEMORY(scf, cnt); + DRMP3_ZERO_MEMORY(ist_pos, cnt); + } else + { + int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1; + for (k = 0; k < cnt; k++) + { + int s = drmp3_bs_get_bits(bitbuf, bits); + ist_pos[k] = (drmp3_uint8)(s == max_scf ? -1 : s); + scf[k] = (drmp3_uint8)s; + } + } + } + ist_pos += cnt; + scf += cnt; + } + scf[0] = scf[1] = scf[2] = 0; +} + +static float drmp3_L3_ldexp_q2(float y, int exp_q2) +{ + static const float g_expfrac[4] = { 9.31322575e-10f,7.83145814e-10f,6.58544508e-10f,5.53767716e-10f }; + int e; + do + { + e = DRMP3_MIN(30*4, exp_q2); + y *= g_expfrac[e & 3]*(1 << 30 >> (e >> 2)); + } while ((exp_q2 -= e) > 0); + return y; +} + +/* +I've had reports of GCC 14 throwing an incorrect -Wstringop-overflow warning here. This is an attempt +to silence this warning. +*/ +#if (defined(__GNUC__) && (__GNUC__ >= 13)) && !defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wstringop-overflow" +#endif +static void drmp3_L3_decode_scalefactors(const drmp3_uint8 *hdr, drmp3_uint8 *ist_pos, drmp3_bs *bs, const drmp3_L3_gr_info *gr, float *scf, int ch) +{ + static const drmp3_uint8 g_scf_partitions[3][28] = { + { 6,5,5, 5,6,5,5,5,6,5, 7,3,11,10,0,0, 7, 7, 7,0, 6, 6,6,3, 8, 8,5,0 }, + { 8,9,6,12,6,9,9,9,6,9,12,6,15,18,0,0, 6,15,12,0, 6,12,9,6, 6,18,9,0 }, + { 9,9,6,12,9,9,9,9,9,9,12,6,18,18,0,0,12,12,12,0,12, 9,9,6,15,12,9,0 } + }; + const drmp3_uint8 *scf_partition = g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb]; + drmp3_uint8 scf_size[4], iscf[40]; + int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi; + float gain; + + if (DRMP3_HDR_TEST_MPEG1(hdr)) + { + static const drmp3_uint8 g_scfc_decode[16] = { 0,1,2,3, 12,5,6,7, 9,10,11,13, 14,15,18,19 }; + int part = g_scfc_decode[gr->scalefac_compress]; + scf_size[1] = scf_size[0] = (drmp3_uint8)(part >> 2); + scf_size[3] = scf_size[2] = (drmp3_uint8)(part & 3); + } else + { + static const drmp3_uint8 g_mod[6*4] = { 5,5,4,4,5,5,4,1,4,3,1,1,5,6,6,1,4,4,4,1,4,3,1,1 }; + int k, modprod, sfc, ist = DRMP3_HDR_TEST_I_STEREO(hdr) && ch; + sfc = gr->scalefac_compress >> ist; + for (k = ist*3*4; sfc >= 0; sfc -= modprod, k += 4) + { + for (modprod = 1, i = 3; i >= 0; i--) + { + scf_size[i] = (drmp3_uint8)(sfc / modprod % g_mod[k + i]); + modprod *= g_mod[k + i]; + } + } + scf_partition += k; + scfsi = -16; + } + drmp3_L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi); + + if (gr->n_short_sfb) + { + int sh = 3 - scf_shift; + for (i = 0; i < gr->n_short_sfb; i += 3) + { + iscf[gr->n_long_sfb + i + 0] = (drmp3_uint8)(iscf[gr->n_long_sfb + i + 0] + (gr->subblock_gain[0] << sh)); + iscf[gr->n_long_sfb + i + 1] = (drmp3_uint8)(iscf[gr->n_long_sfb + i + 1] + (gr->subblock_gain[1] << sh)); + iscf[gr->n_long_sfb + i + 2] = (drmp3_uint8)(iscf[gr->n_long_sfb + i + 2] + (gr->subblock_gain[2] << sh)); + } + } else if (gr->preflag) + { + static const drmp3_uint8 g_preamp[10] = { 1,1,1,1,2,2,3,3,3,2 }; + for (i = 0; i < 10; i++) + { + iscf[11 + i] = (drmp3_uint8)(iscf[11 + i] + g_preamp[i]); + } + } + + gain_exp = gr->global_gain + DRMP3_BITS_DEQUANTIZER_OUT*4 - 210 - (DRMP3_HDR_IS_MS_STEREO(hdr) ? 2 : 0); + gain = drmp3_L3_ldexp_q2(1 << (DRMP3_MAX_SCFI/4), DRMP3_MAX_SCFI - gain_exp); + for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) + { + scf[i] = drmp3_L3_ldexp_q2(gain, iscf[i] << scf_shift); + } +} +#if (defined(__GNUC__) && (__GNUC__ >= 13)) && !defined(__clang__) + #pragma GCC diagnostic pop +#endif + +static const float g_drmp3_pow43[129 + 16] = { + 0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f, + 0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f +}; + +static float drmp3_L3_pow_43(int x) +{ + float frac; + int sign, mult = 256; + + if (x < 129) + { + return g_drmp3_pow43[16 + x]; + } + + if (x < 1024) + { + mult = 16; + x <<= 3; + } + + sign = 2*x & 64; + frac = (float)((x & 63) - sign) / ((x & ~63) + sign); + return g_drmp3_pow43[16 + ((x + sign) >> 6)]*(1.f + frac*((4.f/3) + frac*(2.f/9)))*mult; +} + +static void drmp3_L3_huffman(float *dst, drmp3_bs *bs, const drmp3_L3_gr_info *gr_info, const float *scf, int layer3gr_limit) +{ + static const drmp3_int16 tabs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 785,785,785,785,784,784,784,784,513,513,513,513,513,513,513,513,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256, + -255,1313,1298,1282,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,290,288, + -255,1313,1298,1282,769,769,769,769,529,529,529,529,529,529,529,529,528,528,528,528,528,528,528,528,512,512,512,512,512,512,512,512,290,288, + -253,-318,-351,-367,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,819,818,547,547,275,275,275,275,561,560,515,546,289,274,288,258, + -254,-287,1329,1299,1314,1312,1057,1057,1042,1042,1026,1026,784,784,784,784,529,529,529,529,529,529,529,529,769,769,769,769,768,768,768,768,563,560,306,306,291,259, + -252,-413,-477,-542,1298,-575,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-383,-399,1107,1092,1106,1061,849,849,789,789,1104,1091,773,773,1076,1075,341,340,325,309,834,804,577,577,532,532,516,516,832,818,803,816,561,561,531,531,515,546,289,289,288,258, + -252,-429,-493,-559,1057,1057,1042,1042,529,529,529,529,529,529,529,529,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,-382,1077,-415,1106,1061,1104,849,849,789,789,1091,1076,1029,1075,834,834,597,581,340,340,339,324,804,833,532,532,832,772,818,803,817,787,816,771,290,290,290,290,288,258, + -253,-349,-414,-447,-463,1329,1299,-479,1314,1312,1057,1057,1042,1042,1026,1026,785,785,785,785,784,784,784,784,769,769,769,769,768,768,768,768,-319,851,821,-335,836,850,805,849,341,340,325,336,533,533,579,579,564,564,773,832,578,548,563,516,321,276,306,291,304,259, + -251,-572,-733,-830,-863,-879,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,1396,1351,1381,1366,1395,1335,1380,-559,1334,1138,1138,1063,1063,1350,1392,1031,1031,1062,1062,1364,1363,1120,1120,1333,1348,881,881,881,881,375,374,359,373,343,358,341,325,791,791,1123,1122,-703,1105,1045,-719,865,865,790,790,774,774,1104,1029,338,293,323,308,-799,-815,833,788,772,818,803,816,322,292,307,320,561,531,515,546,289,274,288,258, + -251,-525,-605,-685,-765,-831,-846,1298,1057,1057,1312,1282,785,785,785,785,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,1399,1398,1383,1367,1382,1396,1351,-511,1381,1366,1139,1139,1079,1079,1124,1124,1364,1349,1363,1333,882,882,882,882,807,807,807,807,1094,1094,1136,1136,373,341,535,535,881,775,867,822,774,-591,324,338,-671,849,550,550,866,864,609,609,293,336,534,534,789,835,773,-751,834,804,308,307,833,788,832,772,562,562,547,547,305,275,560,515,290,290, + -252,-397,-477,-557,-622,-653,-719,-735,-750,1329,1299,1314,1057,1057,1042,1042,1312,1282,1024,1024,785,785,785,785,784,784,784,784,769,769,769,769,-383,1127,1141,1111,1126,1140,1095,1110,869,869,883,883,1079,1109,882,882,375,374,807,868,838,881,791,-463,867,822,368,263,852,837,836,-543,610,610,550,550,352,336,534,534,865,774,851,821,850,805,593,533,579,564,773,832,578,578,548,548,577,577,307,276,306,291,516,560,259,259, + -250,-2107,-2507,-2764,-2909,-2974,-3007,-3023,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-767,-1052,-1213,-1277,-1358,-1405,-1469,-1535,-1550,-1582,-1614,-1647,-1662,-1694,-1726,-1759,-1774,-1807,-1822,-1854,-1886,1565,-1919,-1935,-1951,-1967,1731,1730,1580,1717,-1983,1729,1564,-1999,1548,-2015,-2031,1715,1595,-2047,1714,-2063,1610,-2079,1609,-2095,1323,1323,1457,1457,1307,1307,1712,1547,1641,1700,1699,1594,1685,1625,1442,1442,1322,1322,-780,-973,-910,1279,1278,1277,1262,1276,1261,1275,1215,1260,1229,-959,974,974,989,989,-943,735,478,478,495,463,506,414,-1039,1003,958,1017,927,942,987,957,431,476,1272,1167,1228,-1183,1256,-1199,895,895,941,941,1242,1227,1212,1135,1014,1014,490,489,503,487,910,1013,985,925,863,894,970,955,1012,847,-1343,831,755,755,984,909,428,366,754,559,-1391,752,486,457,924,997,698,698,983,893,740,740,908,877,739,739,667,667,953,938,497,287,271,271,683,606,590,712,726,574,302,302,738,736,481,286,526,725,605,711,636,724,696,651,589,681,666,710,364,467,573,695,466,466,301,465,379,379,709,604,665,679,316,316,634,633,436,436,464,269,424,394,452,332,438,363,347,408,393,448,331,422,362,407,392,421,346,406,391,376,375,359,1441,1306,-2367,1290,-2383,1337,-2399,-2415,1426,1321,-2431,1411,1336,-2447,-2463,-2479,1169,1169,1049,1049,1424,1289,1412,1352,1319,-2495,1154,1154,1064,1064,1153,1153,416,390,360,404,403,389,344,374,373,343,358,372,327,357,342,311,356,326,1395,1394,1137,1137,1047,1047,1365,1392,1287,1379,1334,1364,1349,1378,1318,1363,792,792,792,792,1152,1152,1032,1032,1121,1121,1046,1046,1120,1120,1030,1030,-2895,1106,1061,1104,849,849,789,789,1091,1076,1029,1090,1060,1075,833,833,309,324,532,532,832,772,818,803,561,561,531,560,515,546,289,274,288,258, + -250,-1179,-1579,-1836,-1996,-2124,-2253,-2333,-2413,-2477,-2542,-2574,-2607,-2622,-2655,1314,1313,1298,1312,1282,785,785,785,785,1040,1040,1025,1025,768,768,768,768,-766,-798,-830,-862,-895,-911,-927,-943,-959,-975,-991,-1007,-1023,-1039,-1055,-1070,1724,1647,-1103,-1119,1631,1767,1662,1738,1708,1723,-1135,1780,1615,1779,1599,1677,1646,1778,1583,-1151,1777,1567,1737,1692,1765,1722,1707,1630,1751,1661,1764,1614,1736,1676,1763,1750,1645,1598,1721,1691,1762,1706,1582,1761,1566,-1167,1749,1629,767,766,751,765,494,494,735,764,719,749,734,763,447,447,748,718,477,506,431,491,446,476,461,505,415,430,475,445,504,399,460,489,414,503,383,474,429,459,502,502,746,752,488,398,501,473,413,472,486,271,480,270,-1439,-1455,1357,-1471,-1487,-1503,1341,1325,-1519,1489,1463,1403,1309,-1535,1372,1448,1418,1476,1356,1462,1387,-1551,1475,1340,1447,1402,1386,-1567,1068,1068,1474,1461,455,380,468,440,395,425,410,454,364,467,466,464,453,269,409,448,268,432,1371,1473,1432,1417,1308,1460,1355,1446,1459,1431,1083,1083,1401,1416,1458,1445,1067,1067,1370,1457,1051,1051,1291,1430,1385,1444,1354,1415,1400,1443,1082,1082,1173,1113,1186,1066,1185,1050,-1967,1158,1128,1172,1097,1171,1081,-1983,1157,1112,416,266,375,400,1170,1142,1127,1065,793,793,1169,1033,1156,1096,1141,1111,1155,1080,1126,1140,898,898,808,808,897,897,792,792,1095,1152,1032,1125,1110,1139,1079,1124,882,807,838,881,853,791,-2319,867,368,263,822,852,837,866,806,865,-2399,851,352,262,534,534,821,836,594,594,549,549,593,593,533,533,848,773,579,579,564,578,548,563,276,276,577,576,306,291,516,560,305,305,275,259, + -251,-892,-2058,-2620,-2828,-2957,-3023,-3039,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,-559,1530,-575,-591,1528,1527,1407,1526,1391,1023,1023,1023,1023,1525,1375,1268,1268,1103,1103,1087,1087,1039,1039,1523,-604,815,815,815,815,510,495,509,479,508,463,507,447,431,505,415,399,-734,-782,1262,-815,1259,1244,-831,1258,1228,-847,-863,1196,-879,1253,987,987,748,-767,493,493,462,477,414,414,686,669,478,446,461,445,474,429,487,458,412,471,1266,1264,1009,1009,799,799,-1019,-1276,-1452,-1581,-1677,-1757,-1821,-1886,-1933,-1997,1257,1257,1483,1468,1512,1422,1497,1406,1467,1496,1421,1510,1134,1134,1225,1225,1466,1451,1374,1405,1252,1252,1358,1480,1164,1164,1251,1251,1238,1238,1389,1465,-1407,1054,1101,-1423,1207,-1439,830,830,1248,1038,1237,1117,1223,1148,1236,1208,411,426,395,410,379,269,1193,1222,1132,1235,1221,1116,976,976,1192,1162,1177,1220,1131,1191,963,963,-1647,961,780,-1663,558,558,994,993,437,408,393,407,829,978,813,797,947,-1743,721,721,377,392,844,950,828,890,706,706,812,859,796,960,948,843,934,874,571,571,-1919,690,555,689,421,346,539,539,944,779,918,873,932,842,903,888,570,570,931,917,674,674,-2575,1562,-2591,1609,-2607,1654,1322,1322,1441,1441,1696,1546,1683,1593,1669,1624,1426,1426,1321,1321,1639,1680,1425,1425,1305,1305,1545,1668,1608,1623,1667,1592,1638,1666,1320,1320,1652,1607,1409,1409,1304,1304,1288,1288,1664,1637,1395,1395,1335,1335,1622,1636,1394,1394,1319,1319,1606,1621,1392,1392,1137,1137,1137,1137,345,390,360,375,404,373,1047,-2751,-2767,-2783,1062,1121,1046,-2799,1077,-2815,1106,1061,789,789,1105,1104,263,355,310,340,325,354,352,262,339,324,1091,1076,1029,1090,1060,1075,833,833,788,788,1088,1028,818,818,803,803,561,561,531,531,816,771,546,546,289,274,288,258, + -253,-317,-381,-446,-478,-509,1279,1279,-811,-1179,-1451,-1756,-1900,-2028,-2189,-2253,-2333,-2414,-2445,-2511,-2526,1313,1298,-2559,1041,1041,1040,1040,1025,1025,1024,1024,1022,1007,1021,991,1020,975,1019,959,687,687,1018,1017,671,671,655,655,1016,1015,639,639,758,758,623,623,757,607,756,591,755,575,754,559,543,543,1009,783,-575,-621,-685,-749,496,-590,750,749,734,748,974,989,1003,958,988,973,1002,942,987,957,972,1001,926,986,941,971,956,1000,910,985,925,999,894,970,-1071,-1087,-1102,1390,-1135,1436,1509,1451,1374,-1151,1405,1358,1480,1420,-1167,1507,1494,1389,1342,1465,1435,1450,1326,1505,1310,1493,1373,1479,1404,1492,1464,1419,428,443,472,397,736,526,464,464,486,457,442,471,484,482,1357,1449,1434,1478,1388,1491,1341,1490,1325,1489,1463,1403,1309,1477,1372,1448,1418,1433,1476,1356,1462,1387,-1439,1475,1340,1447,1402,1474,1324,1461,1371,1473,269,448,1432,1417,1308,1460,-1711,1459,-1727,1441,1099,1099,1446,1386,1431,1401,-1743,1289,1083,1083,1160,1160,1458,1445,1067,1067,1370,1457,1307,1430,1129,1129,1098,1098,268,432,267,416,266,400,-1887,1144,1187,1082,1173,1113,1186,1066,1050,1158,1128,1143,1172,1097,1171,1081,420,391,1157,1112,1170,1142,1127,1065,1169,1049,1156,1096,1141,1111,1155,1080,1126,1154,1064,1153,1140,1095,1048,-2159,1125,1110,1137,-2175,823,823,1139,1138,807,807,384,264,368,263,868,838,853,791,867,822,852,837,866,806,865,790,-2319,851,821,836,352,262,850,805,849,-2399,533,533,835,820,336,261,578,548,563,577,532,532,832,772,562,562,547,547,305,275,560,515,290,290,288,258 }; + static const drmp3_uint8 tab32[] = { 130,162,193,209,44,28,76,140,9,9,9,9,9,9,9,9,190,254,222,238,126,94,157,157,109,61,173,205}; + static const drmp3_uint8 tab33[] = { 252,236,220,204,188,172,156,140,124,108,92,76,60,44,28,12 }; + static const drmp3_int16 tabindex[2*16] = { 0,32,64,98,0,132,180,218,292,364,426,538,648,746,0,1126,1460,1460,1460,1460,1460,1460,1460,1460,1842,1842,1842,1842,1842,1842,1842,1842 }; + static const drmp3_uint8 g_linbits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,8,10,13,4,5,6,7,8,9,11,13 }; + +#define DRMP3_PEEK_BITS(n) (bs_cache >> (32 - (n))) +#define DRMP3_FLUSH_BITS(n) { bs_cache <<= (n); bs_sh += (n); } +#define DRMP3_CHECK_BITS while (bs_sh >= 0) { bs_cache |= (drmp3_uint32)*bs_next_ptr++ << bs_sh; bs_sh -= 8; } +#define DRMP3_BSPOS ((bs_next_ptr - bs->buf)*8 - 24 + bs_sh) + + float one = 0.0f; + int ireg = 0, big_val_cnt = gr_info->big_values; + const drmp3_uint8 *sfb = gr_info->sfbtab; + const drmp3_uint8 *bs_next_ptr = bs->buf + bs->pos/8; + drmp3_uint32 bs_cache = (((bs_next_ptr[0]*256u + bs_next_ptr[1])*256u + bs_next_ptr[2])*256u + bs_next_ptr[3]) << (bs->pos & 7); + int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8; + bs_next_ptr += 4; + + while (big_val_cnt > 0) + { + int tab_num = gr_info->table_select[ireg]; + int sfb_cnt = gr_info->region_count[ireg++]; + const drmp3_int16 *codebook = tabs + tabindex[tab_num]; + int linbits = g_linbits[tab_num]; + if (linbits) + { + do + { + np = *sfb++ / 2; + pairs_to_decode = DRMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[DRMP3_PEEK_BITS(w)]; + while (leaf < 0) + { + DRMP3_FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[DRMP3_PEEK_BITS(w) - (leaf >> 3)]; + } + DRMP3_FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + if (lsb == 15) + { + lsb += DRMP3_PEEK_BITS(linbits); + DRMP3_FLUSH_BITS(linbits); + DRMP3_CHECK_BITS; + *dst = one*drmp3_L3_pow_43(lsb)*((drmp3_int32)bs_cache < 0 ? -1: 1); + } else + { + *dst = g_drmp3_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + } + DRMP3_FLUSH_BITS(lsb ? 1 : 0); + } + DRMP3_CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } else + { + do + { + np = *sfb++ / 2; + pairs_to_decode = DRMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[DRMP3_PEEK_BITS(w)]; + while (leaf < 0) + { + DRMP3_FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[DRMP3_PEEK_BITS(w) - (leaf >> 3)]; + } + DRMP3_FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + *dst = g_drmp3_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + DRMP3_FLUSH_BITS(lsb ? 1 : 0); + } + DRMP3_CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } + } + + for (np = 1 - big_val_cnt;; dst += 4) + { + const drmp3_uint8 *codebook_count1 = (gr_info->count1_table) ? tab33 : tab32; + int leaf = codebook_count1[DRMP3_PEEK_BITS(4)]; + if (!(leaf & 8)) + { + leaf = codebook_count1[(leaf >> 3) + (bs_cache << 4 >> (32 - (leaf & 3)))]; + } + DRMP3_FLUSH_BITS(leaf & 7); + if (DRMP3_BSPOS > layer3gr_limit) + { + break; + } +#define DRMP3_RELOAD_SCALEFACTOR if (!--np) { np = *sfb++/2; if (!np) break; one = *scf++; } +#define DRMP3_DEQ_COUNT1(s) if (leaf & (128 >> s)) { dst[s] = ((drmp3_int32)bs_cache < 0) ? -one : one; DRMP3_FLUSH_BITS(1) } + DRMP3_RELOAD_SCALEFACTOR; + DRMP3_DEQ_COUNT1(0); + DRMP3_DEQ_COUNT1(1); + DRMP3_RELOAD_SCALEFACTOR; + DRMP3_DEQ_COUNT1(2); + DRMP3_DEQ_COUNT1(3); + DRMP3_CHECK_BITS; + } + + bs->pos = layer3gr_limit; +} + +static void drmp3_L3_midside_stereo(float *left, int n) +{ + int i = 0; + float *right = left + 576; +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) + { + for (; i < n - 3; i += 4) + { + drmp3_f4 vl = DRMP3_VLD(left + i); + drmp3_f4 vr = DRMP3_VLD(right + i); + DRMP3_VSTORE(left + i, DRMP3_VADD(vl, vr)); + DRMP3_VSTORE(right + i, DRMP3_VSUB(vl, vr)); + } +#ifdef __GNUC__ + /* Workaround for spurious -Waggressive-loop-optimizations warning from gcc. + * For more info see: https://github.com/lieff/minimp3/issues/88 + */ + if (__builtin_constant_p(n % 4 == 0) && n % 4 == 0) + return; +#endif + } +#endif + for (; i < n; i++) + { + float a = left[i]; + float b = right[i]; + left[i] = a + b; + right[i] = a - b; + } +} + +static void drmp3_L3_intensity_stereo_band(float *left, int n, float kl, float kr) +{ + int i; + for (i = 0; i < n; i++) + { + left[i + 576] = left[i]*kr; + left[i] = left[i]*kl; + } +} + +static void drmp3_L3_stereo_top_band(const float *right, const drmp3_uint8 *sfb, int nbands, int max_band[3]) +{ + int i, k; + + max_band[0] = max_band[1] = max_band[2] = -1; + + for (i = 0; i < nbands; i++) + { + for (k = 0; k < sfb[i]; k += 2) + { + if (right[k] != 0 || right[k + 1] != 0) + { + max_band[i % 3] = i; + break; + } + } + right += sfb[i]; + } +} + +static void drmp3_L3_stereo_process(float *left, const drmp3_uint8 *ist_pos, const drmp3_uint8 *sfb, const drmp3_uint8 *hdr, int max_band[3], int mpeg2_sh) +{ + static const float g_pan[7*2] = { 0,1,0.21132487f,0.78867513f,0.36602540f,0.63397460f,0.5f,0.5f,0.63397460f,0.36602540f,0.78867513f,0.21132487f,1,0 }; + unsigned i, max_pos = DRMP3_HDR_TEST_MPEG1(hdr) ? 7 : 64; + + for (i = 0; sfb[i]; i++) + { + unsigned ipos = ist_pos[i]; + if ((int)i > max_band[i % 3] && ipos < max_pos) + { + float kl, kr, s = DRMP3_HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1; + if (DRMP3_HDR_TEST_MPEG1(hdr)) + { + kl = g_pan[2*ipos]; + kr = g_pan[2*ipos + 1]; + } else + { + kl = 1; + kr = drmp3_L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh); + if (ipos & 1) + { + kl = kr; + kr = 1; + } + } + drmp3_L3_intensity_stereo_band(left, sfb[i], kl*s, kr*s); + } else if (DRMP3_HDR_TEST_MS_STEREO(hdr)) + { + drmp3_L3_midside_stereo(left, sfb[i]); + } + left += sfb[i]; + } +} + +static void drmp3_L3_intensity_stereo(float *left, drmp3_uint8 *ist_pos, const drmp3_L3_gr_info *gr, const drmp3_uint8 *hdr) +{ + int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb; + int i, max_blocks = gr->n_short_sfb ? 3 : 1; + + drmp3_L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band); + if (gr->n_long_sfb) + { + max_band[0] = max_band[1] = max_band[2] = DRMP3_MAX(DRMP3_MAX(max_band[0], max_band[1]), max_band[2]); + } + for (i = 0; i < max_blocks; i++) + { + int default_pos = DRMP3_HDR_TEST_MPEG1(hdr) ? 3 : 0; + int itop = n_sfb - max_blocks + i; + int prev = itop - max_blocks; + ist_pos[itop] = (drmp3_uint8)(max_band[i] >= prev ? default_pos : ist_pos[prev]); + } + drmp3_L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band, gr[1].scalefac_compress & 1); +} + +static void drmp3_L3_reorder(float *grbuf, float *scratch, const drmp3_uint8 *sfb) +{ + int i, len; + float *src = grbuf, *dst = scratch; + + for (;0 != (len = *sfb); sfb += 3, src += 2*len) + { + for (i = 0; i < len; i++, src++) + { + *dst++ = src[0*len]; + *dst++ = src[1*len]; + *dst++ = src[2*len]; + } + } + DRMP3_COPY_MEMORY(grbuf, scratch, (dst - scratch)*sizeof(float)); +} + +static void drmp3_L3_antialias(float *grbuf, int nbands) +{ + static const float g_aa[2][8] = { + {0.85749293f,0.88174200f,0.94962865f,0.98331459f,0.99551782f,0.99916056f,0.99989920f,0.99999316f}, + {0.51449576f,0.47173197f,0.31337745f,0.18191320f,0.09457419f,0.04096558f,0.01419856f,0.00369997f} + }; + + for (; nbands > 0; nbands--, grbuf += 18) + { + int i = 0; +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (; i < 8; i += 4) + { + drmp3_f4 vu = DRMP3_VLD(grbuf + 18 + i); + drmp3_f4 vd = DRMP3_VLD(grbuf + 14 - i); + drmp3_f4 vc0 = DRMP3_VLD(g_aa[0] + i); + drmp3_f4 vc1 = DRMP3_VLD(g_aa[1] + i); + vd = DRMP3_VREV(vd); + DRMP3_VSTORE(grbuf + 18 + i, DRMP3_VSUB(DRMP3_VMUL(vu, vc0), DRMP3_VMUL(vd, vc1))); + vd = DRMP3_VADD(DRMP3_VMUL(vu, vc1), DRMP3_VMUL(vd, vc0)); + DRMP3_VSTORE(grbuf + 14 - i, DRMP3_VREV(vd)); + } +#endif +#ifndef DR_MP3_ONLY_SIMD + for(; i < 8; i++) + { + float u = grbuf[18 + i]; + float d = grbuf[17 - i]; + grbuf[18 + i] = u*g_aa[0][i] - d*g_aa[1][i]; + grbuf[17 - i] = u*g_aa[1][i] + d*g_aa[0][i]; + } +#endif + } +} + +static void drmp3_L3_dct3_9(float *y) +{ + float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4; + + s0 = y[0]; s2 = y[2]; s4 = y[4]; s6 = y[6]; s8 = y[8]; + t0 = s0 + s6*0.5f; + s0 -= s6; + t4 = (s4 + s2)*0.93969262f; + t2 = (s8 + s2)*0.76604444f; + s6 = (s4 - s8)*0.17364818f; + s4 += s8 - s2; + + s2 = s0 - s4*0.5f; + y[4] = s4 + s0; + s8 = t0 - t2 + s6; + s0 = t0 - t4 + t2; + s4 = t0 + t4 - s6; + + s1 = y[1]; s3 = y[3]; s5 = y[5]; s7 = y[7]; + + s3 *= 0.86602540f; + t0 = (s5 + s1)*0.98480775f; + t4 = (s5 - s7)*0.34202014f; + t2 = (s1 + s7)*0.64278761f; + s1 = (s1 - s5 - s7)*0.86602540f; + + s5 = t0 - s3 - t2; + s7 = t4 - s3 - t0; + s3 = t4 + s3 - t2; + + y[0] = s4 - s7; + y[1] = s2 + s1; + y[2] = s0 - s3; + y[3] = s8 + s5; + y[5] = s8 - s5; + y[6] = s0 + s3; + y[7] = s2 - s1; + y[8] = s4 + s7; +} + +static void drmp3_L3_imdct36(float *grbuf, float *overlap, const float *window, int nbands) +{ + int i, j; + static const float g_twid9[18] = { + 0.73727734f,0.79335334f,0.84339145f,0.88701083f,0.92387953f,0.95371695f,0.97629601f,0.99144486f,0.99904822f,0.67559021f,0.60876143f,0.53729961f,0.46174861f,0.38268343f,0.30070580f,0.21643961f,0.13052619f,0.04361938f + }; + + for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) + { + float co[9], si[9]; + co[0] = -grbuf[0]; + si[0] = grbuf[17]; + for (i = 0; i < 4; i++) + { + si[8 - 2*i] = grbuf[4*i + 1] - grbuf[4*i + 2]; + co[1 + 2*i] = grbuf[4*i + 1] + grbuf[4*i + 2]; + si[7 - 2*i] = grbuf[4*i + 4] - grbuf[4*i + 3]; + co[2 + 2*i] = -(grbuf[4*i + 3] + grbuf[4*i + 4]); + } + drmp3_L3_dct3_9(co); + drmp3_L3_dct3_9(si); + + si[1] = -si[1]; + si[3] = -si[3]; + si[5] = -si[5]; + si[7] = -si[7]; + + i = 0; + +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (; i < 8; i += 4) + { + drmp3_f4 vovl = DRMP3_VLD(overlap + i); + drmp3_f4 vc = DRMP3_VLD(co + i); + drmp3_f4 vs = DRMP3_VLD(si + i); + drmp3_f4 vr0 = DRMP3_VLD(g_twid9 + i); + drmp3_f4 vr1 = DRMP3_VLD(g_twid9 + 9 + i); + drmp3_f4 vw0 = DRMP3_VLD(window + i); + drmp3_f4 vw1 = DRMP3_VLD(window + 9 + i); + drmp3_f4 vsum = DRMP3_VADD(DRMP3_VMUL(vc, vr1), DRMP3_VMUL(vs, vr0)); + DRMP3_VSTORE(overlap + i, DRMP3_VSUB(DRMP3_VMUL(vc, vr0), DRMP3_VMUL(vs, vr1))); + DRMP3_VSTORE(grbuf + i, DRMP3_VSUB(DRMP3_VMUL(vovl, vw0), DRMP3_VMUL(vsum, vw1))); + vsum = DRMP3_VADD(DRMP3_VMUL(vovl, vw1), DRMP3_VMUL(vsum, vw0)); + DRMP3_VSTORE(grbuf + 14 - i, DRMP3_VREV(vsum)); + } +#endif + for (; i < 9; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid9[9 + i] + si[i]*g_twid9[0 + i]; + overlap[i] = co[i]*g_twid9[0 + i] - si[i]*g_twid9[9 + i]; + grbuf[i] = ovl*window[0 + i] - sum*window[9 + i]; + grbuf[17 - i] = ovl*window[9 + i] + sum*window[0 + i]; + } + } +} + +static void drmp3_L3_idct3(float x0, float x1, float x2, float *dst) +{ + float m1 = x1*0.86602540f; + float a1 = x0 - x2*0.5f; + dst[1] = x0 + x2; + dst[0] = a1 + m1; + dst[2] = a1 - m1; +} + +static void drmp3_L3_imdct12(float *x, float *dst, float *overlap) +{ + static const float g_twid3[6] = { 0.79335334f,0.92387953f,0.99144486f, 0.60876143f,0.38268343f,0.13052619f }; + float co[3], si[3]; + int i; + + drmp3_L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co); + drmp3_L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si); + si[1] = -si[1]; + + for (i = 0; i < 3; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid3[3 + i] + si[i]*g_twid3[0 + i]; + overlap[i] = co[i]*g_twid3[0 + i] - si[i]*g_twid3[3 + i]; + dst[i] = ovl*g_twid3[2 - i] - sum*g_twid3[5 - i]; + dst[5 - i] = ovl*g_twid3[5 - i] + sum*g_twid3[2 - i]; + } +} + +static void drmp3_L3_imdct_short(float *grbuf, float *overlap, int nbands) +{ + for (;nbands > 0; nbands--, overlap += 9, grbuf += 18) + { + float tmp[18]; + DRMP3_COPY_MEMORY(tmp, grbuf, sizeof(tmp)); + DRMP3_COPY_MEMORY(grbuf, overlap, 6*sizeof(float)); + drmp3_L3_imdct12(tmp, grbuf + 6, overlap + 6); + drmp3_L3_imdct12(tmp + 1, grbuf + 12, overlap + 6); + drmp3_L3_imdct12(tmp + 2, overlap, overlap + 6); + } +} + +static void drmp3_L3_change_sign(float *grbuf) +{ + int b, i; + for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36) + for (i = 1; i < 18; i += 2) + grbuf[i] = -grbuf[i]; +} + +static void drmp3_L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type, unsigned n_long_bands) +{ + static const float g_mdct_window[2][18] = { + { 0.99904822f,0.99144486f,0.97629601f,0.95371695f,0.92387953f,0.88701083f,0.84339145f,0.79335334f,0.73727734f,0.04361938f,0.13052619f,0.21643961f,0.30070580f,0.38268343f,0.46174861f,0.53729961f,0.60876143f,0.67559021f }, + { 1,1,1,1,1,1,0.99144486f,0.92387953f,0.79335334f,0,0,0,0,0,0,0.13052619f,0.38268343f,0.60876143f } + }; + if (n_long_bands) + { + drmp3_L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands); + grbuf += 18*n_long_bands; + overlap += 9*n_long_bands; + } + if (block_type == DRMP3_SHORT_BLOCK_TYPE) + drmp3_L3_imdct_short(grbuf, overlap, 32 - n_long_bands); + else + drmp3_L3_imdct36(grbuf, overlap, g_mdct_window[block_type == DRMP3_STOP_BLOCK_TYPE], 32 - n_long_bands); +} + +static void drmp3_L3_save_reservoir(drmp3dec *h, drmp3dec_scratch *s) +{ + int pos = (s->bs.pos + 7)/8u; + int remains = s->bs.limit/8u - pos; + if (remains > DRMP3_MAX_BITRESERVOIR_BYTES) + { + pos += remains - DRMP3_MAX_BITRESERVOIR_BYTES; + remains = DRMP3_MAX_BITRESERVOIR_BYTES; + } + if (remains > 0) + { + DRMP3_MOVE_MEMORY(h->reserv_buf, s->maindata + pos, remains); + } + h->reserv = remains; +} + +static int drmp3_L3_restore_reservoir(drmp3dec *h, drmp3_bs *bs, drmp3dec_scratch *s, int main_data_begin) +{ + int frame_bytes = (bs->limit - bs->pos)/8; + int bytes_have = DRMP3_MIN(h->reserv, main_data_begin); + DRMP3_COPY_MEMORY(s->maindata, h->reserv_buf + DRMP3_MAX(0, h->reserv - main_data_begin), DRMP3_MIN(h->reserv, main_data_begin)); + DRMP3_COPY_MEMORY(s->maindata + bytes_have, bs->buf + bs->pos/8, frame_bytes); + drmp3_bs_init(&s->bs, s->maindata, bytes_have + frame_bytes); + return h->reserv >= main_data_begin; +} + +static void drmp3_L3_decode(drmp3dec *h, drmp3dec_scratch *s, drmp3_L3_gr_info *gr_info, int nch) +{ + int ch; + + for (ch = 0; ch < nch; ch++) + { + int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length; + drmp3_L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch, s->scf, ch); + drmp3_L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit); + } + + if (DRMP3_HDR_TEST_I_STEREO(h->header)) + { + drmp3_L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header); + } else if (DRMP3_HDR_IS_MS_STEREO(h->header)) + { + drmp3_L3_midside_stereo(s->grbuf[0], 576); + } + + for (ch = 0; ch < nch; ch++, gr_info++) + { + int aa_bands = 31; + int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0) << (int)(DRMP3_HDR_GET_MY_SAMPLE_RATE(h->header) == 2); + + if (gr_info->n_short_sfb) + { + aa_bands = n_long_bands - 1; + drmp3_L3_reorder(s->grbuf[ch] + n_long_bands*18, s->syn[0], gr_info->sfbtab + gr_info->n_long_sfb); + } + + drmp3_L3_antialias(s->grbuf[ch], aa_bands); + drmp3_L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type, n_long_bands); + drmp3_L3_change_sign(s->grbuf[ch]); + } +} + +static void drmp3d_DCT_II(float *grbuf, int n) +{ + static const float g_sec[24] = { + 10.19000816f,0.50060302f,0.50241929f,3.40760851f,0.50547093f,0.52249861f,2.05778098f,0.51544732f,0.56694406f,1.48416460f,0.53104258f,0.64682180f,1.16943991f,0.55310392f,0.78815460f,0.97256821f,0.58293498f,1.06067765f,0.83934963f,0.62250412f,1.72244716f,0.74453628f,0.67480832f,5.10114861f + }; + int i, k = 0; +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (; k < n; k += 4) + { + drmp3_f4 t[4][8], *x; + float *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + drmp3_f4 x0 = DRMP3_VLD(&y[i*18]); + drmp3_f4 x1 = DRMP3_VLD(&y[(15 - i)*18]); + drmp3_f4 x2 = DRMP3_VLD(&y[(16 + i)*18]); + drmp3_f4 x3 = DRMP3_VLD(&y[(31 - i)*18]); + drmp3_f4 t0 = DRMP3_VADD(x0, x3); + drmp3_f4 t1 = DRMP3_VADD(x1, x2); + drmp3_f4 t2 = DRMP3_VMUL_S(DRMP3_VSUB(x1, x2), g_sec[3*i + 0]); + drmp3_f4 t3 = DRMP3_VMUL_S(DRMP3_VSUB(x0, x3), g_sec[3*i + 1]); + x[0] = DRMP3_VADD(t0, t1); + x[8] = DRMP3_VMUL_S(DRMP3_VSUB(t0, t1), g_sec[3*i + 2]); + x[16] = DRMP3_VADD(t3, t2); + x[24] = DRMP3_VMUL_S(DRMP3_VSUB(t3, t2), g_sec[3*i + 2]); + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + drmp3_f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = DRMP3_VSUB(x0, x7); x0 = DRMP3_VADD(x0, x7); + x7 = DRMP3_VSUB(x1, x6); x1 = DRMP3_VADD(x1, x6); + x6 = DRMP3_VSUB(x2, x5); x2 = DRMP3_VADD(x2, x5); + x5 = DRMP3_VSUB(x3, x4); x3 = DRMP3_VADD(x3, x4); + x4 = DRMP3_VSUB(x0, x3); x0 = DRMP3_VADD(x0, x3); + x3 = DRMP3_VSUB(x1, x2); x1 = DRMP3_VADD(x1, x2); + x[0] = DRMP3_VADD(x0, x1); + x[4] = DRMP3_VMUL_S(DRMP3_VSUB(x0, x1), 0.70710677f); + x5 = DRMP3_VADD(x5, x6); + x6 = DRMP3_VMUL_S(DRMP3_VADD(x6, x7), 0.70710677f); + x7 = DRMP3_VADD(x7, xt); + x3 = DRMP3_VMUL_S(DRMP3_VADD(x3, x4), 0.70710677f); + x5 = DRMP3_VSUB(x5, DRMP3_VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */ + x7 = DRMP3_VADD(x7, DRMP3_VMUL_S(x5, 0.382683432f)); + x5 = DRMP3_VSUB(x5, DRMP3_VMUL_S(x7, 0.198912367f)); + x0 = DRMP3_VSUB(xt, x6); xt = DRMP3_VADD(xt, x6); + x[1] = DRMP3_VMUL_S(DRMP3_VADD(xt, x7), 0.50979561f); + x[2] = DRMP3_VMUL_S(DRMP3_VADD(x4, x3), 0.54119611f); + x[3] = DRMP3_VMUL_S(DRMP3_VSUB(x0, x5), 0.60134488f); + x[5] = DRMP3_VMUL_S(DRMP3_VADD(x0, x5), 0.89997619f); + x[6] = DRMP3_VMUL_S(DRMP3_VSUB(x4, x3), 1.30656302f); + x[7] = DRMP3_VMUL_S(DRMP3_VSUB(xt, x7), 2.56291556f); + } + + if (k > n - 3) + { +#if DRMP3_HAVE_SSE +#define DRMP3_VSAVE2(i, v) _mm_storel_pi((__m64 *)(void*)&y[i*18], v) +#else +#define DRMP3_VSAVE2(i, v) vst1_f32((float32_t *)&y[(i)*18], vget_low_f32(v)) +#endif + for (i = 0; i < 7; i++, y += 4*18) + { + drmp3_f4 s = DRMP3_VADD(t[3][i], t[3][i + 1]); + DRMP3_VSAVE2(0, t[0][i]); + DRMP3_VSAVE2(1, DRMP3_VADD(t[2][i], s)); + DRMP3_VSAVE2(2, DRMP3_VADD(t[1][i], t[1][i + 1])); + DRMP3_VSAVE2(3, DRMP3_VADD(t[2][1 + i], s)); + } + DRMP3_VSAVE2(0, t[0][7]); + DRMP3_VSAVE2(1, DRMP3_VADD(t[2][7], t[3][7])); + DRMP3_VSAVE2(2, t[1][7]); + DRMP3_VSAVE2(3, t[3][7]); + } else + { +#define DRMP3_VSAVE4(i, v) DRMP3_VSTORE(&y[(i)*18], v) + for (i = 0; i < 7; i++, y += 4*18) + { + drmp3_f4 s = DRMP3_VADD(t[3][i], t[3][i + 1]); + DRMP3_VSAVE4(0, t[0][i]); + DRMP3_VSAVE4(1, DRMP3_VADD(t[2][i], s)); + DRMP3_VSAVE4(2, DRMP3_VADD(t[1][i], t[1][i + 1])); + DRMP3_VSAVE4(3, DRMP3_VADD(t[2][1 + i], s)); + } + DRMP3_VSAVE4(0, t[0][7]); + DRMP3_VSAVE4(1, DRMP3_VADD(t[2][7], t[3][7])); + DRMP3_VSAVE4(2, t[1][7]); + DRMP3_VSAVE4(3, t[3][7]); + } + } else +#endif +#ifdef DR_MP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else + for (; k < n; k++) + { + float t[4][8], *x, *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + float x0 = y[i*18]; + float x1 = y[(15 - i)*18]; + float x2 = y[(16 + i)*18]; + float x3 = y[(31 - i)*18]; + float t0 = x0 + x3; + float t1 = x1 + x2; + float t2 = (x1 - x2)*g_sec[3*i + 0]; + float t3 = (x0 - x3)*g_sec[3*i + 1]; + x[0] = t0 + t1; + x[8] = (t0 - t1)*g_sec[3*i + 2]; + x[16] = t3 + t2; + x[24] = (t3 - t2)*g_sec[3*i + 2]; + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = x0 - x7; x0 += x7; + x7 = x1 - x6; x1 += x6; + x6 = x2 - x5; x2 += x5; + x5 = x3 - x4; x3 += x4; + x4 = x0 - x3; x0 += x3; + x3 = x1 - x2; x1 += x2; + x[0] = x0 + x1; + x[4] = (x0 - x1)*0.70710677f; + x5 = x5 + x6; + x6 = (x6 + x7)*0.70710677f; + x7 = x7 + xt; + x3 = (x3 + x4)*0.70710677f; + x5 -= x7*0.198912367f; /* rotate by PI/8 */ + x7 += x5*0.382683432f; + x5 -= x7*0.198912367f; + x0 = xt - x6; xt += x6; + x[1] = (xt + x7)*0.50979561f; + x[2] = (x4 + x3)*0.54119611f; + x[3] = (x0 - x5)*0.60134488f; + x[5] = (x0 + x5)*0.89997619f; + x[6] = (x4 - x3)*1.30656302f; + x[7] = (xt - x7)*2.56291556f; + + } + for (i = 0; i < 7; i++, y += 4*18) + { + y[0*18] = t[0][i]; + y[1*18] = t[2][i] + t[3][i] + t[3][i + 1]; + y[2*18] = t[1][i] + t[1][i + 1]; + y[3*18] = t[2][i + 1] + t[3][i] + t[3][i + 1]; + } + y[0*18] = t[0][7]; + y[1*18] = t[2][7] + t[3][7]; + y[2*18] = t[1][7]; + y[3*18] = t[3][7]; + } +#endif +} + +#ifndef DR_MP3_FLOAT_OUTPUT +typedef drmp3_int16 drmp3d_sample_t; + +static drmp3_int16 drmp3d_scale_pcm(float sample) +{ + drmp3_int16 s; +#if DRMP3_HAVE_ARMV6 + drmp3_int32 s32 = (drmp3_int32)(sample + .5f); + s32 -= (s32 < 0); + s = (drmp3_int16)drmp3_clip_int16_arm(s32); +#else + if (sample >= 32766.5f) return (drmp3_int16) 32767; + if (sample <= -32767.5f) return (drmp3_int16)-32768; + s = (drmp3_int16)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ +#endif + return s; +} +#else +typedef float drmp3d_sample_t; + +static float drmp3d_scale_pcm(float sample) +{ + return sample*(1.f/32768.f); +} +#endif + +static void drmp3d_synth_pair(drmp3d_sample_t *pcm, int nch, const float *z) +{ + float a; + a = (z[14*64] - z[ 0]) * 29; + a += (z[ 1*64] + z[13*64]) * 213; + a += (z[12*64] - z[ 2*64]) * 459; + a += (z[ 3*64] + z[11*64]) * 2037; + a += (z[10*64] - z[ 4*64]) * 5153; + a += (z[ 5*64] + z[ 9*64]) * 6574; + a += (z[ 8*64] - z[ 6*64]) * 37489; + a += z[ 7*64] * 75038; + pcm[0] = drmp3d_scale_pcm(a); + + z += 2; + a = z[14*64] * 104; + a += z[12*64] * 1567; + a += z[10*64] * 9727; + a += z[ 8*64] * 64019; + a += z[ 6*64] * -9975; + a += z[ 4*64] * -45; + a += z[ 2*64] * 146; + a += z[ 0*64] * -5; + pcm[16*nch] = drmp3d_scale_pcm(a); +} + +static void drmp3d_synth(float *xl, drmp3d_sample_t *dstl, int nch, float *lins) +{ + int i; + float *xr = xl + 576*(nch - 1); + drmp3d_sample_t *dstr = dstl + (nch - 1); + + static const float g_win[] = { + -1,26,-31,208,218,401,-519,2063,2000,4788,-5517,7134,5959,35640,-39336,74992, + -1,24,-35,202,222,347,-581,2080,1952,4425,-5879,7640,5288,33791,-41176,74856, + -1,21,-38,196,225,294,-645,2087,1893,4063,-6237,8092,4561,31947,-43006,74630, + -1,19,-41,190,227,244,-711,2085,1822,3705,-6589,8492,3776,30112,-44821,74313, + -1,17,-45,183,228,197,-779,2075,1739,3351,-6935,8840,2935,28289,-46617,73908, + -1,16,-49,176,228,153,-848,2057,1644,3004,-7271,9139,2037,26482,-48390,73415, + -2,14,-53,169,227,111,-919,2032,1535,2663,-7597,9389,1082,24694,-50137,72835, + -2,13,-58,161,224,72,-991,2001,1414,2330,-7910,9592,70,22929,-51853,72169, + -2,11,-63,154,221,36,-1064,1962,1280,2006,-8209,9750,-998,21189,-53534,71420, + -2,10,-68,147,215,2,-1137,1919,1131,1692,-8491,9863,-2122,19478,-55178,70590, + -3,9,-73,139,208,-29,-1210,1870,970,1388,-8755,9935,-3300,17799,-56778,69679, + -3,8,-79,132,200,-57,-1283,1817,794,1095,-8998,9966,-4533,16155,-58333,68692, + -4,7,-85,125,189,-83,-1356,1759,605,814,-9219,9959,-5818,14548,-59838,67629, + -4,7,-91,117,177,-106,-1428,1698,402,545,-9416,9916,-7154,12980,-61289,66494, + -5,6,-97,111,163,-127,-1498,1634,185,288,-9585,9838,-8540,11455,-62684,65290 + }; + float *zlin = lins + 15*64; + const float *w = g_win; + + zlin[4*15] = xl[18*16]; + zlin[4*15 + 1] = xr[18*16]; + zlin[4*15 + 2] = xl[0]; + zlin[4*15 + 3] = xr[0]; + + zlin[4*31] = xl[1 + 18*16]; + zlin[4*31 + 1] = xr[1 + 18*16]; + zlin[4*31 + 2] = xl[1]; + zlin[4*31 + 3] = xr[1]; + + drmp3d_synth_pair(dstr, nch, lins + 4*15 + 1); + drmp3d_synth_pair(dstr + 32*nch, nch, lins + 4*15 + 64 + 1); + drmp3d_synth_pair(dstl, nch, lins + 4*15); + drmp3d_synth_pair(dstl + 32*nch, nch, lins + 4*15 + 64); + +#if DRMP3_HAVE_SIMD + if (drmp3_have_simd()) for (i = 14; i >= 0; i--) + { +#define DRMP3_VLOAD(k) drmp3_f4 w0 = DRMP3_VSET(*w++); drmp3_f4 w1 = DRMP3_VSET(*w++); drmp3_f4 vz = DRMP3_VLD(&zlin[4*i - 64*k]); drmp3_f4 vy = DRMP3_VLD(&zlin[4*i - 64*(15 - k)]); +#define DRMP3_V0(k) { DRMP3_VLOAD(k) b = DRMP3_VADD(DRMP3_VMUL(vz, w1), DRMP3_VMUL(vy, w0)) ; a = DRMP3_VSUB(DRMP3_VMUL(vz, w0), DRMP3_VMUL(vy, w1)); } +#define DRMP3_V1(k) { DRMP3_VLOAD(k) b = DRMP3_VADD(b, DRMP3_VADD(DRMP3_VMUL(vz, w1), DRMP3_VMUL(vy, w0))); a = DRMP3_VADD(a, DRMP3_VSUB(DRMP3_VMUL(vz, w0), DRMP3_VMUL(vy, w1))); } +#define DRMP3_V2(k) { DRMP3_VLOAD(k) b = DRMP3_VADD(b, DRMP3_VADD(DRMP3_VMUL(vz, w1), DRMP3_VMUL(vy, w0))); a = DRMP3_VADD(a, DRMP3_VSUB(DRMP3_VMUL(vy, w1), DRMP3_VMUL(vz, w0))); } + drmp3_f4 a, b; + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*i + 64] = xl[1 + 18*(1 + i)]; + zlin[4*i + 64 + 1] = xr[1 + 18*(1 + i)]; + zlin[4*i - 64 + 2] = xl[18*(1 + i)]; + zlin[4*i - 64 + 3] = xr[18*(1 + i)]; + + DRMP3_V0(0) DRMP3_V2(1) DRMP3_V1(2) DRMP3_V2(3) DRMP3_V1(4) DRMP3_V2(5) DRMP3_V1(6) DRMP3_V2(7) + + { +#ifndef DR_MP3_FLOAT_OUTPUT +#if DRMP3_HAVE_SSE + static const drmp3_f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const drmp3_f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + dstr[(15 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 1); + dstr[(17 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 5); + dstl[(15 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 0); + dstl[(17 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 4); + dstr[(47 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 3); + dstr[(49 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 7); + dstl[(47 - i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 2); + dstl[(49 + i)*nch] = (drmp3_int16)_mm_extract_epi16(pcm8, 6); +#else + int16x4_t pcma, pcmb; + a = DRMP3_VADD(a, DRMP3_VSET(0.5f)); + b = DRMP3_VADD(b, DRMP3_VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, DRMP3_VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, DRMP3_VSET(0))))); + vst1_lane_s16(dstr + (15 - i)*nch, pcma, 1); + vst1_lane_s16(dstr + (17 + i)*nch, pcmb, 1); + vst1_lane_s16(dstl + (15 - i)*nch, pcma, 0); + vst1_lane_s16(dstl + (17 + i)*nch, pcmb, 0); + vst1_lane_s16(dstr + (47 - i)*nch, pcma, 3); + vst1_lane_s16(dstr + (49 + i)*nch, pcmb, 3); + vst1_lane_s16(dstl + (47 - i)*nch, pcma, 2); + vst1_lane_s16(dstl + (49 + i)*nch, pcmb, 2); +#endif +#else + #if DRMP3_HAVE_SSE + static const drmp3_f4 g_scale = { 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f }; + #else + const drmp3_f4 g_scale = vdupq_n_f32(1.0f/32768.0f); + #endif + a = DRMP3_VMUL(a, g_scale); + b = DRMP3_VMUL(b, g_scale); +#if DRMP3_HAVE_SSE + _mm_store_ss(dstr + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstr + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstl + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstl + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstr + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstr + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstl + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2))); + _mm_store_ss(dstl + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2))); +#else + vst1q_lane_f32(dstr + (15 - i)*nch, a, 1); + vst1q_lane_f32(dstr + (17 + i)*nch, b, 1); + vst1q_lane_f32(dstl + (15 - i)*nch, a, 0); + vst1q_lane_f32(dstl + (17 + i)*nch, b, 0); + vst1q_lane_f32(dstr + (47 - i)*nch, a, 3); + vst1q_lane_f32(dstr + (49 + i)*nch, b, 3); + vst1q_lane_f32(dstl + (47 - i)*nch, a, 2); + vst1q_lane_f32(dstl + (49 + i)*nch, b, 2); +#endif +#endif /* DR_MP3_FLOAT_OUTPUT */ + } + } else +#endif +#ifdef DR_MP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else + for (i = 14; i >= 0; i--) + { +#define DRMP3_LOAD(k) float w0 = *w++; float w1 = *w++; float *vz = &zlin[4*i - k*64]; float *vy = &zlin[4*i - (15 - k)*64]; +#define DRMP3_S0(k) { int j; DRMP3_LOAD(k); for (j = 0; j < 4; j++) b[j] = vz[j]*w1 + vy[j]*w0, a[j] = vz[j]*w0 - vy[j]*w1; } +#define DRMP3_S1(k) { int j; DRMP3_LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vz[j]*w0 - vy[j]*w1; } +#define DRMP3_S2(k) { int j; DRMP3_LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vy[j]*w1 - vz[j]*w0; } + float a[4], b[4]; + + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*(i + 16)] = xl[1 + 18*(1 + i)]; + zlin[4*(i + 16) + 1] = xr[1 + 18*(1 + i)]; + zlin[4*(i - 16) + 2] = xl[18*(1 + i)]; + zlin[4*(i - 16) + 3] = xr[18*(1 + i)]; + + DRMP3_S0(0) DRMP3_S2(1) DRMP3_S1(2) DRMP3_S2(3) DRMP3_S1(4) DRMP3_S2(5) DRMP3_S1(6) DRMP3_S2(7) + + dstr[(15 - i)*nch] = drmp3d_scale_pcm(a[1]); + dstr[(17 + i)*nch] = drmp3d_scale_pcm(b[1]); + dstl[(15 - i)*nch] = drmp3d_scale_pcm(a[0]); + dstl[(17 + i)*nch] = drmp3d_scale_pcm(b[0]); + dstr[(47 - i)*nch] = drmp3d_scale_pcm(a[3]); + dstr[(49 + i)*nch] = drmp3d_scale_pcm(b[3]); + dstl[(47 - i)*nch] = drmp3d_scale_pcm(a[2]); + dstl[(49 + i)*nch] = drmp3d_scale_pcm(b[2]); + } +#endif +} + +static void drmp3d_synth_granule(float *qmf_state, float *grbuf, int nbands, int nch, drmp3d_sample_t *pcm, float *lins) +{ + int i; + for (i = 0; i < nch; i++) + { + drmp3d_DCT_II(grbuf + 576*i, nbands); + } + + DRMP3_COPY_MEMORY(lins, qmf_state, sizeof(float)*15*64); + + for (i = 0; i < nbands; i += 2) + { + drmp3d_synth(grbuf + i, pcm + 32*nch*i, nch, lins + i*64); + } +#ifndef DR_MP3_NONSTANDARD_BUT_LOGICAL + if (nch == 1) + { + for (i = 0; i < 15*64; i += 2) + { + qmf_state[i] = lins[nbands*64 + i]; + } + } else +#endif + { + DRMP3_COPY_MEMORY(qmf_state, lins + nbands*64, sizeof(float)*15*64); + } +} + +static int drmp3d_match_frame(const drmp3_uint8 *hdr, int mp3_bytes, int frame_bytes) +{ + int i, nmatch; + for (i = 0, nmatch = 0; nmatch < DRMP3_MAX_FRAME_SYNC_MATCHES; nmatch++) + { + i += drmp3_hdr_frame_bytes(hdr + i, frame_bytes) + drmp3_hdr_padding(hdr + i); + if (i + DRMP3_HDR_SIZE > mp3_bytes) + return nmatch > 0; + if (!drmp3_hdr_compare(hdr, hdr + i)) + return 0; + } + return 1; +} + +static int drmp3d_find_frame(const drmp3_uint8 *mp3, int mp3_bytes, int *free_format_bytes, int *ptr_frame_bytes) +{ + int i, k; + for (i = 0; i < mp3_bytes - DRMP3_HDR_SIZE; i++, mp3++) + { + if (drmp3_hdr_valid(mp3)) + { + int frame_bytes = drmp3_hdr_frame_bytes(mp3, *free_format_bytes); + int frame_and_padding = frame_bytes + drmp3_hdr_padding(mp3); + + for (k = DRMP3_HDR_SIZE; !frame_bytes && k < DRMP3_MAX_FREE_FORMAT_FRAME_SIZE && i + 2*k < mp3_bytes - DRMP3_HDR_SIZE; k++) + { + if (drmp3_hdr_compare(mp3, mp3 + k)) + { + int fb = k - drmp3_hdr_padding(mp3); + int nextfb = fb + drmp3_hdr_padding(mp3 + k); + if (i + k + nextfb + DRMP3_HDR_SIZE > mp3_bytes || !drmp3_hdr_compare(mp3, mp3 + k + nextfb)) + continue; + frame_and_padding = k; + frame_bytes = fb; + *free_format_bytes = fb; + } + } + + if ((frame_bytes && i + frame_and_padding <= mp3_bytes && + drmp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) || + (!i && frame_and_padding == mp3_bytes)) + { + *ptr_frame_bytes = frame_and_padding; + return i; + } + *free_format_bytes = 0; + } + } + *ptr_frame_bytes = 0; + return mp3_bytes; +} + +DRMP3_API void drmp3dec_init(drmp3dec *dec) +{ + dec->header[0] = 0; +} + +DRMP3_API int drmp3dec_decode_frame(drmp3dec *dec, const drmp3_uint8 *mp3, int mp3_bytes, void *pcm, drmp3dec_frame_info *info) +{ + int i = 0, igr, frame_size = 0, success = 1; + const drmp3_uint8 *hdr; + drmp3_bs bs_frame[1]; + + if (mp3_bytes > 4 && dec->header[0] == 0xff && drmp3_hdr_compare(dec->header, mp3)) + { + frame_size = drmp3_hdr_frame_bytes(mp3, dec->free_format_bytes) + drmp3_hdr_padding(mp3); + if (frame_size != mp3_bytes && (frame_size + DRMP3_HDR_SIZE > mp3_bytes || !drmp3_hdr_compare(mp3, mp3 + frame_size))) + { + frame_size = 0; + } + } + if (!frame_size) + { + DRMP3_ZERO_MEMORY(dec, sizeof(drmp3dec)); + i = drmp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes, &frame_size); + if (!frame_size || i + frame_size > mp3_bytes) + { + info->frame_bytes = i; + return 0; + } + } + + hdr = mp3 + i; + DRMP3_COPY_MEMORY(dec->header, hdr, DRMP3_HDR_SIZE); + info->frame_bytes = i + frame_size; + info->channels = DRMP3_HDR_IS_MONO(hdr) ? 1 : 2; + info->sample_rate = drmp3_hdr_sample_rate_hz(hdr); + info->layer = 4 - DRMP3_HDR_GET_LAYER(hdr); + info->bitrate_kbps = drmp3_hdr_bitrate_kbps(hdr); + + drmp3_bs_init(bs_frame, hdr + DRMP3_HDR_SIZE, frame_size - DRMP3_HDR_SIZE); + if (DRMP3_HDR_IS_CRC(hdr)) + { + drmp3_bs_get_bits(bs_frame, 16); + } + + if (info->layer == 3) + { + int main_data_begin = drmp3_L3_read_side_info(bs_frame, dec->scratch.gr_info, hdr); + if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) + { + drmp3dec_init(dec); + return 0; + } + success = drmp3_L3_restore_reservoir(dec, bs_frame, &dec->scratch, main_data_begin); + if (success && pcm != NULL) + { + for (igr = 0; igr < (DRMP3_HDR_TEST_MPEG1(hdr) ? 2 : 1); igr++, pcm = DRMP3_OFFSET_PTR(pcm, sizeof(drmp3d_sample_t)*576*info->channels)) + { + DRMP3_ZERO_MEMORY(dec->scratch.grbuf[0], 576*2*sizeof(float)); + drmp3_L3_decode(dec, &dec->scratch, dec->scratch.gr_info + igr*info->channels, info->channels); + drmp3d_synth_granule(dec->qmf_state, dec->scratch.grbuf[0], 18, info->channels, (drmp3d_sample_t*)pcm, dec->scratch.syn[0]); + } + } + drmp3_L3_save_reservoir(dec, &dec->scratch); + } else + { +#ifdef DR_MP3_ONLY_MP3 + return 0; +#else + drmp3_L12_scale_info sci[1]; + + if (pcm == NULL) { + return drmp3_hdr_frame_samples(hdr); + } + + drmp3_L12_read_scale_info(hdr, bs_frame, sci); + + DRMP3_ZERO_MEMORY(dec->scratch.grbuf[0], 576*2*sizeof(float)); + for (i = 0, igr = 0; igr < 3; igr++) + { + if (12 == (i += drmp3_L12_dequantize_granule(dec->scratch.grbuf[0] + i, bs_frame, sci, info->layer | 1))) + { + i = 0; + drmp3_L12_apply_scf_384(sci, sci->scf + igr, dec->scratch.grbuf[0]); + drmp3d_synth_granule(dec->qmf_state, dec->scratch.grbuf[0], 12, info->channels, (drmp3d_sample_t*)pcm, dec->scratch.syn[0]); + DRMP3_ZERO_MEMORY(dec->scratch.grbuf[0], 576*2*sizeof(float)); + pcm = DRMP3_OFFSET_PTR(pcm, sizeof(drmp3d_sample_t)*384*info->channels); + } + if (bs_frame->pos > bs_frame->limit) + { + drmp3dec_init(dec); + return 0; + } + } +#endif + } + + return success*drmp3_hdr_frame_samples(dec->header); +} + +DRMP3_API void drmp3dec_f32_to_s16(const float *in, drmp3_int16 *out, size_t num_samples) +{ + size_t i = 0; +#if DRMP3_HAVE_SIMD + size_t aligned_count = num_samples & ~7; + for(; i < aligned_count; i+=8) + { + drmp3_f4 scale = DRMP3_VSET(32768.0f); + drmp3_f4 a = DRMP3_VMUL(DRMP3_VLD(&in[i ]), scale); + drmp3_f4 b = DRMP3_VMUL(DRMP3_VLD(&in[i+4]), scale); +#if DRMP3_HAVE_SSE + drmp3_f4 s16max = DRMP3_VSET( 32767.0f); + drmp3_f4 s16min = DRMP3_VSET(-32768.0f); + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, s16max), s16min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, s16max), s16min))); + out[i ] = (drmp3_int16)_mm_extract_epi16(pcm8, 0); + out[i+1] = (drmp3_int16)_mm_extract_epi16(pcm8, 1); + out[i+2] = (drmp3_int16)_mm_extract_epi16(pcm8, 2); + out[i+3] = (drmp3_int16)_mm_extract_epi16(pcm8, 3); + out[i+4] = (drmp3_int16)_mm_extract_epi16(pcm8, 4); + out[i+5] = (drmp3_int16)_mm_extract_epi16(pcm8, 5); + out[i+6] = (drmp3_int16)_mm_extract_epi16(pcm8, 6); + out[i+7] = (drmp3_int16)_mm_extract_epi16(pcm8, 7); +#else + int16x4_t pcma, pcmb; + a = DRMP3_VADD(a, DRMP3_VSET(0.5f)); + b = DRMP3_VADD(b, DRMP3_VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, DRMP3_VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, DRMP3_VSET(0))))); + vst1_lane_s16(out+i , pcma, 0); + vst1_lane_s16(out+i+1, pcma, 1); + vst1_lane_s16(out+i+2, pcma, 2); + vst1_lane_s16(out+i+3, pcma, 3); + vst1_lane_s16(out+i+4, pcmb, 0); + vst1_lane_s16(out+i+5, pcmb, 1); + vst1_lane_s16(out+i+6, pcmb, 2); + vst1_lane_s16(out+i+7, pcmb, 3); +#endif + } +#endif + for(; i < num_samples; i++) + { + float sample = in[i] * 32768.0f; + if (sample >= 32766.5f) + out[i] = (drmp3_int16) 32767; + else if (sample <= -32767.5f) + out[i] = (drmp3_int16)-32768; + else + { + short s = (drmp3_int16)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ + out[i] = s; + } + } +} + + + +/************************************************************************************************************************************************************ + + Main Public API + + ************************************************************************************************************************************************************/ +/* SIZE_MAX */ +#if defined(SIZE_MAX) + #define DRMP3_SIZE_MAX SIZE_MAX +#else + #if defined(_WIN64) || defined(_LP64) || defined(__LP64__) + #define DRMP3_SIZE_MAX ((drmp3_uint64)0xFFFFFFFFFFFFFFFF) + #else + #define DRMP3_SIZE_MAX 0xFFFFFFFF + #endif +#endif +/* End SIZE_MAX */ + +/* Options. */ +#ifndef DRMP3_SEEK_LEADING_MP3_FRAMES +#define DRMP3_SEEK_LEADING_MP3_FRAMES 2 +#endif + +#define DRMP3_MIN_DATA_CHUNK_SIZE 16384 + +/* The size in bytes of each chunk of data to read from the MP3 stream. minimp3 recommends at least 16K, but in an attempt to reduce data movement I'm making this slightly larger. */ +#ifndef DRMP3_DATA_CHUNK_SIZE +#define DRMP3_DATA_CHUNK_SIZE (DRMP3_MIN_DATA_CHUNK_SIZE*4) +#endif + + +#define DRMP3_COUNTOF(x) (sizeof(x) / sizeof(x[0])) +#define DRMP3_CLAMP(x, lo, hi) (DRMP3_MAX(lo, DRMP3_MIN(x, hi))) + +#ifndef DRMP3_PI_D +#define DRMP3_PI_D 3.14159265358979323846264 +#endif + +#define DRMP3_DEFAULT_RESAMPLER_LPF_ORDER 2 + +static DRMP3_INLINE float drmp3_mix_f32(float x, float y, float a) +{ + return x*(1-a) + y*a; +} +static DRMP3_INLINE float drmp3_mix_f32_fast(float x, float y, float a) +{ + float r0 = (y - x); + float r1 = r0*a; + return x + r1; + /*return x + (y - x)*a;*/ +} + + +/* +Greatest common factor using Euclid's algorithm iteratively. +*/ +static DRMP3_INLINE drmp3_uint32 drmp3_gcf_u32(drmp3_uint32 a, drmp3_uint32 b) +{ + for (;;) { + if (b == 0) { + break; + } else { + drmp3_uint32 t = a; + a = b; + b = t % a; + } + } + + return a; +} + + +static void* drmp3__malloc_default(size_t sz, void* pUserData) +{ + (void)pUserData; + return DRMP3_MALLOC(sz); +} + +static void* drmp3__realloc_default(void* p, size_t sz, void* pUserData) +{ + (void)pUserData; + return DRMP3_REALLOC(p, sz); +} + +static void drmp3__free_default(void* p, void* pUserData) +{ + (void)pUserData; + DRMP3_FREE(p); +} + + +static void* drmp3__malloc_from_callbacks(size_t sz, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onMalloc != NULL) { + return pAllocationCallbacks->onMalloc(sz, pAllocationCallbacks->pUserData); + } + + /* Try using realloc(). */ + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(NULL, sz, pAllocationCallbacks->pUserData); + } + + return NULL; +} + +static void* drmp3__realloc_from_callbacks(void* p, size_t szNew, size_t szOld, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks == NULL) { + return NULL; + } + + if (pAllocationCallbacks->onRealloc != NULL) { + return pAllocationCallbacks->onRealloc(p, szNew, pAllocationCallbacks->pUserData); + } + + /* Try emulating realloc() in terms of malloc()/free(). */ + if (pAllocationCallbacks->onMalloc != NULL && pAllocationCallbacks->onFree != NULL) { + void* p2; + + p2 = pAllocationCallbacks->onMalloc(szNew, pAllocationCallbacks->pUserData); + if (p2 == NULL) { + return NULL; + } + + if (p != NULL) { + DRMP3_COPY_MEMORY(p2, p, szOld); + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } + + return p2; + } + + return NULL; +} + +static void drmp3__free_from_callbacks(void* p, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (p == NULL || pAllocationCallbacks == NULL) { + return; + } + + if (pAllocationCallbacks->onFree != NULL) { + pAllocationCallbacks->onFree(p, pAllocationCallbacks->pUserData); + } +} + + +static drmp3_allocation_callbacks drmp3_copy_allocation_callbacks_or_defaults(const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + /* Copy. */ + return *pAllocationCallbacks; + } else { + /* Defaults. */ + drmp3_allocation_callbacks allocationCallbacks; + allocationCallbacks.pUserData = NULL; + allocationCallbacks.onMalloc = drmp3__malloc_default; + allocationCallbacks.onRealloc = drmp3__realloc_default; + allocationCallbacks.onFree = drmp3__free_default; + return allocationCallbacks; + } +} + + + +static size_t drmp3__on_read(drmp3* pMP3, void* pBufferOut, size_t bytesToRead) +{ + size_t bytesRead; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + /* + Don't try reading 0 bytes from the callback. This can happen when the stream is clamped against + ID3v1 or APE tags at the end of the stream. + */ + if (bytesToRead == 0) { + return 0; + } + + bytesRead = pMP3->onRead(pMP3->pUserData, pBufferOut, bytesToRead); + pMP3->streamCursor += bytesRead; + + return bytesRead; +} + +static size_t drmp3__on_read_clamped(drmp3* pMP3, void* pBufferOut, size_t bytesToRead) +{ + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + if (pMP3->streamLength == DRMP3_UINT64_MAX) { + return drmp3__on_read(pMP3, pBufferOut, bytesToRead); + } else { + drmp3_uint64 bytesRemaining; + + bytesRemaining = (pMP3->streamLength - pMP3->streamCursor); + if (bytesToRead > bytesRemaining) { + bytesToRead = (size_t)bytesRemaining; + } + + return drmp3__on_read(pMP3, pBufferOut, bytesToRead); + } +} + +static drmp3_bool32 drmp3__on_seek(drmp3* pMP3, int offset, drmp3_seek_origin origin) +{ + DRMP3_ASSERT(offset >= 0); + DRMP3_ASSERT(origin == DRMP3_SEEK_SET || origin == DRMP3_SEEK_CUR); + + if (!pMP3->onSeek(pMP3->pUserData, offset, origin)) { + return DRMP3_FALSE; + } + + if (origin == DRMP3_SEEK_SET) { + pMP3->streamCursor = (drmp3_uint64)offset; + } else{ + pMP3->streamCursor += offset; + } + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3__on_seek_64(drmp3* pMP3, drmp3_uint64 offset, drmp3_seek_origin origin) +{ + if (offset <= 0x7FFFFFFF) { + return drmp3__on_seek(pMP3, (int)offset, origin); + } + + /* Getting here "offset" is too large for a 32-bit integer. We just keep seeking forward until we hit the offset. */ + if (!drmp3__on_seek(pMP3, 0x7FFFFFFF, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; + } + + offset -= 0x7FFFFFFF; + while (offset > 0) { + if (offset <= 0x7FFFFFFF) { + if (!drmp3__on_seek(pMP3, (int)offset, DRMP3_SEEK_CUR)) { + return DRMP3_FALSE; + } + offset = 0; + } else { + if (!drmp3__on_seek(pMP3, 0x7FFFFFFF, DRMP3_SEEK_CUR)) { + return DRMP3_FALSE; + } + offset -= 0x7FFFFFFF; + } + } + + return DRMP3_TRUE; +} + +static void drmp3__on_meta(drmp3* pMP3, drmp3_metadata_type type, const void* pRawData, size_t rawDataSize) +{ + if (pMP3->onMeta) { + drmp3_metadata metadata; + + DRMP3_ZERO_OBJECT(&metadata); + metadata.type = type; + metadata.pRawData = pRawData; + metadata.rawDataSize = rawDataSize; + + pMP3->onMeta(pMP3->pUserDataMeta, &metadata); + } +} + + +static drmp3_uint32 drmp3_decode_next_frame_ex__callbacks(drmp3* pMP3, drmp3d_sample_t* pPCMFrames, drmp3dec_frame_info* pMP3FrameInfo, const drmp3_uint8** ppMP3FrameData) +{ + drmp3_uint32 pcmFramesRead = 0; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + if (pMP3->atEnd) { + return 0; + } + + for (;;) { + drmp3dec_frame_info info; + + /* minimp3 recommends doing data submission in chunks of at least 16K. If we don't have at least 16K bytes available, get more. */ + if (pMP3->dataSize < DRMP3_MIN_DATA_CHUNK_SIZE) { + size_t bytesRead; + + /* First we need to move the data down. */ + if (pMP3->pData != NULL) { + DRMP3_MOVE_MEMORY(pMP3->pData, pMP3->pData + pMP3->dataConsumed, pMP3->dataSize); + } + + pMP3->dataConsumed = 0; + + if (pMP3->dataCapacity < DRMP3_DATA_CHUNK_SIZE) { + drmp3_uint8* pNewData; + size_t newDataCap; + + newDataCap = DRMP3_DATA_CHUNK_SIZE; + + pNewData = (drmp3_uint8*)drmp3__realloc_from_callbacks(pMP3->pData, newDataCap, pMP3->dataCapacity, &pMP3->allocationCallbacks); + if (pNewData == NULL) { + return 0; /* Out of memory. */ + } + + pMP3->pData = pNewData; + pMP3->dataCapacity = newDataCap; + } + + bytesRead = drmp3__on_read_clamped(pMP3, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); + if (bytesRead == 0) { + if (pMP3->dataSize == 0) { + pMP3->atEnd = DRMP3_TRUE; + return 0; /* No data. */ + } + } + + pMP3->dataSize += bytesRead; + } + + if (pMP3->dataSize > INT_MAX) { + pMP3->atEnd = DRMP3_TRUE; + return 0; /* File too big. */ + } + + DRMP3_ASSERT(pMP3->pData != NULL); + DRMP3_ASSERT(pMP3->dataCapacity > 0); + + /* Do a runtime check here to try silencing a false-positive from clang-analyzer. */ + if (pMP3->pData == NULL) { + return 0; + } + + pcmFramesRead = drmp3dec_decode_frame(&pMP3->decoder, pMP3->pData + pMP3->dataConsumed, (int)pMP3->dataSize, pPCMFrames, &info); /* <-- Safe size_t -> int conversion thanks to the check above. */ + + /* Consume the data. */ + pMP3->dataConsumed += (size_t)info.frame_bytes; + pMP3->dataSize -= (size_t)info.frame_bytes; + + /* pcmFramesRead will be equal to 0 if decoding failed. If it is zero and info.frame_bytes > 0 then we have successfully decoded the frame. */ + if (pcmFramesRead > 0) { + pcmFramesRead = drmp3_hdr_frame_samples(pMP3->decoder.header); + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = pcmFramesRead; + pMP3->mp3FrameChannels = info.channels; + pMP3->mp3FrameSampleRate = info.sample_rate; + + if (pMP3FrameInfo != NULL) { + *pMP3FrameInfo = info; + } + + if (ppMP3FrameData != NULL) { + *ppMP3FrameData = pMP3->pData + pMP3->dataConsumed - (size_t)info.frame_bytes; + } + + break; + } else if (info.frame_bytes == 0) { + /* Need more data. minimp3 recommends doing data submission in 16K chunks. */ + size_t bytesRead; + + /* First we need to move the data down. */ + DRMP3_MOVE_MEMORY(pMP3->pData, pMP3->pData + pMP3->dataConsumed, pMP3->dataSize); + pMP3->dataConsumed = 0; + + if (pMP3->dataCapacity == pMP3->dataSize) { + /* No room. Expand. */ + drmp3_uint8* pNewData; + size_t newDataCap; + + newDataCap = pMP3->dataCapacity + DRMP3_DATA_CHUNK_SIZE; + + pNewData = (drmp3_uint8*)drmp3__realloc_from_callbacks(pMP3->pData, newDataCap, pMP3->dataCapacity, &pMP3->allocationCallbacks); + if (pNewData == NULL) { + return 0; /* Out of memory. */ + } + + pMP3->pData = pNewData; + pMP3->dataCapacity = newDataCap; + } + + /* Fill in a chunk. */ + bytesRead = drmp3__on_read_clamped(pMP3, pMP3->pData + pMP3->dataSize, (pMP3->dataCapacity - pMP3->dataSize)); + if (bytesRead == 0) { + pMP3->atEnd = DRMP3_TRUE; + return 0; /* Error reading more data. */ + } + + pMP3->dataSize += bytesRead; + } + }; + + return pcmFramesRead; +} + +static drmp3_uint32 drmp3_decode_next_frame_ex__memory(drmp3* pMP3, drmp3d_sample_t* pPCMFrames, drmp3dec_frame_info* pMP3FrameInfo, const drmp3_uint8** ppMP3FrameData) +{ + drmp3_uint32 pcmFramesRead = 0; + drmp3dec_frame_info info; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->memory.pData != NULL); + + if (pMP3->atEnd) { + return 0; + } + + for (;;) { + pcmFramesRead = drmp3dec_decode_frame(&pMP3->decoder, pMP3->memory.pData + pMP3->memory.currentReadPos, (int)(pMP3->memory.dataSize - pMP3->memory.currentReadPos), pPCMFrames, &info); + if (pcmFramesRead > 0) { + pcmFramesRead = drmp3_hdr_frame_samples(pMP3->decoder.header); + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = pcmFramesRead; + pMP3->mp3FrameChannels = info.channels; + pMP3->mp3FrameSampleRate = info.sample_rate; + + if (pMP3FrameInfo != NULL) { + *pMP3FrameInfo = info; + } + + if (ppMP3FrameData != NULL) { + *ppMP3FrameData = pMP3->memory.pData + pMP3->memory.currentReadPos; + } + + break; + } else if (info.frame_bytes > 0) { + /* No frames were read, but it looks like we skipped past one. Read the next MP3 frame. */ + pMP3->memory.currentReadPos += (size_t)info.frame_bytes; + pMP3->streamCursor += (size_t)info.frame_bytes; + } else { + /* Nothing at all was read. Abort. */ + break; + } + } + + /* Consume the data. */ + pMP3->memory.currentReadPos += (size_t)info.frame_bytes; + pMP3->streamCursor += (size_t)info.frame_bytes; + + return pcmFramesRead; +} + +static drmp3_uint32 drmp3_decode_next_frame_ex(drmp3* pMP3, drmp3d_sample_t* pPCMFrames, drmp3dec_frame_info* pMP3FrameInfo, const drmp3_uint8** ppMP3FrameData) +{ + if (pMP3->memory.pData != NULL && pMP3->memory.dataSize > 0) { + return drmp3_decode_next_frame_ex__memory(pMP3, pPCMFrames, pMP3FrameInfo, ppMP3FrameData); + } else { + return drmp3_decode_next_frame_ex__callbacks(pMP3, pPCMFrames, pMP3FrameInfo, ppMP3FrameData); + } +} + +static drmp3_uint32 drmp3_decode_next_frame(drmp3* pMP3) +{ + DRMP3_ASSERT(pMP3 != NULL); + return drmp3_decode_next_frame_ex(pMP3, (drmp3d_sample_t*)pMP3->pcmFrames, NULL, NULL); +} + +#if 0 +static drmp3_uint32 drmp3_seek_next_frame(drmp3* pMP3) +{ + drmp3_uint32 pcmFrameCount; + + DRMP3_ASSERT(pMP3 != NULL); + + pcmFrameCount = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFrameCount == 0) { + return 0; + } + + /* We have essentially just skipped past the frame, so just set the remaining samples to 0. */ + pMP3->currentPCMFrame += pcmFrameCount; + pMP3->pcmFramesConsumedInMP3Frame = pcmFrameCount; + pMP3->pcmFramesRemainingInMP3Frame = 0; + + return pcmFrameCount; +} +#endif + +static drmp3_bool32 drmp3_init_internal(drmp3* pMP3, drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, drmp3_meta_proc onMeta, void* pUserData, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3dec_frame_info firstFrameInfo; + const drmp3_uint8* pFirstFrameData; + drmp3_uint32 firstFramePCMFrameCount; + drmp3_uint32 detectedMP3FrameCount = 0xFFFFFFFF; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(onRead != NULL); + + /* This function assumes the output object has already been reset to 0. Do not do that here, otherwise things will break. */ + drmp3dec_init(&pMP3->decoder); + + pMP3->onRead = onRead; + pMP3->onSeek = onSeek; + pMP3->onMeta = onMeta; + pMP3->pUserData = pUserData; + pMP3->pUserDataMeta = pUserDataMeta; + pMP3->allocationCallbacks = drmp3_copy_allocation_callbacks_or_defaults(pAllocationCallbacks); + + if (pMP3->allocationCallbacks.onFree == NULL || (pMP3->allocationCallbacks.onMalloc == NULL && pMP3->allocationCallbacks.onRealloc == NULL)) { + return DRMP3_FALSE; /* Invalid allocation callbacks. */ + } + + pMP3->streamCursor = 0; + pMP3->streamLength = DRMP3_UINT64_MAX; + pMP3->streamStartOffset = 0; + pMP3->delayInPCMFrames = 0; + pMP3->paddingInPCMFrames = 0; + pMP3->totalPCMFrameCount = DRMP3_UINT64_MAX; + + /* We'll first check for any ID3v1 or APE tags. */ + #if 1 + if (onSeek != NULL && onTell != NULL) { + if (onSeek(pUserData, 0, DRMP3_SEEK_END)) { + drmp3_int64 streamLen; + int streamEndOffset = 0; + + /* First get the length of the stream. We need this so we can ensure the stream is big enough to store the tags. */ + if (onTell(pUserData, &streamLen)) { + /* ID3v1 */ + if (streamLen > 128) { + char id3[3]; + if (onSeek(pUserData, streamEndOffset - 128, DRMP3_SEEK_END)) { + if (onRead(pUserData, id3, 3) == 3 && id3[0] == 'T' && id3[1] == 'A' && id3[2] == 'G') { + /* We have an ID3v1 tag. */ + streamEndOffset -= 128; + streamLen -= 128; + + /* Fire a metadata callback for the TAG data. */ + if (onMeta != NULL) { + drmp3_uint8 tag[128]; + tag[0] = 'T'; tag[1] = 'A'; tag[2] = 'G'; + + if (onRead(pUserData, tag + 3, 125) == 125) { + drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_ID3V1, tag, 128); + } + } + } else { + /* No ID3v1 tag. */ + } + } else { + /* Failed to seek to the ID3v1 tag. */ + } + } else { + /* Stream too short. No ID3v1 tag. */ + } + + /* APE */ + if (streamLen > 32) { + char ape[32]; /* The footer. */ + if (onSeek(pUserData, streamEndOffset - 32, DRMP3_SEEK_END)) { + if (onRead(pUserData, ape, 32) == 32 && ape[0] == 'A' && ape[1] == 'P' && ape[2] == 'E' && ape[3] == 'T' && ape[4] == 'A' && ape[5] == 'G' && ape[6] == 'E' && ape[7] == 'X') { + /* We have an APE tag. */ + drmp3_uint32 tagSize = + ((drmp3_uint32)ape[24] << 0) | + ((drmp3_uint32)ape[25] << 8) | + ((drmp3_uint32)ape[26] << 16) | + ((drmp3_uint32)ape[27] << 24); + + if (32 + tagSize < streamLen) { + streamEndOffset -= 32 + tagSize; + streamLen -= 32 + tagSize; + + /* Fire a metadata callback for the APE data. Must include both the main content and footer. */ + if (onMeta != NULL) { + /* We first need to seek to the start of the APE tag. */ + if (onSeek(pUserData, streamEndOffset, DRMP3_SEEK_END)) { + size_t apeTagSize = (size_t)tagSize + 32; + drmp3_uint8* pTagData = (drmp3_uint8*)drmp3_malloc(apeTagSize, pAllocationCallbacks); + if (pTagData != NULL) { + if (onRead(pUserData, pTagData, apeTagSize) == apeTagSize) { + drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_APE, pTagData, apeTagSize); + } + + drmp3_free(pTagData, pAllocationCallbacks); + } + } + } + } else { + /* The tag size is larger than the stream. Invalid APE tag. */ + } + } + } + } else { + /* Stream too short. No APE tag. */ + } + + /* Seek back to the start. */ + if (!onSeek(pUserData, 0, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek back to the start. */ + } + + pMP3->streamLength = (drmp3_uint64)streamLen; + + if (pMP3->memory.pData != NULL) { + pMP3->memory.dataSize = (size_t)pMP3->streamLength; + } + } else { + /* Failed to get the length of the stream. ID3v1 and APE tags cannot be skipped. */ + if (!onSeek(pUserData, 0, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek back to the start. */ + } + } + } else { + /* Failed to seek to the end. Cannot skip ID3v1 or APE tags. */ + } + } else { + /* No onSeek or onTell callback. Cannot skip ID3v1 or APE tags. */ + } + #endif + + + /* ID3v2 tags */ + #if 1 + { + char header[10]; + if (onRead(pUserData, header, 10) == 10) { + if (header[0] == 'I' && header[1] == 'D' && header[2] == '3') { + drmp3_uint32 tagSize = + (((drmp3_uint32)header[6] & 0x7F) << 21) | + (((drmp3_uint32)header[7] & 0x7F) << 14) | + (((drmp3_uint32)header[8] & 0x7F) << 7) | + (((drmp3_uint32)header[9] & 0x7F) << 0); + + /* Account for the footer. */ + if (header[5] & 0x10) { + tagSize += 10; + } + + /* Read the tag content and fire a metadata callback. */ + if (onMeta != NULL) { + size_t tagSizeWithHeader = 10 + tagSize; + drmp3_uint8* pTagData = (drmp3_uint8*)drmp3_malloc(tagSizeWithHeader, pAllocationCallbacks); + if (pTagData != NULL) { + DRMP3_COPY_MEMORY(pTagData, header, 10); + + if (onRead(pUserData, pTagData + 10, tagSize) == tagSize) { + drmp3__on_meta(pMP3, DRMP3_METADATA_TYPE_ID3V2, pTagData, tagSizeWithHeader); + } + + drmp3_free(pTagData, pAllocationCallbacks); + } + } else { + /* Don't have a metadata callback, so just skip the tag. */ + if (onSeek != NULL) { + if (!onSeek(pUserData, tagSize, DRMP3_SEEK_CUR)) { + return DRMP3_FALSE; /* Failed to seek past the ID3v2 tag. */ + } + } else { + /* Don't have a seek callback. Read and discard. */ + char discard[1024]; + + while (tagSize > 0) { + size_t bytesToRead = tagSize; + if (bytesToRead > sizeof(discard)) { + bytesToRead = sizeof(discard); + } + + if (onRead(pUserData, discard, bytesToRead) != bytesToRead) { + return DRMP3_FALSE; /* Failed to read data. */ + } + + tagSize -= (drmp3_uint32)bytesToRead; + } + } + } + + pMP3->streamStartOffset += 10 + tagSize; /* +10 for the header. */ + pMP3->streamCursor = pMP3->streamStartOffset; + } else { + /* Not an ID3v2 tag. Seek back to the start. */ + if (onSeek != NULL) { + if (!onSeek(pUserData, 0, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek back to the start. */ + } + } else { + /* Don't have a seek callback to move backwards. We'll just fall through and let the decoding process re-sync. The ideal solution here would be to read into the cache. */ + + /* + TODO: Copy the header into the cache. Will need to allocate space. See drmp3_decode_next_frame_ex__callbacks. There is not need + to handle the memory case because that will always have a seek implementation and will never hit this code path. + */ + } + } + } else { + /* Failed to read the header. We can return false here. If we couldn't read 10 bytes there's no way we'll have a valid MP3 stream. */ + return DRMP3_FALSE; + } + } + #endif + + /* + Decode the first frame to confirm that it is indeed a valid MP3 stream. Note that it's possible the first frame + is actually a Xing/LAME/VBRI header. If this is the case we need to skip over it. + */ + firstFramePCMFrameCount = drmp3_decode_next_frame_ex(pMP3, (drmp3d_sample_t*)pMP3->pcmFrames, &firstFrameInfo, &pFirstFrameData); + if (firstFramePCMFrameCount > 0) { + DRMP3_ASSERT(pFirstFrameData != NULL); + + /* + It might be a header. If so, we need to clear out the cached PCM frames in order to trigger a reload of fresh + data when decoding starts. We can assume all validation has already been performed to check if this is a valid + MP3 frame and that there is more than 0 bytes making up the frame. + + We're going to be basing this parsing code off the minimp3_ex implementation. + */ + #if 1 + DRMP3_ASSERT(firstFrameInfo.frame_bytes > 0); + { + drmp3_bs bs; + drmp3_L3_gr_info grInfo[4]; + + drmp3_bs_init(&bs, pFirstFrameData + DRMP3_HDR_SIZE, firstFrameInfo.frame_bytes - DRMP3_HDR_SIZE); + + if (DRMP3_HDR_IS_CRC(pFirstFrameData)) { + drmp3_bs_get_bits(&bs, 16); /* CRC. */ + } + + if (drmp3_L3_read_side_info(&bs, grInfo, pFirstFrameData) >= 0) { + drmp3_bool32 isXing = DRMP3_FALSE; + drmp3_bool32 isInfo = DRMP3_FALSE; + const drmp3_uint8* pTagData; + const drmp3_uint8* pTagDataBeg; + const void* pDataBufferEnd = NULL; + size_t frameBytes; + + pTagDataBeg = pFirstFrameData + DRMP3_HDR_SIZE + (bs.pos/8); + pTagData = pTagDataBeg; + + /* + We need to determine how many bytes are actually available in pTagData. Unfortunately this is different depending on + whether or not it's being decoded from memory or callbacks. + */ + if (pMP3->memory.pData != NULL && pMP3->memory.dataSize > 0) { + pDataBufferEnd = pMP3->memory.pData + pMP3->memory.dataSize; + } else { + pDataBufferEnd = pMP3->pData + pMP3->dataCapacity; + } + + frameBytes = DRMP3_MIN((size_t)firstFrameInfo.frame_bytes, (size_t)((drmp3_uint8*)pDataBufferEnd - pFirstFrameData)); + + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 8) { + goto done_xing_info; /* Frame too small for a Xing/Info tag. */ + } + + /* Check for both "Xing" and "Info" identifiers. */ + isXing = (pTagData[0] == 'X' && pTagData[1] == 'i' && pTagData[2] == 'n' && pTagData[3] == 'g'); + isInfo = (pTagData[0] == 'I' && pTagData[1] == 'n' && pTagData[2] == 'f' && pTagData[3] == 'o'); + + if (isXing || isInfo) { + drmp3_uint32 bytes = 0; + drmp3_uint32 flags = pTagData[7]; + + pTagData += 8; /* Skip past the ID and flags. */ + + if (flags & 0x01) { /* FRAMES flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 4) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + detectedMP3FrameCount = (drmp3_uint32)pTagData[0] << 24 | (drmp3_uint32)pTagData[1] << 16 | (drmp3_uint32)pTagData[2] << 8 | (drmp3_uint32)pTagData[3]; + pTagData += 4; + } + + if (flags & 0x02) { /* BYTES flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 4) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + bytes = (drmp3_uint32)pTagData[0] << 24 | (drmp3_uint32)pTagData[1] << 16 | (drmp3_uint32)pTagData[2] << 8 | (drmp3_uint32)pTagData[3]; + (void)bytes; /* <-- Just to silence a warning about `bytes` being assigned but unused. Want to leave this here in case I want to make use of it later. */ + pTagData += 4; + } + + if (flags & 0x04) { /* TOC flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 100) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + /* TODO: Extract and bind seek points. */ + pTagData += 100; + } + + if (flags & 0x08) { /* SCALE flag. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 4) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + pTagData += 4; + } + + /* At this point we're done with the Xing/Info header. Now we can look at the LAME data. */ + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 1) { + goto done_xing_info; /* Not enough data left to check for a LAME header. */ + } + + if (pTagData[0]) { + int delayInPCMFrames; + int paddingInPCMFrames; + + if (frameBytes < (size_t)(pTagData - pFirstFrameData) + 36) { + goto done_xing_info; /* Invalid Xing/Info tag. */ + } + + pTagData += 21; + + delayInPCMFrames = (( (drmp3_uint32)pTagData[0] << 4) | ((drmp3_uint32)pTagData[1] >> 4)) + (528 + 1); + paddingInPCMFrames = ((((drmp3_uint32)pTagData[1] & 0xF) << 8) | ((drmp3_uint32)pTagData[2] )) - (528 + 1); + if (paddingInPCMFrames < 0) { + paddingInPCMFrames = 0; /* Padding cannot be negative. Probably a malformed file. Ignore. */ + } + + pMP3->delayInPCMFrames = (drmp3_uint32)delayInPCMFrames; + pMP3->paddingInPCMFrames = (drmp3_uint32)paddingInPCMFrames; + } + + /* + My understanding is that if the "Xing" header is present we can consider this to be a VBR stream and if the "Info" header is + present it's a CBR stream. If this is not the case let me know! I'm just tracking this for the time being in case I want to + look at doing some CBR optimizations later on, such as faster seeking. + */ + if (isXing) { + pMP3->isVBR = DRMP3_TRUE; + } else if (isInfo) { + pMP3->isCBR = DRMP3_TRUE; + } + + /* Post the raw data of the tag to the metadata callback. */ + if (onMeta != NULL) { + drmp3_metadata_type metadataType = isXing ? DRMP3_METADATA_TYPE_XING : DRMP3_METADATA_TYPE_VBRI; + size_t tagDataSize; + + tagDataSize = (size_t)firstFrameInfo.frame_bytes; + tagDataSize -= (size_t)(pTagDataBeg - pFirstFrameData); + + drmp3__on_meta(pMP3, metadataType, pTagDataBeg, tagDataSize); + } + + /* Since this was identified as a tag, we don't want to treat it as audio. We need to clear out the PCM cache. */ + pMP3->pcmFramesRemainingInMP3Frame = 0; + + /* The start offset needs to be moved to the end of this frame so it's not included in any audio processing after seeking. */ + pMP3->streamStartOffset += (drmp3_uint32)(firstFrameInfo.frame_bytes); + pMP3->streamCursor = pMP3->streamStartOffset; + + /* + The internal decoder needs to be reset to clear out any state. If we don't reset this state, it's possible for + there to be inconsistencies in the number of samples read when reading to the end of the stream depending on + whether or not the caller seeks to the start of the stream. + */ + drmp3dec_init(&pMP3->decoder); + } + + done_xing_info:; + } else { + /* Failed to read the side info. */ + } + } + #endif + } else { + /* Not a valid MP3 stream. */ + drmp3__free_from_callbacks(pMP3->pData, &pMP3->allocationCallbacks); /* The call above may have allocated memory. Need to make sure it's freed before aborting. */ + return DRMP3_FALSE; + } + + if (detectedMP3FrameCount != 0xFFFFFFFF) { + pMP3->totalPCMFrameCount = (drmp3_uint64)detectedMP3FrameCount * firstFramePCMFrameCount; + } + + pMP3->channels = pMP3->mp3FrameChannels; + pMP3->sampleRate = pMP3->mp3FrameSampleRate; + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init(drmp3* pMP3, drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, drmp3_meta_proc onMeta, void* pUserData, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pMP3 == NULL || onRead == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + return drmp3_init_internal(pMP3, onRead, onSeek, onTell, onMeta, pUserData, pUserData, pAllocationCallbacks); +} + + +static size_t drmp3__on_read_memory(void* pUserData, void* pBufferOut, size_t bytesToRead) +{ + drmp3* pMP3 = (drmp3*)pUserData; + size_t bytesRemaining; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->memory.dataSize >= pMP3->memory.currentReadPos); + + bytesRemaining = pMP3->memory.dataSize - pMP3->memory.currentReadPos; + if (bytesToRead > bytesRemaining) { + bytesToRead = bytesRemaining; + } + + if (bytesToRead > 0) { + DRMP3_COPY_MEMORY(pBufferOut, pMP3->memory.pData + pMP3->memory.currentReadPos, bytesToRead); + pMP3->memory.currentReadPos += bytesToRead; + } + + return bytesToRead; +} + +static drmp3_bool32 drmp3__on_seek_memory(void* pUserData, int byteOffset, drmp3_seek_origin origin) +{ + drmp3* pMP3 = (drmp3*)pUserData; + drmp3_int64 newCursor; + + DRMP3_ASSERT(pMP3 != NULL); + + if (origin == DRMP3_SEEK_SET) { + newCursor = 0; + } else if (origin == DRMP3_SEEK_CUR) { + newCursor = (drmp3_int64)pMP3->memory.currentReadPos; + } else if (origin == DRMP3_SEEK_END) { + newCursor = (drmp3_int64)pMP3->memory.dataSize; + } else { + DRMP3_ASSERT(!"Invalid seek origin"); + return DRMP3_FALSE; + } + + newCursor += byteOffset; + + if (newCursor < 0) { + return DRMP3_FALSE; /* Trying to seek prior to the start of the buffer. */ + } + if ((size_t)newCursor > pMP3->memory.dataSize) { + return DRMP3_FALSE; /* Trying to seek beyond the end of the buffer. */ + } + + pMP3->memory.currentReadPos = (size_t)newCursor; + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3__on_tell_memory(void* pUserData, drmp3_int64* pCursor) +{ + drmp3* pMP3 = (drmp3*)pUserData; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pCursor != NULL); + + *pCursor = (drmp3_int64)pMP3->memory.currentReadPos; + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_memory_with_metadata(drmp3* pMP3, const void* pData, size_t dataSize, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3_bool32 result; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + + if (pData == NULL || dataSize == 0) { + return DRMP3_FALSE; + } + + pMP3->memory.pData = (const drmp3_uint8*)pData; + pMP3->memory.dataSize = dataSize; + pMP3->memory.currentReadPos = 0; + + result = drmp3_init_internal(pMP3, drmp3__on_read_memory, drmp3__on_seek_memory, drmp3__on_tell_memory, onMeta, pMP3, pUserDataMeta, pAllocationCallbacks); + if (result == DRMP3_FALSE) { + return DRMP3_FALSE; + } + + /* Adjust the length of the memory stream to account for ID3v1 and APE tags. */ + if (pMP3->streamLength <= (drmp3_uint64)DRMP3_SIZE_MAX) { + pMP3->memory.dataSize = (size_t)pMP3->streamLength; /* Safe cast. */ + } + + if (pMP3->streamStartOffset > (drmp3_uint64)DRMP3_SIZE_MAX) { + return DRMP3_FALSE; /* Tags too big. */ + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_memory(drmp3* pMP3, const void* pData, size_t dataSize, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + return drmp3_init_memory_with_metadata(pMP3, pData, dataSize, NULL, NULL, pAllocationCallbacks); +} + + +#ifndef DR_MP3_NO_STDIO +#include +#include /* For wcslen(), wcsrtombs() */ + +/* Errno */ +/* drmp3_result_from_errno() is only used inside DR_MP3_NO_STDIO for now. Move this out if it's ever used elsewhere. */ +#include +static drmp3_result drmp3_result_from_errno(int e) +{ + switch (e) + { + case 0: return DRMP3_SUCCESS; + #ifdef EPERM + case EPERM: return DRMP3_INVALID_OPERATION; + #endif + #ifdef ENOENT + case ENOENT: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef ESRCH + case ESRCH: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef EINTR + case EINTR: return DRMP3_INTERRUPT; + #endif + #ifdef EIO + case EIO: return DRMP3_IO_ERROR; + #endif + #ifdef ENXIO + case ENXIO: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef E2BIG + case E2BIG: return DRMP3_INVALID_ARGS; + #endif + #ifdef ENOEXEC + case ENOEXEC: return DRMP3_INVALID_FILE; + #endif + #ifdef EBADF + case EBADF: return DRMP3_INVALID_FILE; + #endif + #ifdef ECHILD + case ECHILD: return DRMP3_ERROR; + #endif + #ifdef EAGAIN + case EAGAIN: return DRMP3_UNAVAILABLE; + #endif + #ifdef ENOMEM + case ENOMEM: return DRMP3_OUT_OF_MEMORY; + #endif + #ifdef EACCES + case EACCES: return DRMP3_ACCESS_DENIED; + #endif + #ifdef EFAULT + case EFAULT: return DRMP3_BAD_ADDRESS; + #endif + #ifdef ENOTBLK + case ENOTBLK: return DRMP3_ERROR; + #endif + #ifdef EBUSY + case EBUSY: return DRMP3_BUSY; + #endif + #ifdef EEXIST + case EEXIST: return DRMP3_ALREADY_EXISTS; + #endif + #ifdef EXDEV + case EXDEV: return DRMP3_ERROR; + #endif + #ifdef ENODEV + case ENODEV: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef ENOTDIR + case ENOTDIR: return DRMP3_NOT_DIRECTORY; + #endif + #ifdef EISDIR + case EISDIR: return DRMP3_IS_DIRECTORY; + #endif + #ifdef EINVAL + case EINVAL: return DRMP3_INVALID_ARGS; + #endif + #ifdef ENFILE + case ENFILE: return DRMP3_TOO_MANY_OPEN_FILES; + #endif + #ifdef EMFILE + case EMFILE: return DRMP3_TOO_MANY_OPEN_FILES; + #endif + #ifdef ENOTTY + case ENOTTY: return DRMP3_INVALID_OPERATION; + #endif + #ifdef ETXTBSY + case ETXTBSY: return DRMP3_BUSY; + #endif + #ifdef EFBIG + case EFBIG: return DRMP3_TOO_BIG; + #endif + #ifdef ENOSPC + case ENOSPC: return DRMP3_NO_SPACE; + #endif + #ifdef ESPIPE + case ESPIPE: return DRMP3_BAD_SEEK; + #endif + #ifdef EROFS + case EROFS: return DRMP3_ACCESS_DENIED; + #endif + #ifdef EMLINK + case EMLINK: return DRMP3_TOO_MANY_LINKS; + #endif + #ifdef EPIPE + case EPIPE: return DRMP3_BAD_PIPE; + #endif + #ifdef EDOM + case EDOM: return DRMP3_OUT_OF_RANGE; + #endif + #ifdef ERANGE + case ERANGE: return DRMP3_OUT_OF_RANGE; + #endif + #ifdef EDEADLK + case EDEADLK: return DRMP3_DEADLOCK; + #endif + #ifdef ENAMETOOLONG + case ENAMETOOLONG: return DRMP3_PATH_TOO_LONG; + #endif + #ifdef ENOLCK + case ENOLCK: return DRMP3_ERROR; + #endif + #ifdef ENOSYS + case ENOSYS: return DRMP3_NOT_IMPLEMENTED; + #endif + #if defined(ENOTEMPTY) && ENOTEMPTY != EEXIST /* In AIX, ENOTEMPTY and EEXIST use the same value. */ + case ENOTEMPTY: return DRMP3_DIRECTORY_NOT_EMPTY; + #endif + #ifdef ELOOP + case ELOOP: return DRMP3_TOO_MANY_LINKS; + #endif + #ifdef ENOMSG + case ENOMSG: return DRMP3_NO_MESSAGE; + #endif + #ifdef EIDRM + case EIDRM: return DRMP3_ERROR; + #endif + #ifdef ECHRNG + case ECHRNG: return DRMP3_ERROR; + #endif + #ifdef EL2NSYNC + case EL2NSYNC: return DRMP3_ERROR; + #endif + #ifdef EL3HLT + case EL3HLT: return DRMP3_ERROR; + #endif + #ifdef EL3RST + case EL3RST: return DRMP3_ERROR; + #endif + #ifdef ELNRNG + case ELNRNG: return DRMP3_OUT_OF_RANGE; + #endif + #ifdef EUNATCH + case EUNATCH: return DRMP3_ERROR; + #endif + #ifdef ENOCSI + case ENOCSI: return DRMP3_ERROR; + #endif + #ifdef EL2HLT + case EL2HLT: return DRMP3_ERROR; + #endif + #ifdef EBADE + case EBADE: return DRMP3_ERROR; + #endif + #ifdef EBADR + case EBADR: return DRMP3_ERROR; + #endif + #ifdef EXFULL + case EXFULL: return DRMP3_ERROR; + #endif + #ifdef ENOANO + case ENOANO: return DRMP3_ERROR; + #endif + #ifdef EBADRQC + case EBADRQC: return DRMP3_ERROR; + #endif + #ifdef EBADSLT + case EBADSLT: return DRMP3_ERROR; + #endif + #ifdef EBFONT + case EBFONT: return DRMP3_INVALID_FILE; + #endif + #ifdef ENOSTR + case ENOSTR: return DRMP3_ERROR; + #endif + #ifdef ENODATA + case ENODATA: return DRMP3_NO_DATA_AVAILABLE; + #endif + #ifdef ETIME + case ETIME: return DRMP3_TIMEOUT; + #endif + #ifdef ENOSR + case ENOSR: return DRMP3_NO_DATA_AVAILABLE; + #endif + #ifdef ENONET + case ENONET: return DRMP3_NO_NETWORK; + #endif + #ifdef ENOPKG + case ENOPKG: return DRMP3_ERROR; + #endif + #ifdef EREMOTE + case EREMOTE: return DRMP3_ERROR; + #endif + #ifdef ENOLINK + case ENOLINK: return DRMP3_ERROR; + #endif + #ifdef EADV + case EADV: return DRMP3_ERROR; + #endif + #ifdef ESRMNT + case ESRMNT: return DRMP3_ERROR; + #endif + #ifdef ECOMM + case ECOMM: return DRMP3_ERROR; + #endif + #ifdef EPROTO + case EPROTO: return DRMP3_ERROR; + #endif + #ifdef EMULTIHOP + case EMULTIHOP: return DRMP3_ERROR; + #endif + #ifdef EDOTDOT + case EDOTDOT: return DRMP3_ERROR; + #endif + #ifdef EBADMSG + case EBADMSG: return DRMP3_BAD_MESSAGE; + #endif + #ifdef EOVERFLOW + case EOVERFLOW: return DRMP3_TOO_BIG; + #endif + #ifdef ENOTUNIQ + case ENOTUNIQ: return DRMP3_NOT_UNIQUE; + #endif + #ifdef EBADFD + case EBADFD: return DRMP3_ERROR; + #endif + #ifdef EREMCHG + case EREMCHG: return DRMP3_ERROR; + #endif + #ifdef ELIBACC + case ELIBACC: return DRMP3_ACCESS_DENIED; + #endif + #ifdef ELIBBAD + case ELIBBAD: return DRMP3_INVALID_FILE; + #endif + #ifdef ELIBSCN + case ELIBSCN: return DRMP3_INVALID_FILE; + #endif + #ifdef ELIBMAX + case ELIBMAX: return DRMP3_ERROR; + #endif + #ifdef ELIBEXEC + case ELIBEXEC: return DRMP3_ERROR; + #endif + #ifdef EILSEQ + case EILSEQ: return DRMP3_INVALID_DATA; + #endif + #ifdef ERESTART + case ERESTART: return DRMP3_ERROR; + #endif + #ifdef ESTRPIPE + case ESTRPIPE: return DRMP3_ERROR; + #endif + #ifdef EUSERS + case EUSERS: return DRMP3_ERROR; + #endif + #ifdef ENOTSOCK + case ENOTSOCK: return DRMP3_NOT_SOCKET; + #endif + #ifdef EDESTADDRREQ + case EDESTADDRREQ: return DRMP3_NO_ADDRESS; + #endif + #ifdef EMSGSIZE + case EMSGSIZE: return DRMP3_TOO_BIG; + #endif + #ifdef EPROTOTYPE + case EPROTOTYPE: return DRMP3_BAD_PROTOCOL; + #endif + #ifdef ENOPROTOOPT + case ENOPROTOOPT: return DRMP3_PROTOCOL_UNAVAILABLE; + #endif + #ifdef EPROTONOSUPPORT + case EPROTONOSUPPORT: return DRMP3_PROTOCOL_NOT_SUPPORTED; + #endif + #ifdef ESOCKTNOSUPPORT + case ESOCKTNOSUPPORT: return DRMP3_SOCKET_NOT_SUPPORTED; + #endif + #ifdef EOPNOTSUPP + case EOPNOTSUPP: return DRMP3_INVALID_OPERATION; + #endif + #ifdef EPFNOSUPPORT + case EPFNOSUPPORT: return DRMP3_PROTOCOL_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EAFNOSUPPORT + case EAFNOSUPPORT: return DRMP3_ADDRESS_FAMILY_NOT_SUPPORTED; + #endif + #ifdef EADDRINUSE + case EADDRINUSE: return DRMP3_ALREADY_IN_USE; + #endif + #ifdef EADDRNOTAVAIL + case EADDRNOTAVAIL: return DRMP3_ERROR; + #endif + #ifdef ENETDOWN + case ENETDOWN: return DRMP3_NO_NETWORK; + #endif + #ifdef ENETUNREACH + case ENETUNREACH: return DRMP3_NO_NETWORK; + #endif + #ifdef ENETRESET + case ENETRESET: return DRMP3_NO_NETWORK; + #endif + #ifdef ECONNABORTED + case ECONNABORTED: return DRMP3_NO_NETWORK; + #endif + #ifdef ECONNRESET + case ECONNRESET: return DRMP3_CONNECTION_RESET; + #endif + #ifdef ENOBUFS + case ENOBUFS: return DRMP3_NO_SPACE; + #endif + #ifdef EISCONN + case EISCONN: return DRMP3_ALREADY_CONNECTED; + #endif + #ifdef ENOTCONN + case ENOTCONN: return DRMP3_NOT_CONNECTED; + #endif + #ifdef ESHUTDOWN + case ESHUTDOWN: return DRMP3_ERROR; + #endif + #ifdef ETOOMANYREFS + case ETOOMANYREFS: return DRMP3_ERROR; + #endif + #ifdef ETIMEDOUT + case ETIMEDOUT: return DRMP3_TIMEOUT; + #endif + #ifdef ECONNREFUSED + case ECONNREFUSED: return DRMP3_CONNECTION_REFUSED; + #endif + #ifdef EHOSTDOWN + case EHOSTDOWN: return DRMP3_NO_HOST; + #endif + #ifdef EHOSTUNREACH + case EHOSTUNREACH: return DRMP3_NO_HOST; + #endif + #ifdef EALREADY + case EALREADY: return DRMP3_IN_PROGRESS; + #endif + #ifdef EINPROGRESS + case EINPROGRESS: return DRMP3_IN_PROGRESS; + #endif + #ifdef ESTALE + case ESTALE: return DRMP3_INVALID_FILE; + #endif + #ifdef EUCLEAN + case EUCLEAN: return DRMP3_ERROR; + #endif + #ifdef ENOTNAM + case ENOTNAM: return DRMP3_ERROR; + #endif + #ifdef ENAVAIL + case ENAVAIL: return DRMP3_ERROR; + #endif + #ifdef EISNAM + case EISNAM: return DRMP3_ERROR; + #endif + #ifdef EREMOTEIO + case EREMOTEIO: return DRMP3_IO_ERROR; + #endif + #ifdef EDQUOT + case EDQUOT: return DRMP3_NO_SPACE; + #endif + #ifdef ENOMEDIUM + case ENOMEDIUM: return DRMP3_DOES_NOT_EXIST; + #endif + #ifdef EMEDIUMTYPE + case EMEDIUMTYPE: return DRMP3_ERROR; + #endif + #ifdef ECANCELED + case ECANCELED: return DRMP3_CANCELLED; + #endif + #ifdef ENOKEY + case ENOKEY: return DRMP3_ERROR; + #endif + #ifdef EKEYEXPIRED + case EKEYEXPIRED: return DRMP3_ERROR; + #endif + #ifdef EKEYREVOKED + case EKEYREVOKED: return DRMP3_ERROR; + #endif + #ifdef EKEYREJECTED + case EKEYREJECTED: return DRMP3_ERROR; + #endif + #ifdef EOWNERDEAD + case EOWNERDEAD: return DRMP3_ERROR; + #endif + #ifdef ENOTRECOVERABLE + case ENOTRECOVERABLE: return DRMP3_ERROR; + #endif + #ifdef ERFKILL + case ERFKILL: return DRMP3_ERROR; + #endif + #ifdef EHWPOISON + case EHWPOISON: return DRMP3_ERROR; + #endif + default: return DRMP3_ERROR; + } +} +/* End Errno */ + +/* fopen */ +static drmp3_result drmp3_fopen(FILE** ppFile, const char* pFilePath, const char* pOpenMode) +{ +#if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err; +#endif + + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRMP3_INVALID_ARGS; + } + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + err = fopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drmp3_result_from_errno(err); + } +#else +#if defined(_WIN32) || defined(__APPLE__) + *ppFile = fopen(pFilePath, pOpenMode); +#else + #if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 && defined(_LARGEFILE64_SOURCE) + *ppFile = fopen64(pFilePath, pOpenMode); + #else + *ppFile = fopen(pFilePath, pOpenMode); + #endif +#endif + if (*ppFile == NULL) { + drmp3_result result = drmp3_result_from_errno(errno); + if (result == DRMP3_SUCCESS) { + result = DRMP3_ERROR; /* Just a safety check to make sure we never ever return success when pFile == NULL. */ + } + + return result; + } +#endif + + return DRMP3_SUCCESS; +} + +/* +_wfopen() isn't always available in all compilation environments. + + * Windows only. + * MSVC seems to support it universally as far back as VC6 from what I can tell (haven't checked further back). + * MinGW-64 (both 32- and 64-bit) seems to support it. + * MinGW wraps it in !defined(__STRICT_ANSI__). + * OpenWatcom wraps it in !defined(_NO_EXT_KEYS). + +This can be reviewed as compatibility issues arise. The preference is to use _wfopen_s() and _wfopen() as opposed to the wcsrtombs() +fallback, so if you notice your compiler not detecting this properly I'm happy to look at adding support. +*/ +#if defined(_WIN32) + #if defined(_MSC_VER) || defined(__MINGW64__) || (!defined(__STRICT_ANSI__) && !defined(_NO_EXT_KEYS)) + #define DRMP3_HAS_WFOPEN + #endif +#endif + +static drmp3_result drmp3_wfopen(FILE** ppFile, const wchar_t* pFilePath, const wchar_t* pOpenMode, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (ppFile != NULL) { + *ppFile = NULL; /* Safety. */ + } + + if (pFilePath == NULL || pOpenMode == NULL || ppFile == NULL) { + return DRMP3_INVALID_ARGS; + } + +#if defined(DRMP3_HAS_WFOPEN) + { + /* Use _wfopen() on Windows. */ + #if defined(_MSC_VER) && _MSC_VER >= 1400 + errno_t err = _wfopen_s(ppFile, pFilePath, pOpenMode); + if (err != 0) { + return drmp3_result_from_errno(err); + } + #else + *ppFile = _wfopen(pFilePath, pOpenMode); + if (*ppFile == NULL) { + return drmp3_result_from_errno(errno); + } + #endif + (void)pAllocationCallbacks; + } +#else + /* + Use fopen() on anything other than Windows. Requires a conversion. This is annoying because + fopen() is locale specific. The only real way I can think of to do this is with wcsrtombs(). Note + that wcstombs() is apparently not thread-safe because it uses a static global mbstate_t object for + maintaining state. I've checked this with -std=c89 and it works, but if somebody get's a compiler + error I'll look into improving compatibility. + */ + + /* + Some compilers don't support wchar_t or wcsrtombs() which we're using below. In this case we just + need to abort with an error. If you encounter a compiler lacking such support, add it to this list + and submit a bug report and it'll be added to the library upstream. + */ + #if defined(__DJGPP__) + { + /* Nothing to do here. This will fall through to the error check below. */ + } + #else + { + mbstate_t mbs; + size_t lenMB; + const wchar_t* pFilePathTemp = pFilePath; + char* pFilePathMB = NULL; + char pOpenModeMB[32] = {0}; + + /* Get the length first. */ + DRMP3_ZERO_OBJECT(&mbs); + lenMB = wcsrtombs(NULL, &pFilePathTemp, 0, &mbs); + if (lenMB == (size_t)-1) { + return drmp3_result_from_errno(errno); + } + + pFilePathMB = (char*)drmp3__malloc_from_callbacks(lenMB + 1, pAllocationCallbacks); + if (pFilePathMB == NULL) { + return DRMP3_OUT_OF_MEMORY; + } + + pFilePathTemp = pFilePath; + DRMP3_ZERO_OBJECT(&mbs); + wcsrtombs(pFilePathMB, &pFilePathTemp, lenMB + 1, &mbs); + + /* The open mode should always consist of ASCII characters so we should be able to do a trivial conversion. */ + { + size_t i = 0; + for (;;) { + if (pOpenMode[i] == 0) { + pOpenModeMB[i] = '\0'; + break; + } + + pOpenModeMB[i] = (char)pOpenMode[i]; + i += 1; + } + } + + *ppFile = fopen(pFilePathMB, pOpenModeMB); + + drmp3__free_from_callbacks(pFilePathMB, pAllocationCallbacks); + } + #endif + + if (*ppFile == NULL) { + return DRMP3_ERROR; + } +#endif + + return DRMP3_SUCCESS; +} +/* End fopen */ + + +static size_t drmp3__on_read_stdio(void* pUserData, void* pBufferOut, size_t bytesToRead) +{ + return fread(pBufferOut, 1, bytesToRead, (FILE*)pUserData); +} + +static drmp3_bool32 drmp3__on_seek_stdio(void* pUserData, int offset, drmp3_seek_origin origin) +{ + int whence = SEEK_SET; + if (origin == DRMP3_SEEK_CUR) { + whence = SEEK_CUR; + } else if (origin == DRMP3_SEEK_END) { + whence = SEEK_END; + } + + return fseek((FILE*)pUserData, offset, whence) == 0; +} + +static drmp3_bool32 drmp3__on_tell_stdio(void* pUserData, drmp3_int64* pCursor) +{ + FILE* pFileStdio = (FILE*)pUserData; + drmp3_int64 result; + + /* These were all validated at a higher level. */ + DRMP3_ASSERT(pFileStdio != NULL); + DRMP3_ASSERT(pCursor != NULL); + +#if defined(_WIN32) && !defined(NXDK) + #if defined(_MSC_VER) && _MSC_VER > 1200 + result = _ftelli64(pFileStdio); + #else + result = ftell(pFileStdio); + #endif +#else + result = ftell(pFileStdio); +#endif + + *pCursor = result; + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata(drmp3* pMP3, const char* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3_bool32 result; + FILE* pFile; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + + if (drmp3_fopen(&pFile, pFilePath, "rb") != DRMP3_SUCCESS) { + return DRMP3_FALSE; + } + + result = drmp3_init_internal(pMP3, drmp3__on_read_stdio, drmp3__on_seek_stdio, drmp3__on_tell_stdio, onMeta, (void*)pFile, pUserDataMeta, pAllocationCallbacks); + if (result != DRMP3_TRUE) { + fclose(pFile); + return result; + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_file_with_metadata_w(drmp3* pMP3, const wchar_t* pFilePath, drmp3_meta_proc onMeta, void* pUserDataMeta, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3_bool32 result; + FILE* pFile; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + DRMP3_ZERO_OBJECT(pMP3); + + if (drmp3_wfopen(&pFile, pFilePath, L"rb", pAllocationCallbacks) != DRMP3_SUCCESS) { + return DRMP3_FALSE; + } + + result = drmp3_init_internal(pMP3, drmp3__on_read_stdio, drmp3__on_seek_stdio, drmp3__on_tell_stdio, onMeta, (void*)pFile, pUserDataMeta, pAllocationCallbacks); + if (result != DRMP3_TRUE) { + fclose(pFile); + return result; + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_init_file(drmp3* pMP3, const char* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + return drmp3_init_file_with_metadata(pMP3, pFilePath, NULL, NULL, pAllocationCallbacks); +} + +DRMP3_API drmp3_bool32 drmp3_init_file_w(drmp3* pMP3, const wchar_t* pFilePath, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + return drmp3_init_file_with_metadata_w(pMP3, pFilePath, NULL, NULL, pAllocationCallbacks); +} +#endif + +DRMP3_API void drmp3_uninit(drmp3* pMP3) +{ + if (pMP3 == NULL) { + return; + } + +#ifndef DR_MP3_NO_STDIO + if (pMP3->onRead == drmp3__on_read_stdio) { + FILE* pFile = (FILE*)pMP3->pUserData; + if (pFile != NULL) { + fclose(pFile); + pMP3->pUserData = NULL; /* Make sure the file handle is cleared to NULL to we don't attempt to close it a second time. */ + } + } +#endif + + drmp3__free_from_callbacks(pMP3->pData, &pMP3->allocationCallbacks); +} + +#if defined(DR_MP3_FLOAT_OUTPUT) +static void drmp3_f32_to_s16(drmp3_int16* dst, const float* src, drmp3_uint64 sampleCount) +{ + drmp3_uint64 i; + drmp3_uint64 i4; + drmp3_uint64 sampleCount4; + + /* Unrolled. */ + i = 0; + sampleCount4 = sampleCount >> 2; + for (i4 = 0; i4 < sampleCount4; i4 += 1) { + float x0 = src[i+0]; + float x1 = src[i+1]; + float x2 = src[i+2]; + float x3 = src[i+3]; + + x0 = ((x0 < -1) ? -1 : ((x0 > 1) ? 1 : x0)); + x1 = ((x1 < -1) ? -1 : ((x1 > 1) ? 1 : x1)); + x2 = ((x2 < -1) ? -1 : ((x2 > 1) ? 1 : x2)); + x3 = ((x3 < -1) ? -1 : ((x3 > 1) ? 1 : x3)); + + x0 = x0 * 32767.0f; + x1 = x1 * 32767.0f; + x2 = x2 * 32767.0f; + x3 = x3 * 32767.0f; + + dst[i+0] = (drmp3_int16)x0; + dst[i+1] = (drmp3_int16)x1; + dst[i+2] = (drmp3_int16)x2; + dst[i+3] = (drmp3_int16)x3; + + i += 4; + } + + /* Leftover. */ + for (; i < sampleCount; i += 1) { + float x = src[i]; + x = ((x < -1) ? -1 : ((x > 1) ? 1 : x)); /* clip */ + x = x * 32767.0f; /* -1..1 to -32767..32767 */ + + dst[i] = (drmp3_int16)x; + } +} +#endif + +#if !defined(DR_MP3_FLOAT_OUTPUT) +static void drmp3_s16_to_f32(float* dst, const drmp3_int16* src, drmp3_uint64 sampleCount) +{ + drmp3_uint64 i; + for (i = 0; i < sampleCount; i += 1) { + float x = (float)src[i]; + x = x * 0.000030517578125f; /* -32768..32767 to -1..0.999969482421875 */ + dst[i] = x; + } +} +#endif + + +static drmp3_uint64 drmp3_read_pcm_frames_raw(drmp3* pMP3, drmp3_uint64 framesToRead, void* pBufferOut) +{ + drmp3_uint64 totalFramesRead = 0; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onRead != NULL); + + while (framesToRead > 0) { + drmp3_uint32 framesToConsume; + + /* Skip frames if necessary. */ + if (pMP3->currentPCMFrame < pMP3->delayInPCMFrames) { + drmp3_uint32 framesToSkip = (drmp3_uint32)DRMP3_MIN(pMP3->pcmFramesRemainingInMP3Frame, pMP3->delayInPCMFrames - pMP3->currentPCMFrame); + + pMP3->currentPCMFrame += framesToSkip; + pMP3->pcmFramesConsumedInMP3Frame += framesToSkip; + pMP3->pcmFramesRemainingInMP3Frame -= framesToSkip; + } + + framesToConsume = (drmp3_uint32)DRMP3_MIN(pMP3->pcmFramesRemainingInMP3Frame, framesToRead); + + /* Clamp the number of frames to read to the padding. */ + if (pMP3->totalPCMFrameCount != DRMP3_UINT64_MAX && pMP3->totalPCMFrameCount > pMP3->paddingInPCMFrames) { + if (pMP3->currentPCMFrame < (pMP3->totalPCMFrameCount - pMP3->paddingInPCMFrames)) { + drmp3_uint64 framesRemainigToPadding = (pMP3->totalPCMFrameCount - pMP3->paddingInPCMFrames) - pMP3->currentPCMFrame; + if (framesToConsume > framesRemainigToPadding) { + framesToConsume = (drmp3_uint32)framesRemainigToPadding; + } + } else { + /* We're into the padding. Abort. */ + break; + } + } + + if (pBufferOut != NULL) { + #if defined(DR_MP3_FLOAT_OUTPUT) + { + /* f32 */ + float* pFramesOutF32 = (float*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(float) * totalFramesRead * pMP3->channels); + float* pFramesInF32 = (float*)DRMP3_OFFSET_PTR(&pMP3->pcmFrames[0], sizeof(float) * pMP3->pcmFramesConsumedInMP3Frame * pMP3->mp3FrameChannels); + DRMP3_COPY_MEMORY(pFramesOutF32, pFramesInF32, sizeof(float) * framesToConsume * pMP3->channels); + } + #else + { + /* s16 */ + drmp3_int16* pFramesOutS16 = (drmp3_int16*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(drmp3_int16) * totalFramesRead * pMP3->channels); + drmp3_int16* pFramesInS16 = (drmp3_int16*)DRMP3_OFFSET_PTR(&pMP3->pcmFrames[0], sizeof(drmp3_int16) * pMP3->pcmFramesConsumedInMP3Frame * pMP3->mp3FrameChannels); + DRMP3_COPY_MEMORY(pFramesOutS16, pFramesInS16, sizeof(drmp3_int16) * framesToConsume * pMP3->channels); + } + #endif + } + + pMP3->currentPCMFrame += framesToConsume; + pMP3->pcmFramesConsumedInMP3Frame += framesToConsume; + pMP3->pcmFramesRemainingInMP3Frame -= framesToConsume; + totalFramesRead += framesToConsume; + framesToRead -= framesToConsume; + + if (framesToRead == 0) { + break; + } + + /* If the cursor is already at the padding we need to abort. */ + if (pMP3->totalPCMFrameCount != DRMP3_UINT64_MAX && pMP3->totalPCMFrameCount > pMP3->paddingInPCMFrames && pMP3->currentPCMFrame >= (pMP3->totalPCMFrameCount - pMP3->paddingInPCMFrames)) { + break; + } + + DRMP3_ASSERT(pMP3->pcmFramesRemainingInMP3Frame == 0); + + /* At this point we have exhausted our in-memory buffer so we need to re-fill. */ + if (drmp3_decode_next_frame(pMP3) == 0) { + break; + } + } + + return totalFramesRead; +} + + +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_f32(drmp3* pMP3, drmp3_uint64 framesToRead, float* pBufferOut) +{ + if (pMP3 == NULL || pMP3->onRead == NULL) { + return 0; + } + +#if defined(DR_MP3_FLOAT_OUTPUT) + /* Fast path. No conversion required. */ + return drmp3_read_pcm_frames_raw(pMP3, framesToRead, pBufferOut); +#else + /* Slow path. Convert from s16 to f32. */ + { + drmp3_int16 pTempS16[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + drmp3_uint64 totalPCMFramesRead = 0; + + while (totalPCMFramesRead < framesToRead) { + drmp3_uint64 framesJustRead; + drmp3_uint64 framesRemaining = framesToRead - totalPCMFramesRead; + drmp3_uint64 framesToReadNow = DRMP3_COUNTOF(pTempS16) / pMP3->channels; + if (framesToReadNow > framesRemaining) { + framesToReadNow = framesRemaining; + } + + framesJustRead = drmp3_read_pcm_frames_raw(pMP3, framesToReadNow, pTempS16); + if (framesJustRead == 0) { + break; + } + + drmp3_s16_to_f32((float*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(float) * totalPCMFramesRead * pMP3->channels), pTempS16, framesJustRead * pMP3->channels); + totalPCMFramesRead += framesJustRead; + } + + return totalPCMFramesRead; + } +#endif +} + +DRMP3_API drmp3_uint64 drmp3_read_pcm_frames_s16(drmp3* pMP3, drmp3_uint64 framesToRead, drmp3_int16* pBufferOut) +{ + if (pMP3 == NULL || pMP3->onRead == NULL) { + return 0; + } + +#if !defined(DR_MP3_FLOAT_OUTPUT) + /* Fast path. No conversion required. */ + return drmp3_read_pcm_frames_raw(pMP3, framesToRead, pBufferOut); +#else + /* Slow path. Convert from f32 to s16. */ + { + float pTempF32[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + drmp3_uint64 totalPCMFramesRead = 0; + + while (totalPCMFramesRead < framesToRead) { + drmp3_uint64 framesJustRead; + drmp3_uint64 framesRemaining = framesToRead - totalPCMFramesRead; + drmp3_uint64 framesToReadNow = DRMP3_COUNTOF(pTempF32) / pMP3->channels; + if (framesToReadNow > framesRemaining) { + framesToReadNow = framesRemaining; + } + + framesJustRead = drmp3_read_pcm_frames_raw(pMP3, framesToReadNow, pTempF32); + if (framesJustRead == 0) { + break; + } + + drmp3_f32_to_s16((drmp3_int16*)DRMP3_OFFSET_PTR(pBufferOut, sizeof(drmp3_int16) * totalPCMFramesRead * pMP3->channels), pTempF32, framesJustRead * pMP3->channels); + totalPCMFramesRead += framesJustRead; + } + + return totalPCMFramesRead; + } +#endif +} + +static void drmp3_reset(drmp3* pMP3) +{ + DRMP3_ASSERT(pMP3 != NULL); + + pMP3->pcmFramesConsumedInMP3Frame = 0; + pMP3->pcmFramesRemainingInMP3Frame = 0; + pMP3->currentPCMFrame = 0; + pMP3->dataSize = 0; + pMP3->atEnd = DRMP3_FALSE; + drmp3dec_init(&pMP3->decoder); +} + +static drmp3_bool32 drmp3_seek_to_start_of_stream(drmp3* pMP3) +{ + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->onSeek != NULL); + + /* Seek to the start of the stream to begin with. */ + if (!drmp3__on_seek_64(pMP3, pMP3->streamStartOffset, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; + } + + /* Clear any cached data. */ + drmp3_reset(pMP3); + return DRMP3_TRUE; +} + + +static drmp3_bool32 drmp3_seek_forward_by_pcm_frames__brute_force(drmp3* pMP3, drmp3_uint64 frameOffset) +{ + drmp3_uint64 framesRead; + + /* + Just using a dumb read-and-discard for now. What would be nice is to parse only the header of the MP3 frame, and then skip over leading + frames without spending the time doing a full decode. I cannot see an easy way to do this in minimp3, however, so it may involve some + kind of manual processing. + */ +#if defined(DR_MP3_FLOAT_OUTPUT) + framesRead = drmp3_read_pcm_frames_f32(pMP3, frameOffset, NULL); +#else + framesRead = drmp3_read_pcm_frames_s16(pMP3, frameOffset, NULL); +#endif + if (framesRead != frameOffset) { + return DRMP3_FALSE; + } + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3_seek_to_pcm_frame__brute_force(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + DRMP3_ASSERT(pMP3 != NULL); + + if (frameIndex == pMP3->currentPCMFrame) { + return DRMP3_TRUE; + } + + /* + If we're moving foward we just read from where we're at. Otherwise we need to move back to the start of + the stream and read from the beginning. + */ + if (frameIndex < pMP3->currentPCMFrame) { + /* Moving backward. Move to the start of the stream and then move forward. */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + } + + DRMP3_ASSERT(frameIndex >= pMP3->currentPCMFrame); + return drmp3_seek_forward_by_pcm_frames__brute_force(pMP3, (frameIndex - pMP3->currentPCMFrame)); +} + +static drmp3_bool32 drmp3_find_closest_seek_point(drmp3* pMP3, drmp3_uint64 frameIndex, drmp3_uint32* pSeekPointIndex) +{ + drmp3_uint32 iSeekPoint; + + DRMP3_ASSERT(pSeekPointIndex != NULL); + + *pSeekPointIndex = 0; + + if (frameIndex < pMP3->pSeekPoints[0].pcmFrameIndex) { + return DRMP3_FALSE; + } + + /* Linear search for simplicity to begin with while I'm getting this thing working. Once it's all working change this to a binary search. */ + for (iSeekPoint = 0; iSeekPoint < pMP3->seekPointCount; ++iSeekPoint) { + if (pMP3->pSeekPoints[iSeekPoint].pcmFrameIndex > frameIndex) { + break; /* Found it. */ + } + + *pSeekPointIndex = iSeekPoint; + } + + return DRMP3_TRUE; +} + +static drmp3_bool32 drmp3_seek_to_pcm_frame__seek_table(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + drmp3_seek_point seekPoint; + drmp3_uint32 priorSeekPointIndex; + drmp3_uint16 iMP3Frame; + drmp3_uint64 leftoverFrames; + + DRMP3_ASSERT(pMP3 != NULL); + DRMP3_ASSERT(pMP3->pSeekPoints != NULL); + DRMP3_ASSERT(pMP3->seekPointCount > 0); + + /* If there is no prior seekpoint it means the target PCM frame comes before the first seek point. Just assume a seekpoint at the start of the file in this case. */ + if (drmp3_find_closest_seek_point(pMP3, frameIndex, &priorSeekPointIndex)) { + seekPoint = pMP3->pSeekPoints[priorSeekPointIndex]; + } else { + seekPoint.seekPosInBytes = 0; + seekPoint.pcmFrameIndex = 0; + seekPoint.mp3FramesToDiscard = 0; + seekPoint.pcmFramesToDiscard = 0; + } + + /* First thing to do is seek to the first byte of the relevant MP3 frame. */ + if (!drmp3__on_seek_64(pMP3, seekPoint.seekPosInBytes, DRMP3_SEEK_SET)) { + return DRMP3_FALSE; /* Failed to seek. */ + } + + /* Clear any cached data. */ + drmp3_reset(pMP3); + + /* Whole MP3 frames need to be discarded first. */ + for (iMP3Frame = 0; iMP3Frame < seekPoint.mp3FramesToDiscard; ++iMP3Frame) { + drmp3_uint32 pcmFramesRead; + drmp3d_sample_t* pPCMFrames; + + /* Pass in non-null for the last frame because we want to ensure the sample rate converter is preloaded correctly. */ + pPCMFrames = NULL; + if (iMP3Frame == seekPoint.mp3FramesToDiscard-1) { + pPCMFrames = (drmp3d_sample_t*)pMP3->pcmFrames; + } + + /* We first need to decode the next frame. */ + pcmFramesRead = drmp3_decode_next_frame_ex(pMP3, pPCMFrames, NULL, NULL); + if (pcmFramesRead == 0) { + return DRMP3_FALSE; + } + } + + /* We seeked to an MP3 frame in the raw stream so we need to make sure the current PCM frame is set correctly. */ + pMP3->currentPCMFrame = seekPoint.pcmFrameIndex - seekPoint.pcmFramesToDiscard; + + /* + Now at this point we can follow the same process as the brute force technique where we just skip over unnecessary MP3 frames and then + read-and-discard at least 2 whole MP3 frames. + */ + leftoverFrames = frameIndex - pMP3->currentPCMFrame; + return drmp3_seek_forward_by_pcm_frames__brute_force(pMP3, leftoverFrames); +} + +DRMP3_API drmp3_bool32 drmp3_seek_to_pcm_frame(drmp3* pMP3, drmp3_uint64 frameIndex) +{ + if (pMP3 == NULL || pMP3->onSeek == NULL) { + return DRMP3_FALSE; + } + + if (frameIndex == 0) { + return drmp3_seek_to_start_of_stream(pMP3); + } + + /* Use the seek table if we have one. */ + if (pMP3->pSeekPoints != NULL && pMP3->seekPointCount > 0) { + return drmp3_seek_to_pcm_frame__seek_table(pMP3, frameIndex); + } else { + return drmp3_seek_to_pcm_frame__brute_force(pMP3, frameIndex); + } +} + +DRMP3_API drmp3_bool32 drmp3_get_mp3_and_pcm_frame_count(drmp3* pMP3, drmp3_uint64* pMP3FrameCount, drmp3_uint64* pPCMFrameCount) +{ + drmp3_uint64 currentPCMFrame; + drmp3_uint64 totalPCMFrameCount; + drmp3_uint64 totalMP3FrameCount; + + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + /* + The way this works is we move back to the start of the stream, iterate over each MP3 frame and calculate the frame count based + on our output sample rate, the seek back to the PCM frame we were sitting on before calling this function. + */ + + /* The stream must support seeking for this to work. */ + if (pMP3->onSeek == NULL) { + return DRMP3_FALSE; + } + + /* We'll need to seek back to where we were, so grab the PCM frame we're currently sitting on so we can restore later. */ + currentPCMFrame = pMP3->currentPCMFrame; + + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + + totalPCMFrameCount = 0; + totalMP3FrameCount = 0; + + for (;;) { + drmp3_uint32 pcmFramesInCurrentMP3Frame; + + pcmFramesInCurrentMP3Frame = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFramesInCurrentMP3Frame == 0) { + break; + } + + totalPCMFrameCount += pcmFramesInCurrentMP3Frame; + totalMP3FrameCount += 1; + } + + /* Finally, we need to seek back to where we were. */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + + if (!drmp3_seek_to_pcm_frame(pMP3, currentPCMFrame)) { + return DRMP3_FALSE; + } + + if (pMP3FrameCount != NULL) { + *pMP3FrameCount = totalMP3FrameCount; + } + if (pPCMFrameCount != NULL) { + *pPCMFrameCount = totalPCMFrameCount; + } + + return DRMP3_TRUE; +} + +DRMP3_API drmp3_uint64 drmp3_get_pcm_frame_count(drmp3* pMP3) +{ + drmp3_uint64 totalPCMFrameCount; + + if (pMP3 == NULL) { + return 0; + } + + if (pMP3->totalPCMFrameCount != DRMP3_UINT64_MAX) { + totalPCMFrameCount = pMP3->totalPCMFrameCount; + + if (totalPCMFrameCount >= pMP3->delayInPCMFrames) { + totalPCMFrameCount -= pMP3->delayInPCMFrames; + } else { + /* The delay is greater than the frame count reported by the Xing/Info tag. Assume it's invalid and ignore. */ + } + + if (totalPCMFrameCount >= pMP3->paddingInPCMFrames) { + totalPCMFrameCount -= pMP3->paddingInPCMFrames; + } else { + /* The padding is greater than the frame count reported by the Xing/Info tag. Assume it's invalid and ignore. */ + } + + return totalPCMFrameCount; + } else { + /* Unknown frame count. Need to calculate it. */ + if (!drmp3_get_mp3_and_pcm_frame_count(pMP3, NULL, &totalPCMFrameCount)) { + return 0; + } + + return totalPCMFrameCount; + } +} + +DRMP3_API drmp3_uint64 drmp3_get_mp3_frame_count(drmp3* pMP3) +{ + drmp3_uint64 totalMP3FrameCount; + if (!drmp3_get_mp3_and_pcm_frame_count(pMP3, &totalMP3FrameCount, NULL)) { + return 0; + } + + return totalMP3FrameCount; +} + +static void drmp3__accumulate_running_pcm_frame_count(drmp3* pMP3, drmp3_uint32 pcmFrameCountIn, drmp3_uint64* pRunningPCMFrameCount, float* pRunningPCMFrameCountFractionalPart) +{ + float srcRatio; + float pcmFrameCountOutF; + drmp3_uint32 pcmFrameCountOut; + + srcRatio = (float)pMP3->mp3FrameSampleRate / (float)pMP3->sampleRate; + DRMP3_ASSERT(srcRatio > 0); + + pcmFrameCountOutF = *pRunningPCMFrameCountFractionalPart + (pcmFrameCountIn / srcRatio); + pcmFrameCountOut = (drmp3_uint32)pcmFrameCountOutF; + *pRunningPCMFrameCountFractionalPart = pcmFrameCountOutF - pcmFrameCountOut; + *pRunningPCMFrameCount += pcmFrameCountOut; +} + +typedef struct +{ + drmp3_uint64 bytePos; + drmp3_uint64 pcmFrameIndex; /* <-- After sample rate conversion. */ +} drmp3__seeking_mp3_frame_info; + +DRMP3_API drmp3_bool32 drmp3_calculate_seek_points(drmp3* pMP3, drmp3_uint32* pSeekPointCount, drmp3_seek_point* pSeekPoints) +{ + drmp3_uint32 seekPointCount; + drmp3_uint64 currentPCMFrame; + drmp3_uint64 totalMP3FrameCount; + drmp3_uint64 totalPCMFrameCount; + + if (pMP3 == NULL || pSeekPointCount == NULL || pSeekPoints == NULL) { + return DRMP3_FALSE; /* Invalid args. */ + } + + seekPointCount = *pSeekPointCount; + if (seekPointCount == 0) { + return DRMP3_FALSE; /* The client has requested no seek points. Consider this to be invalid arguments since the client has probably not intended this. */ + } + + /* We'll need to seek back to the current sample after calculating the seekpoints so we need to go ahead and grab the current location at the top. */ + currentPCMFrame = pMP3->currentPCMFrame; + + /* We never do more than the total number of MP3 frames and we limit it to 32-bits. */ + if (!drmp3_get_mp3_and_pcm_frame_count(pMP3, &totalMP3FrameCount, &totalPCMFrameCount)) { + return DRMP3_FALSE; + } + + /* If there's less than DRMP3_SEEK_LEADING_MP3_FRAMES+1 frames we just report 1 seek point which will be the very start of the stream. */ + if (totalMP3FrameCount < DRMP3_SEEK_LEADING_MP3_FRAMES+1) { + seekPointCount = 1; + pSeekPoints[0].seekPosInBytes = 0; + pSeekPoints[0].pcmFrameIndex = 0; + pSeekPoints[0].mp3FramesToDiscard = 0; + pSeekPoints[0].pcmFramesToDiscard = 0; + } else { + drmp3_uint64 pcmFramesBetweenSeekPoints; + drmp3__seeking_mp3_frame_info mp3FrameInfo[DRMP3_SEEK_LEADING_MP3_FRAMES+1]; + drmp3_uint64 runningPCMFrameCount = 0; + float runningPCMFrameCountFractionalPart = 0; + drmp3_uint64 nextTargetPCMFrame; + drmp3_uint32 iMP3Frame; + drmp3_uint32 iSeekPoint; + + if (seekPointCount > totalMP3FrameCount-1) { + seekPointCount = (drmp3_uint32)totalMP3FrameCount-1; + } + + pcmFramesBetweenSeekPoints = totalPCMFrameCount / (seekPointCount+1); + + /* + Here is where we actually calculate the seek points. We need to start by moving the start of the stream. We then enumerate over each + MP3 frame. + */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + + /* + We need to cache the byte positions of the previous MP3 frames. As a new MP3 frame is iterated, we cycle the byte positions in this + array. The value in the first item in this array is the byte position that will be reported in the next seek point. + */ + + /* We need to initialize the array of MP3 byte positions for the leading MP3 frames. */ + for (iMP3Frame = 0; iMP3Frame < DRMP3_SEEK_LEADING_MP3_FRAMES+1; ++iMP3Frame) { + drmp3_uint32 pcmFramesInCurrentMP3FrameIn; + + /* The byte position of the next frame will be the stream's cursor position, minus whatever is sitting in the buffer. */ + DRMP3_ASSERT(pMP3->streamCursor >= pMP3->dataSize); + mp3FrameInfo[iMP3Frame].bytePos = pMP3->streamCursor - pMP3->dataSize; + mp3FrameInfo[iMP3Frame].pcmFrameIndex = runningPCMFrameCount; + + /* We need to get information about this frame so we can know how many samples it contained. */ + pcmFramesInCurrentMP3FrameIn = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFramesInCurrentMP3FrameIn == 0) { + return DRMP3_FALSE; /* This should never happen. */ + } + + drmp3__accumulate_running_pcm_frame_count(pMP3, pcmFramesInCurrentMP3FrameIn, &runningPCMFrameCount, &runningPCMFrameCountFractionalPart); + } + + /* + At this point we will have extracted the byte positions of the leading MP3 frames. We can now start iterating over each seek point and + calculate them. + */ + nextTargetPCMFrame = 0; + for (iSeekPoint = 0; iSeekPoint < seekPointCount; ++iSeekPoint) { + nextTargetPCMFrame += pcmFramesBetweenSeekPoints; + + for (;;) { + if (nextTargetPCMFrame < runningPCMFrameCount) { + /* The next seek point is in the current MP3 frame. */ + pSeekPoints[iSeekPoint].seekPosInBytes = mp3FrameInfo[0].bytePos; + pSeekPoints[iSeekPoint].pcmFrameIndex = nextTargetPCMFrame; + pSeekPoints[iSeekPoint].mp3FramesToDiscard = DRMP3_SEEK_LEADING_MP3_FRAMES; + pSeekPoints[iSeekPoint].pcmFramesToDiscard = (drmp3_uint16)(nextTargetPCMFrame - mp3FrameInfo[DRMP3_SEEK_LEADING_MP3_FRAMES-1].pcmFrameIndex); + break; + } else { + size_t i; + drmp3_uint32 pcmFramesInCurrentMP3FrameIn; + + /* + The next seek point is not in the current MP3 frame, so continue on to the next one. The first thing to do is cycle the cached + MP3 frame info. + */ + for (i = 0; i < DRMP3_COUNTOF(mp3FrameInfo)-1; ++i) { + mp3FrameInfo[i] = mp3FrameInfo[i+1]; + } + + /* Cache previous MP3 frame info. */ + mp3FrameInfo[DRMP3_COUNTOF(mp3FrameInfo)-1].bytePos = pMP3->streamCursor - pMP3->dataSize; + mp3FrameInfo[DRMP3_COUNTOF(mp3FrameInfo)-1].pcmFrameIndex = runningPCMFrameCount; + + /* + Go to the next MP3 frame. This shouldn't ever fail, but just in case it does we just set the seek point and break. If it happens, it + should only ever do it for the last seek point. + */ + pcmFramesInCurrentMP3FrameIn = drmp3_decode_next_frame_ex(pMP3, NULL, NULL, NULL); + if (pcmFramesInCurrentMP3FrameIn == 0) { + pSeekPoints[iSeekPoint].seekPosInBytes = mp3FrameInfo[0].bytePos; + pSeekPoints[iSeekPoint].pcmFrameIndex = nextTargetPCMFrame; + pSeekPoints[iSeekPoint].mp3FramesToDiscard = DRMP3_SEEK_LEADING_MP3_FRAMES; + pSeekPoints[iSeekPoint].pcmFramesToDiscard = (drmp3_uint16)(nextTargetPCMFrame - mp3FrameInfo[DRMP3_SEEK_LEADING_MP3_FRAMES-1].pcmFrameIndex); + break; + } + + drmp3__accumulate_running_pcm_frame_count(pMP3, pcmFramesInCurrentMP3FrameIn, &runningPCMFrameCount, &runningPCMFrameCountFractionalPart); + } + } + } + + /* Finally, we need to seek back to where we were. */ + if (!drmp3_seek_to_start_of_stream(pMP3)) { + return DRMP3_FALSE; + } + if (!drmp3_seek_to_pcm_frame(pMP3, currentPCMFrame)) { + return DRMP3_FALSE; + } + } + + *pSeekPointCount = seekPointCount; + return DRMP3_TRUE; +} + +DRMP3_API drmp3_bool32 drmp3_bind_seek_table(drmp3* pMP3, drmp3_uint32 seekPointCount, drmp3_seek_point* pSeekPoints) +{ + if (pMP3 == NULL) { + return DRMP3_FALSE; + } + + if (seekPointCount == 0 || pSeekPoints == NULL) { + /* Unbinding. */ + pMP3->seekPointCount = 0; + pMP3->pSeekPoints = NULL; + } else { + /* Binding. */ + pMP3->seekPointCount = seekPointCount; + pMP3->pSeekPoints = pSeekPoints; + } + + return DRMP3_TRUE; +} + + +static float* drmp3__full_read_and_close_f32(drmp3* pMP3, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +{ + drmp3_uint64 totalFramesRead = 0; + drmp3_uint64 framesCapacity = 0; + float* pFrames = NULL; + float temp[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + + DRMP3_ASSERT(pMP3 != NULL); + + for (;;) { + drmp3_uint64 framesToReadRightNow = DRMP3_COUNTOF(temp) / pMP3->channels; + drmp3_uint64 framesJustRead = drmp3_read_pcm_frames_f32(pMP3, framesToReadRightNow, temp); + if (framesJustRead == 0) { + break; + } + + /* Reallocate the output buffer if there's not enough room. */ + if (framesCapacity < totalFramesRead + framesJustRead) { + drmp3_uint64 oldFramesBufferSize; + drmp3_uint64 newFramesBufferSize; + drmp3_uint64 newFramesCap; + float* pNewFrames; + + newFramesCap = framesCapacity * 2; + if (newFramesCap < totalFramesRead + framesJustRead) { + newFramesCap = totalFramesRead + framesJustRead; + } + + oldFramesBufferSize = framesCapacity * pMP3->channels * sizeof(float); + newFramesBufferSize = newFramesCap * pMP3->channels * sizeof(float); + if (newFramesBufferSize > (drmp3_uint64)DRMP3_SIZE_MAX) { + break; + } + + pNewFrames = (float*)drmp3__realloc_from_callbacks(pFrames, (size_t)newFramesBufferSize, (size_t)oldFramesBufferSize, &pMP3->allocationCallbacks); + if (pNewFrames == NULL) { + drmp3__free_from_callbacks(pFrames, &pMP3->allocationCallbacks); + pFrames = NULL; + totalFramesRead = 0; + break; + } + + pFrames = pNewFrames; + framesCapacity = newFramesCap; + } + + DRMP3_COPY_MEMORY(pFrames + totalFramesRead*pMP3->channels, temp, (size_t)(framesJustRead*pMP3->channels*sizeof(float))); + totalFramesRead += framesJustRead; + + /* If the number of frames we asked for is less that what we actually read it means we've reached the end. */ + if (framesJustRead != framesToReadRightNow) { + break; + } + } + + if (pConfig != NULL) { + pConfig->channels = pMP3->channels; + pConfig->sampleRate = pMP3->sampleRate; + } + + drmp3_uninit(pMP3); + + if (pTotalFrameCount) { + *pTotalFrameCount = totalFramesRead; + } + + return pFrames; +} + +static drmp3_int16* drmp3__full_read_and_close_s16(drmp3* pMP3, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount) +{ + drmp3_uint64 totalFramesRead = 0; + drmp3_uint64 framesCapacity = 0; + drmp3_int16* pFrames = NULL; + drmp3_int16 temp[1152*2]; /* MP3 frames have a maximum per-channel sample count of 1152. Times 2 to account for stereo. */ + + DRMP3_ASSERT(pMP3 != NULL); + + for (;;) { + drmp3_uint64 framesToReadRightNow = DRMP3_COUNTOF(temp) / pMP3->channels; + drmp3_uint64 framesJustRead = drmp3_read_pcm_frames_s16(pMP3, framesToReadRightNow, temp); + if (framesJustRead == 0) { + break; + } + + /* Reallocate the output buffer if there's not enough room. */ + if (framesCapacity < totalFramesRead + framesJustRead) { + drmp3_uint64 newFramesBufferSize; + drmp3_uint64 oldFramesBufferSize; + drmp3_uint64 newFramesCap; + drmp3_int16* pNewFrames; + + newFramesCap = framesCapacity * 2; + if (newFramesCap < totalFramesRead + framesJustRead) { + newFramesCap = totalFramesRead + framesJustRead; + } + + oldFramesBufferSize = framesCapacity * pMP3->channels * sizeof(drmp3_int16); + newFramesBufferSize = newFramesCap * pMP3->channels * sizeof(drmp3_int16); + if (newFramesBufferSize > (drmp3_uint64)DRMP3_SIZE_MAX) { + break; + } + + pNewFrames = (drmp3_int16*)drmp3__realloc_from_callbacks(pFrames, (size_t)newFramesBufferSize, (size_t)oldFramesBufferSize, &pMP3->allocationCallbacks); + if (pNewFrames == NULL) { + drmp3__free_from_callbacks(pFrames, &pMP3->allocationCallbacks); + pFrames = NULL; + totalFramesRead = 0; + break; + } + + pFrames = pNewFrames; + framesCapacity = newFramesCap; + } + + DRMP3_COPY_MEMORY(pFrames + totalFramesRead*pMP3->channels, temp, (size_t)(framesJustRead*pMP3->channels*sizeof(drmp3_int16))); + totalFramesRead += framesJustRead; + + /* If the number of frames we asked for is less that what we actually read it means we've reached the end. */ + if (framesJustRead != framesToReadRightNow) { + break; + } + } + + if (pConfig != NULL) { + pConfig->channels = pMP3->channels; + pConfig->sampleRate = pMP3->sampleRate; + } + + drmp3_uninit(pMP3); + + if (pTotalFrameCount) { + *pTotalFrameCount = totalFramesRead; + } + + return pFrames; +} + + +DRMP3_API float* drmp3_open_and_read_pcm_frames_f32(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init(&mp3, onRead, onSeek, onTell, NULL, pUserData, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); +} + +DRMP3_API drmp3_int16* drmp3_open_and_read_pcm_frames_s16(drmp3_read_proc onRead, drmp3_seek_proc onSeek, drmp3_tell_proc onTell, void* pUserData, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init(&mp3, onRead, onSeek, onTell, NULL, pUserData, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_s16(&mp3, pConfig, pTotalFrameCount); +} + + +DRMP3_API float* drmp3_open_memory_and_read_pcm_frames_f32(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_memory(&mp3, pData, dataSize, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); +} + +DRMP3_API drmp3_int16* drmp3_open_memory_and_read_pcm_frames_s16(const void* pData, size_t dataSize, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_memory(&mp3, pData, dataSize, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_s16(&mp3, pConfig, pTotalFrameCount); +} + + +#ifndef DR_MP3_NO_STDIO +DRMP3_API float* drmp3_open_file_and_read_pcm_frames_f32(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_file(&mp3, filePath, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_f32(&mp3, pConfig, pTotalFrameCount); +} + +DRMP3_API drmp3_int16* drmp3_open_file_and_read_pcm_frames_s16(const char* filePath, drmp3_config* pConfig, drmp3_uint64* pTotalFrameCount, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + drmp3 mp3; + if (!drmp3_init_file(&mp3, filePath, pAllocationCallbacks)) { + return NULL; + } + + return drmp3__full_read_and_close_s16(&mp3, pConfig, pTotalFrameCount); +} +#endif + +DRMP3_API void* drmp3_malloc(size_t sz, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + return drmp3__malloc_from_callbacks(sz, pAllocationCallbacks); + } else { + return drmp3__malloc_default(sz, NULL); + } +} + +DRMP3_API void drmp3_free(void* p, const drmp3_allocation_callbacks* pAllocationCallbacks) +{ + if (pAllocationCallbacks != NULL) { + drmp3__free_from_callbacks(p, pAllocationCallbacks); + } else { + drmp3__free_default(p, NULL); + } +} + +#endif /* dr_mp3_c */ +#endif /*DR_MP3_IMPLEMENTATION*/ + +/* +DIFFERENCES BETWEEN minimp3 AND dr_mp3 +====================================== +- First, keep in mind that minimp3 (https://github.com/lieff/minimp3) is where all the real work was done. All of the + code relating to the actual decoding remains mostly unmodified, apart from some namespacing changes. +- dr_mp3 adds a pulling style API which allows you to deliver raw data via callbacks. So, rather than pushing data + to the decoder, the decoder _pulls_ data from your callbacks. +- In addition to callbacks, a decoder can be initialized from a block of memory and a file. +- The dr_mp3 pull API reads PCM frames rather than whole MP3 frames. +- dr_mp3 adds convenience APIs for opening and decoding entire files in one go. +- dr_mp3 is fully namespaced, including the implementation section, which is more suitable when compiling projects + as a single translation unit (aka unity builds). At the time of writing this, a unity build is not possible when + using minimp3 in conjunction with stb_vorbis. dr_mp3 addresses this. +*/ + +/* +REVISION HISTORY +================ +v0.7.4 - TBD + - Fix an overflow error with "Xing" and "Info" tag parsing. + - Add some validation checks for "Xing" and "Info" tag parsing. + - Reduce size of some stack allocations. + - Improvements to SIMD detection. + +v0.7.3 - 2026-01-17 + - Fix an error in drmp3_open_and_read_pcm_frames_s16() and family when memory allocation fails. + - Fix some compilation warnings. + +v0.7.2 - 2025-12-02 + - Reduce stack space to improve robustness on embedded systems. + - Fix a compilation error with MSVC Clang toolset relating to cpuid. + - Fix an error with APE tag parsing. + +v0.7.1 - 2025-09-10 + - Silence a warning with GCC. + - Fix an error with the NXDK build. + - Fix a decoding inconsistency when seeking. Prior to this change, reading to the end of the stream immediately after initializing will result in a different number of samples read than if the stream is seeked to the start and read to the end. + +v0.7.0 - 2025-07-23 + - The old `DRMP3_IMPLEMENTATION` has been removed. Use `DR_MP3_IMPLEMENTATION` instead. The reason for this change is that in the future everything will eventually be using the underscored naming convention in the future, so `drmp3` will become `dr_mp3`. + - API CHANGE: Seek origins have been renamed to match the naming convention used by dr_wav and my other libraries. + - drmp3_seek_origin_start -> DRMP3_SEEK_SET + - drmp3_seek_origin_current -> DRMP3_SEEK_CUR + - DRMP3_SEEK_END (new) + - API CHANGE: Add DRMP3_SEEK_END as a seek origin for the seek callback. This is required for detection of ID3v1 and APE tags. + - API CHANGE: Add onTell callback to `drmp3_init()`. This is needed in order to track the location of ID3v1 and APE tags. + - API CHANGE: Add onMeta callback to `drmp3_init()`. This is used for reporting tag data back to the caller. Currently this only reports the raw tag data which means applications need to parse the data themselves. + - API CHANGE: Rename `drmp3dec_frame_info.hz` to `drmp3dec_frame_info.sample_rate`. + - Add detection of ID3v2, ID3v1, APE and Xing/VBRI tags. This should fix errors with some files where the decoder was reading tags as audio data. + - Delay and padding samples from LAME tags are now handled. + - Fix compilation for AIX OS. + +v0.6.40 - 2024-12-17 + - Improve detection of ARM64EC + +v0.6.39 - 2024-02-27 + - Fix a Wdouble-promotion warning. + +v0.6.38 - 2023-11-02 + - Fix build for ARMv6-M. + +v0.6.37 - 2023-07-07 + - Silence a static analysis warning. + +v0.6.36 - 2023-06-17 + - Fix an incorrect date in revision history. No functional change. + +v0.6.35 - 2023-05-22 + - Minor code restructure. No functional change. + +v0.6.34 - 2022-09-17 + - Fix compilation with DJGPP. + - Fix compilation when compiling with x86 with no SSE2. + - Remove an unnecessary variable from the drmp3 structure. + +v0.6.33 - 2022-04-10 + - Fix compilation error with the MSVC ARM64 build. + - Fix compilation error on older versions of GCC. + - Remove some unused functions. + +v0.6.32 - 2021-12-11 + - Fix a warning with Clang. + +v0.6.31 - 2021-08-22 + - Fix a bug when loading from memory. + +v0.6.30 - 2021-08-16 + - Silence some warnings. + - Replace memory operations with DRMP3_* macros. + +v0.6.29 - 2021-08-08 + - Bring up to date with minimp3. + +v0.6.28 - 2021-07-31 + - Fix platform detection for ARM64. + - Fix a compilation error with C89. + +v0.6.27 - 2021-02-21 + - Fix a warning due to referencing _MSC_VER when it is undefined. + +v0.6.26 - 2021-01-31 + - Bring up to date with minimp3. + +v0.6.25 - 2020-12-26 + - Remove DRMP3_DEFAULT_CHANNELS and DRMP3_DEFAULT_SAMPLE_RATE which are leftovers from some removed APIs. + +v0.6.24 - 2020-12-07 + - Fix a typo in version date for 0.6.23. + +v0.6.23 - 2020-12-03 + - Fix an error where a file can be closed twice when initialization of the decoder fails. + +v0.6.22 - 2020-12-02 + - Fix an error where it's possible for a file handle to be left open when initialization of the decoder fails. + +v0.6.21 - 2020-11-28 + - Bring up to date with minimp3. + +v0.6.20 - 2020-11-21 + - Fix compilation with OpenWatcom. + +v0.6.19 - 2020-11-13 + - Minor code clean up. + +v0.6.18 - 2020-11-01 + - Improve compiler support for older versions of GCC. + +v0.6.17 - 2020-09-28 + - Bring up to date with minimp3. + +v0.6.16 - 2020-08-02 + - Simplify sized types. + +v0.6.15 - 2020-07-25 + - Fix a compilation warning. + +v0.6.14 - 2020-07-23 + - Fix undefined behaviour with memmove(). + +v0.6.13 - 2020-07-06 + - Fix a bug when converting from s16 to f32 in drmp3_read_pcm_frames_f32(). + +v0.6.12 - 2020-06-23 + - Add include guard for the implementation section. + +v0.6.11 - 2020-05-26 + - Fix use of uninitialized variable error. + +v0.6.10 - 2020-05-16 + - Add compile-time and run-time version querying. + - DRMP3_VERSION_MINOR + - DRMP3_VERSION_MAJOR + - DRMP3_VERSION_REVISION + - DRMP3_VERSION_STRING + - drmp3_version() + - drmp3_version_string() + +v0.6.9 - 2020-04-30 + - Change the `pcm` parameter of drmp3dec_decode_frame() to a `const drmp3_uint8*` for consistency with internal APIs. + +v0.6.8 - 2020-04-26 + - Optimizations to decoding when initializing from memory. + +v0.6.7 - 2020-04-25 + - Fix a compilation error with DR_MP3_NO_STDIO + - Optimization to decoding by reducing some data movement. + +v0.6.6 - 2020-04-23 + - Fix a minor bug with the running PCM frame counter. + +v0.6.5 - 2020-04-19 + - Fix compilation error on ARM builds. + +v0.6.4 - 2020-04-19 + - Bring up to date with changes to minimp3. + +v0.6.3 - 2020-04-13 + - Fix some pedantic warnings. + +v0.6.2 - 2020-04-10 + - Fix a crash in drmp3_open_*_and_read_pcm_frames_*() if the output config object is NULL. + +v0.6.1 - 2020-04-05 + - Fix warnings. + +v0.6.0 - 2020-04-04 + - API CHANGE: Remove the pConfig parameter from the following APIs: + - drmp3_init() + - drmp3_init_memory() + - drmp3_init_file() + - Add drmp3_init_file_w() for opening a file from a wchar_t encoded path. + +v0.5.6 - 2020-02-12 + - Bring up to date with minimp3. + +v0.5.5 - 2020-01-29 + - Fix a memory allocation bug in high level s16 decoding APIs. + +v0.5.4 - 2019-12-02 + - Fix a possible null pointer dereference when using custom memory allocators for realloc(). + +v0.5.3 - 2019-11-14 + - Fix typos in documentation. + +v0.5.2 - 2019-11-02 + - Bring up to date with minimp3. + +v0.5.1 - 2019-10-08 + - Fix a warning with GCC. + +v0.5.0 - 2019-10-07 + - API CHANGE: Add support for user defined memory allocation routines. This system allows the program to specify their own memory allocation + routines with a user data pointer for client-specific contextual data. This adds an extra parameter to the end of the following APIs: + - drmp3_init() + - drmp3_init_file() + - drmp3_init_memory() + - drmp3_open_and_read_pcm_frames_f32() + - drmp3_open_and_read_pcm_frames_s16() + - drmp3_open_memory_and_read_pcm_frames_f32() + - drmp3_open_memory_and_read_pcm_frames_s16() + - drmp3_open_file_and_read_pcm_frames_f32() + - drmp3_open_file_and_read_pcm_frames_s16() + - API CHANGE: Renamed the following APIs: + - drmp3_open_and_read_f32() -> drmp3_open_and_read_pcm_frames_f32() + - drmp3_open_and_read_s16() -> drmp3_open_and_read_pcm_frames_s16() + - drmp3_open_memory_and_read_f32() -> drmp3_open_memory_and_read_pcm_frames_f32() + - drmp3_open_memory_and_read_s16() -> drmp3_open_memory_and_read_pcm_frames_s16() + - drmp3_open_file_and_read_f32() -> drmp3_open_file_and_read_pcm_frames_f32() + - drmp3_open_file_and_read_s16() -> drmp3_open_file_and_read_pcm_frames_s16() + +v0.4.7 - 2019-07-28 + - Fix a compiler error. + +v0.4.6 - 2019-06-14 + - Fix a compiler error. + +v0.4.5 - 2019-06-06 + - Bring up to date with minimp3. + +v0.4.4 - 2019-05-06 + - Fixes to the VC6 build. + +v0.4.3 - 2019-05-05 + - Use the channel count and/or sample rate of the first MP3 frame instead of DRMP3_DEFAULT_CHANNELS and + DRMP3_DEFAULT_SAMPLE_RATE when they are set to 0. To use the old behaviour, just set the relevant property to + DRMP3_DEFAULT_CHANNELS or DRMP3_DEFAULT_SAMPLE_RATE. + - Add s16 reading APIs + - drmp3_read_pcm_frames_s16 + - drmp3_open_memory_and_read_pcm_frames_s16 + - drmp3_open_and_read_pcm_frames_s16 + - drmp3_open_file_and_read_pcm_frames_s16 + - Add drmp3_get_mp3_and_pcm_frame_count() to the public header section. + - Add support for C89. + - Change license to choice of public domain or MIT-0. + +v0.4.2 - 2019-02-21 + - Fix a warning. + +v0.4.1 - 2018-12-30 + - Fix a warning. + +v0.4.0 - 2018-12-16 + - API CHANGE: Rename some APIs: + - drmp3_read_f32 -> to drmp3_read_pcm_frames_f32 + - drmp3_seek_to_frame -> drmp3_seek_to_pcm_frame + - drmp3_open_and_decode_f32 -> drmp3_open_and_read_pcm_frames_f32 + - drmp3_open_and_decode_memory_f32 -> drmp3_open_memory_and_read_pcm_frames_f32 + - drmp3_open_and_decode_file_f32 -> drmp3_open_file_and_read_pcm_frames_f32 + - Add drmp3_get_pcm_frame_count(). + - Add drmp3_get_mp3_frame_count(). + - Improve seeking performance. + +v0.3.2 - 2018-09-11 + - Fix a couple of memory leaks. + - Bring up to date with minimp3. + +v0.3.1 - 2018-08-25 + - Fix C++ build. + +v0.3.0 - 2018-08-25 + - Bring up to date with minimp3. This has a minor API change: the "pcm" parameter of drmp3dec_decode_frame() has + been changed from short* to void* because it can now output both s16 and f32 samples, depending on whether or + not the DR_MP3_FLOAT_OUTPUT option is set. + +v0.2.11 - 2018-08-08 + - Fix a bug where the last part of a file is not read. + +v0.2.10 - 2018-08-07 + - Improve 64-bit detection. + +v0.2.9 - 2018-08-05 + - Fix C++ build on older versions of GCC. + - Bring up to date with minimp3. + +v0.2.8 - 2018-08-02 + - Fix compilation errors with older versions of GCC. + +v0.2.7 - 2018-07-13 + - Bring up to date with minimp3. + +v0.2.6 - 2018-07-12 + - Bring up to date with minimp3. + +v0.2.5 - 2018-06-22 + - Bring up to date with minimp3. + +v0.2.4 - 2018-05-12 + - Bring up to date with minimp3. + +v0.2.3 - 2018-04-29 + - Fix TCC build. + +v0.2.2 - 2018-04-28 + - Fix bug when opening a decoder from memory. + +v0.2.1 - 2018-04-27 + - Efficiency improvements when the decoder reaches the end of the stream. + +v0.2 - 2018-04-21 + - Bring up to date with minimp3. + - Start using major.minor.revision versioning. + +v0.1d - 2018-03-30 + - Bring up to date with minimp3. + +v0.1c - 2018-03-11 + - Fix C++ build error. + +v0.1b - 2018-03-07 + - Bring up to date with minimp3. + +v0.1a - 2018-02-28 + - Fix compilation error on GCC/Clang. + - Fix some warnings. + +v0.1 - 2018-02-xx + - Initial versioned release. +*/ + +/* +This software is available as a choice of the following licenses. Choose +whichever you prefer. + +=============================================================================== +ALTERNATIVE 1 - Public Domain (www.unlicense.org) +=============================================================================== +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +=============================================================================== +ALTERNATIVE 2 - MIT No Attribution +=============================================================================== +Copyright 2023 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index b4f2e14b..cd789837 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -193,6 +193,24 @@ bool global_settings::ConfigParseAudio(cParser& Parser, const std::string& token return true; } + if (token == "fmradio.enabled") + { + ParseOne(Parser, fmradio_enabled); + return true; + } + + if (token == "fmradio.cleansound") + { + ParseOne(Parser, fmradio_cleansound); + return true; + } + + if (token == "fmradio.stations") + { + fmradio_stations = Parser.getToken(false); + return true; + } + if (token == "sound.volume.vehicle") { ParseOneClamped(Parser, VehicleVolume, 0.f, 1.f); @@ -1555,6 +1573,9 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "sound.volume.positional", EnvironmentPositionalVolume ); export_as_text( Output, "sound.volume.ambient", EnvironmentAmbientVolume ); export_as_text( Output, "sound.asyncload", AudioAsyncLoad ); + export_as_text( Output, "fmradio.enabled", fmradio_enabled ); + export_as_text( Output, "fmradio.cleansound", fmradio_cleansound ); + export_as_text( Output, "fmradio.stations", fmradio_stations ); export_as_text( Output, "physicslog", WriteLogFlag ); export_as_text( Output, "fullphysics", FullPhysics ); export_as_text( Output, "debuglog", iWriteLogEnabled ); diff --git a/utilities/Globals.h b/utilities/Globals.h index ad216cd9..a7eb4d87 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -201,6 +201,10 @@ struct global_settings { int audio_max_sources = 30; std::string AudioRenderer; bool AudioAsyncLoad{ false }; // sound files are decoded by worker threads + // broadcast radio receiver + bool fmradio_enabled{ true }; // allows cab radio receivers to play internet radio streams + bool fmradio_cleansound{ false }; // disables simulation of the receiver speaker + std::string fmradio_stations{ "fmradio.txt" }; // list of stations available to the receivers // input float fMouseXScale{ 1.5f }; float fMouseYScale{ 0.2f }; diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index f0b2367f..7ab5e115 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -483,6 +483,13 @@ TTrain::commandhandler_map const TTrain::m_commandhandlers = { {user_command::radiovolumeincrease, &TTrain::OnCommand_radiovolumeincrease}, {user_command::radiovolumedecrease, &TTrain::OnCommand_radiovolumedecrease}, {user_command::radiovolumeset, &TTrain::OnCommand_radiovolumeset}, + {user_command::fmradiotoggle, &TTrain::OnCommand_fmradiotoggle}, + {user_command::fmradiovolumeincrease, &TTrain::OnCommand_fmradiovolumeincrease}, + {user_command::fmradiovolumedecrease, &TTrain::OnCommand_fmradiovolumedecrease}, + {user_command::fmradiovolumeset, &TTrain::OnCommand_fmradiovolumeset}, + {user_command::fmradiotuneincrease, &TTrain::OnCommand_fmradiotuneincrease}, + {user_command::fmradiotunedecrease, &TTrain::OnCommand_fmradiotunedecrease}, + {user_command::fmradiotuneset, &TTrain::OnCommand_fmradiotuneset}, {user_command::cabchangeforward, &TTrain::OnCommand_cabchangeforward}, {user_command::cabchangebackward, &TTrain::OnCommand_cabchangebackward}, {user_command::generictoggle0, &TTrain::OnCommand_generictoggle}, @@ -8186,6 +8193,58 @@ void TTrain::OnCommand_radiovolumeset(TTrain *Train, command_data const &Command } } +void TTrain::OnCommand_fmradiotoggle(TTrain *Train, command_data const &Command) +{ + if (Command.action == GLFW_PRESS && Train->m_fmradiopresent) + { + Train->m_fmradiopower = !Train->m_fmradiopower; + } +} + +void TTrain::OnCommand_fmradiovolumeincrease(TTrain *Train, command_data const &Command) +{ + // the knob keeps turning for as long as the control is held, the actual movement takes place in the update + Train->m_fmradiovolumechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? 1 : 0); +} + +void TTrain::OnCommand_fmradiovolumedecrease(TTrain *Train, command_data const &Command) +{ + Train->m_fmradiovolumechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? -1 : 0); +} + +void TTrain::OnCommand_fmradiovolumeset(TTrain *Train, command_data const &Command) +{ + if (Command.action != GLFW_RELEASE && Train->m_fmradiopresent) + { + Train->m_fmradiovolume = std::clamp(static_cast(Command.param1), 0.f, 1.f); + if (false == Train->m_controlmapper.contains("fmradio_sw:")) + { + // receivers without dedicated switch are switched with the volume knob + Train->m_fmradiopower = Train->m_fmradiovolume > 0.f; + } + } +} + +void TTrain::OnCommand_fmradiotuneincrease(TTrain *Train, command_data const &Command) +{ + Train->m_fmradiotunechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? 1 : 0); +} + +void TTrain::OnCommand_fmradiotunedecrease(TTrain *Train, command_data const &Command) +{ + Train->m_fmradiotunechange = (Command.action != GLFW_RELEASE && Train->m_fmradiopresent ? -1 : 0); +} + +// sets the receiver to frequency specified in MHz +void TTrain::OnCommand_fmradiotuneset(TTrain *Train, command_data const &Command) +{ + if (Command.action != GLFW_RELEASE && Train->m_fmradiopresent) + { + Train->m_fmradiodial = std::clamp(100.f * (static_cast(Command.param1) - audio::EU07_FMRADIO_BANDSTART) / (audio::EU07_FMRADIO_BANDEND - audio::EU07_FMRADIO_BANDSTART), 0.f, 100.f); + Train->m_fmradiotuned = true; + } +} + void TTrain::OnCommand_cabchangeforward(TTrain *Train, command_data const &Command) { @@ -10040,6 +10099,7 @@ void TTrain::update_sounds(double const Deltatime) } update_sounds_radio(); + update_sounds_fmradio(Deltatime); } void TTrain::update_sounds_resonancenoise(sound_source &Sound) const @@ -10132,6 +10192,52 @@ void TTrain::update_sounds_radio() } } +// operates the broadcast radio receiver +void TTrain::update_sounds_fmradio(double const Deltatime) +{ + // NOTE: there's a single receiver in the simulation, it plays in the cab occupied by the user + if (simulation::Train != this || false == m_fmradiopresent || false == Global.fmradio_enabled) + { + return; + } + auto &fmradio{audio::renderer.fmradio()}; + auto const bandwidth{audio::EU07_FMRADIO_BANDEND - audio::EU07_FMRADIO_BANDSTART}; + if (false == m_fmradiotuned) + { + // initially the receiver is tuned to the first station on the list + m_fmradiodial = 100.f * (fmradio.default_frequency() - audio::EU07_FMRADIO_BANDSTART) / bandwidth; + m_fmradiotuned = true; + } + // knobs + if (m_fmradiovolumechange != 0) + { + m_fmradiovolume = std::clamp(m_fmradiovolume + m_fmradiovolumechange * 0.5f * static_cast(Deltatime), 0.f, 1.f); + if (false == m_controlmapper.contains("fmradio_sw:")) + { + // receivers without dedicated switch are switched with the volume knob + m_fmradiopower = m_fmradiovolume > 0.f; + } + } + if (m_fmradiotunechange != 0) + { + // the knob moves slowly at first to allow fine tuning, then speeds up + m_fmradiotunetime += static_cast(Deltatime); + auto const tunerate{std::lerp(0.15f, 2.5f, std::clamp(m_fmradiotunetime * 0.5f, 0.f, 1.f))}; // MHz per second + m_fmradiodial = std::clamp(m_fmradiodial + 100.f * m_fmradiotunechange * tunerate * static_cast(Deltatime) / bandwidth, 0.f, 100.f); + } + else + { + m_fmradiotunetime = 0.f; + } + + fmradio.type(m_fmradiotype); + fmradio.tune(audio::EU07_FMRADIO_BANDSTART + m_fmradiodial * 0.01f * bandwidth); + fmradio.volume(m_fmradiovolume); + fmradio.placement(m_fmradiosound.location(), m_fmradiosound.soundproofing_factor(), m_fmradiosound.range()); + // NOTE: the receiver runs on batteries of its own, so it doesn't care about the state of the vehicle + fmradio.power(m_fmradiopower); +} + void TTrain::update_screens(double dt) { for (auto &screen : m_screens) @@ -10234,6 +10340,7 @@ bool TTrain::LoadMMediaFile(std::string const &asFileName) } // NOTE: since radiosound is an incomplete template not using std::optional it gets a special treatment m_radiosound.owner(DynamicObject); + m_fmradiosound.owner(DynamicObject); CabSoundLocations.clear(); cParser parser(asFileName, cParser::buffer_FILE, DynamicObject->asBaseDir, true, std::vector(), true); @@ -10342,6 +10449,8 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName) } } m_radiosound.offset(nullvector); + m_fmradiosound.offset(nullvector); + m_fmradiopresent = false; for (auto const &[soundsource, soundoffset] : CabSoundLocations) { if (soundsource.get() && soundsource.get()->offset() == nullvector) @@ -10567,6 +10676,39 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName) parser.getTokens(); parser >> ScreenUpdateRate; } + else if (token == "fmradio:") + { + // broadcast radio receiver, the parameter selects character of its sound + std::string fmradiotype; + parser.getTokens(); + parser >> fmradiotype; + m_fmradiotype = (fmradiotype == "wooden" ? audio::fmradio_type::wooden : audio::fmradio_type::cheap); + m_fmradiopresent = true; + } + else if (token == "fmradiospeaker:") + { + // submodel marking location of the receiver speaker, the sound is emitted from there + std::string speakername; + parser.getTokens(); + parser >> speakername; + TSubModel *speaker{nullptr}; + for (auto const *source : {DynamicObject->mdKabina, DynamicObject->mdLowPolyInt}) + { + if (source != nullptr && (speaker = source->GetFromName(speakername)) != nullptr) + { + break; + } + } + if (speaker != nullptr) + { + m_fmradiosound.offset(speaker->offset(1.f)); + } + else + { + ErrorLog("Bad config: failed to locate fm radio speaker submodel \"" + speakername + "\"", logtype::model); + } + m_fmradiopresent = true; + } // btLampkaUnknown`"unknown",mdKabina,false); } while (token != "" // TODO: enable full per-cab deserialization when/if .mmd files get proper per-cab switch configuration @@ -10608,6 +10750,15 @@ bool TTrain::InitializeCab(int NewCabNo, std::string const &asFileName) { m_radiosound.offset(caboffset); } + if (m_fmradiosound.offset() == nullvector) + { + m_fmradiosound.offset(caboffset); + } + if (m_fmradiopresent && false == m_fmradiopower && m_controlmapper.contains("fmradiovolume_sw:") && false == m_controlmapper.contains("fmradio_sw:")) + { + // receivers without dedicated switch are switched with the volume knob, so it has to start at the off position + m_fmradiovolume = 0.f; + } std::vector>, glm::vec3>> soundlocations = { {dsbReverserKey, ggDirKey.model_offset()}, {dsbNastawnikJazdy, ggJointCtrl.model_offset()}, @@ -11947,6 +12098,33 @@ bool TTrain::initialize_gauge(cParser &Parser, std::string const &Label, int con } } + // broadcast radio receiver + if (Label == "fmradio_sw:" || Label == "fmradiovolume_sw:" || Label == "fmradiotune_sw:" || Label == "fmradioscale:") + { + auto &gauge = Cabine[Cabindex].Gauge(-1); // pierwsza wolna gałka + gauge.Load(Parser, DynamicObject); + if (Label == "fmradio_sw:") + { + gauge.AssignBool(&m_fmradiopower); + } + else + { + gauge.AssignFloat(Label == "fmradiovolume_sw:" ? &m_fmradiovolume : &m_fmradiodial); + } + if (Label != "fmradioscale:") + { + // the scale pointer is an indicator, the rest are controls + m_controlmapper.insert(gauge, Label); + } + m_fmradiopresent = true; + // unless the speaker location is defined explicitly, the sound comes from the first defined element of the receiver + if (m_fmradiosound.offset() == glm::vec3()) + { + m_fmradiosound.offset(gauge.model_offset()); + } + return true; + } + // ABu 090305: uniwersalne przyciski lub inne rzeczy if (Label == "mainctrlact:") { diff --git a/vehicle/Train.h b/vehicle/Train.h index 4fc11dc2..7a5f5c53 100644 --- a/vehicle/Train.h +++ b/vehicle/Train.h @@ -216,6 +216,7 @@ class TTrain { void update_sounds_runningnoise( sound_source &Sound ); void update_sounds_resonancenoise( sound_source &Sound ) const; void update_sounds_radio(); + void update_sounds_fmradio( double const Deltatime ); inline end cab_to_end( int const End ) const { return End == 2 ? end::rear : end::front; } @@ -485,6 +486,13 @@ class TTrain { static void OnCommand_radiovolumeincrease(TTrain *Train, command_data const &Command); static void OnCommand_radiovolumedecrease(TTrain *Train, command_data const &Command); static void OnCommand_radiovolumeset(TTrain *Train, command_data const &Command); + static void OnCommand_fmradiotoggle(TTrain *Train, command_data const &Command); + static void OnCommand_fmradiovolumeincrease(TTrain *Train, command_data const &Command); + static void OnCommand_fmradiovolumedecrease(TTrain *Train, command_data const &Command); + static void OnCommand_fmradiovolumeset(TTrain *Train, command_data const &Command); + static void OnCommand_fmradiotuneincrease(TTrain *Train, command_data const &Command); + static void OnCommand_fmradiotunedecrease(TTrain *Train, command_data const &Command); + static void OnCommand_fmradiotuneset(TTrain *Train, command_data const &Command); static void OnCommand_cabchangeforward( TTrain *Train, command_data const &Command ); static void OnCommand_cabchangebackward( TTrain *Train, command_data const &Command ); static void OnCommand_generictoggle( TTrain *Train, command_data const &Command ); @@ -919,6 +927,17 @@ private: bool m_couplingdisconnect { false }; bool m_couplingdisconnectback { false }; float m_radiovolume { Global.DefaultRadioVolume }; + // broadcast radio receiver + bool m_fmradiopresent { false }; // the cab is equipped with the receiver + audio::fmradio_type m_fmradiotype { audio::fmradio_type::cheap }; + bool m_fmradiopower { false }; + bool m_fmradiotuned { false }; // helper, the tuning knob was set to its initial position + float m_fmradiovolume { 0.5f }; // position of the volume knob, in range 0-1 + float m_fmradiodial { 50.f }; // position of the tuning knob, in range 0-100 covering the whole band. NOTE: gauges ignore changes of bound value smaller than 0.01, hence the wide range + int m_fmradiovolumechange { 0 }; // direction in which the volume knob is being turned + int m_fmradiotunechange { 0 }; // direction in which the tuning knob is being turned + float m_fmradiotunetime { 0.f }; // how long the tuning knob is being turned, it gets faster with time + sound_source m_fmradiosound { sound_placement::internal, 2 * EU07_SOUND_CABCONTROLSCUTOFFRANGE }; // provides location and soundproofing of the receiver public: float fPress[20][7]; // cisnienia dla wszystkich czlonow From f30fefd91cd8fe4023e6aa6e183c0ec3b0537197 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:15:00 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Sie=C4=87=20trakcyjna:=20prz=C4=99s=C5=82a?= =?UTF-8?q?=20wg=20typ=C3=B3w=20sieci,=20wysi=C4=99gniki=20z=20katalogu,?= =?UTF-8?q?=20bramki,=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 From 619f048f3118672f6a839593f6a93f17d66a8495 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 01:12:06 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Sie=C4=87=20trakcyjna:=20kotwienia,=20kotwi?= =?UTF-8?q?enie=20=C5=9Brodkowe,=20parasole=20i=20wysi=C4=99gniki=20rozsta?= =?UTF-8?q?wione,=20konsole=204890/4690?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sam kod exe z gałęzi siec-trakcyjna od poprzedniego commita (bez testów, notatek i danych z tests/catenary). Dane (data/traction_*.txt) leżą poza repozytorium. - kotwienia ciężarowe i stałe (0404/0405, przewody zakotwione osobno, sieci bez własnych danych: network as ) - kotwienie środkowe 0406/0408/0418/0416: lina, V i zaciski na linie nośnej liczonej jak przęsło, obejma 4160 na teownikach - parasole i wysięgniki rozstawione na konsolach 4890, wysięgi pomocnicze 0167 na belkach 4690, trzeci wysięgnik w osi słupa, lina nośna z innego wysięgnika (messenger_from) - przęsła w nietypowych sytuacjach nie przechodzą na stary wygląd (bez Y, bez wieszaków, lina nad przewodem) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GfTogWDsfCE8RGFGvm2eyp --- application/editormodecatenary.cpp | 1627 ++++++++++++++++++++++++++-- application/editormodecatenary.h | 56 +- lang/editor_pl.po | 357 ++++++ rendering/opengl33renderer.cpp | 7 + rendering/openglrenderer.cpp | 7 + world/Traction.cpp | 18 +- world/catenary_cantilever.cpp | 71 +- world/catenary_cantilever.h | 18 + world/catenary_network.cpp | 65 +- world/catenary_network.h | 5 + world/catenary_scene.cpp | 999 ++++++++++++++++- world/catenary_scene.h | 21 + world/catenary_span.cpp | 51 +- world/catenary_span.h | 14 +- world/catenary_suspension.cpp | 53 +- world/catenary_suspension.h | 33 +- world/catenary_tensioner.cpp | 970 +++++++++++++++-- world/catenary_tensioner.h | 150 ++- 18 files changed, 4237 insertions(+), 285 deletions(-) diff --git a/application/editormodecatenary.cpp b/application/editormodecatenary.cpp index 4286ec23..261acfb8 100644 --- a/application/editormodecatenary.cpp +++ b/application/editormodecatenary.cpp @@ -45,6 +45,8 @@ double const kSpanSearch{150.0}; // m, the farthest a neighbouring pole of the s 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 kInsertWidth{1.5}; // m in the plan from the line of a span: a pole placed there goes into the span +double const kInsertEnds{2.0}; // m from the ends of the span where it doesn't 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) @@ -61,7 +63,18 @@ double nearest_parameter(TTrack const &Track, int const Path, glm::dvec3 const & parameter = k / 256.0; } } - return parameter; + // on a long path a step of the search is metres: refined around the best one, so that a point square to the track (a pole + // given by another cantilever on it) gets its own foot on the track and not one a metre off + auto low{std::max(0.0, parameter - 1.0 / 256.0)}, high{std::min(1.0, parameter + 1.0 / 256.0)}; + for (int k = 0; k < 40; ++k) + { + auto const a{low + (high - low) / 3.0}, b{high - (high - low) / 3.0}; + if (geometry::plan_distance(curve.point(a), Point) < geometry::plan_distance(curve.point(b), Point)) + high = b; + else + low = a; + } + return 0.5 * (low + high); } glm::dvec2 forward_at(TTrack const &Track, int const Path, double const Parameter) @@ -87,6 +100,23 @@ glm::dvec3 wire_of(catenary::suspension const &Data) return {Data.at[0], Data.at[1], Data.at[2]}; } +// the suspensions on the pole of this one (the same type at the same point, as the scenery pairs an umbrella): this one too +std::vector pole_sharers(std::size_t const Index) +{ + std::vector list; + auto const *own{catenary_scene_get(Index)}; + if (own == nullptr || false == own->has_pole) + return list; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data != nullptr && data->has_pole && data->pole_type == own->pole_type + && glm::length(glm::dvec3{data->pole[0] - own->pole[0], data->pole[1] - own->pole[1], data->pole[2] - own->pole[2]}) < 0.05) + list.push_back(index); + } + return list; +} + // from the pole towards the wires, in the plan; across the track glm::dvec2 across_of(catenary::suspension const &Data) { @@ -404,6 +434,11 @@ bool catenary_editor::catenary_spot_at(TTrack &Track, int const Path, glm::dvec3 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); + { + // the centre of the bend is on the side of the chord's middle + auto const bend{glm::dot(0.5 * (plan_of(before) + plan_of(after)) - plan_of(Spot.axis), right)}; + Spot.inward = std::abs(bend) < 1e-6 ? 0 : bend > 0.0 ? 1 : -1; + } switch (m_catenary.bend) { case 1: Spot.curve = false; break; @@ -812,6 +847,32 @@ bool catenary_editor::catenary_switch_error(std::size_t const Index) const return check.error; } +double catenary_editor::catenary_cant(catenary_spot const &Spot) +{ + if (Spot.track == nullptr || Spot.path < 0 || Spot.path >= static_cast(Spot.track->m_paths.size())) + return 0.0; + auto const &rolls{Spot.track->m_paths[Spot.path].rolls}; + return std::abs(glm::radians(glm::mix(static_cast(rolls[0]), static_cast(rolls[1]), std::clamp(Spot.parameter, 0.0, 1.0)))); +} + +glm::dvec3 catenary_editor::catenary_cant_shift(catenary_spot const &Spot, double const Height) +{ + auto const cant{catenary_cant(Spot)}; + if (cant < 1e-4) + return glm::dvec3{0.0}; + // towards the centre of the bend; on a cant without a bend (shouldn't be) by the sign of the roll: a positive one lifts the left rail + auto side{Spot.inward}; + if (side == 0) + { + auto const &rolls{Spot.track->m_paths[Spot.path].rolls}; + side = glm::mix(rolls[0], rolls[1], static_cast(Spot.parameter)) > 0.0f ? 1 : -1; + } + auto const right{right_of(Spot.forward) * static_cast(side)}; + auto const shift{Height * std::sin(cant)}; + // the rail heads: the lower rail stays at the points of the track, the middle goes up by half the cant (TSegment::Init) + return glm::dvec3{right.x * shift, 0.75 * std::sin(cant), right.y * shift}; +} + 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}; @@ -832,8 +893,9 @@ void catenary_editor::catenary_compose(catenary_spot &Spot, double const Clearan 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); + // on a superelevated track the stagger is measured from the leaning axis of the track (table 23-0007) + auto const wire{glm::dvec3{Spot.axis.x, railhead + height, Spot.axis.z} + catenary_cant_shift(Spot, height) + out * offset}; + Spot.reach = glm::dot(plan_of(polepoint) - plan_of(wire), plan_of(out)) - 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()}; @@ -935,7 +997,8 @@ catenary_editor::catenary_spot const *catenary_editor::catenary_spot_of(std::siz } // 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; + auto const lean{catenary_cant_shift(spot, wire.y - (spot.axis.y + kRailTop))}; + spot.toward = glm::dot(plan_of(wire) - plan_of(spot.axis + lean), right) >= 0.0; spot.data = *data; spot.card = data->card; auto const decade{(data->card / 10) % 10}; @@ -946,7 +1009,9 @@ catenary_editor::catenary_spot const *catenary_editor::catenary_spot_of(std::siz 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 where{Spot.curve ? format(STR_C("curve R %.0f m"), Spot.radius) : std::string{STR("straight")}}; + if (auto const cant{catenary_cant(Spot)}; cant >= 1e-4) + where += format(STR_C(", superelevation %.0f mm"), 1500.0 * std::sin(cant)); 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()); @@ -976,7 +1041,13 @@ int catenary_editor::catenary_hit() const 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})}; + // the pole, or the cantilever from it to the wires (two cantilevers of an umbrella or spread ones share one pole: the + // cantilever tells them apart, and wins a tie with the pole) + auto const wire{wire_of(*data)}; + auto const plane{data->has_spread ? glm::dvec3{data->spread[0], foot.y, data->spread[2]} : foot}; + glm::dvec3 const root{plane.x, wire.y, plane.z}; + auto const arm{mouse_to_segment(projection, root, root + (wire - root) * 0.85)}; // not up to the wires: the span is picked there + auto const distance{std::min(mouse_to_segment(projection, foot, foot + glm::dvec3{0.0, pole_top(*data), 0.0}), arm * 0.999f)}; if (distance < best) { best = distance; @@ -1015,9 +1086,36 @@ int catenary_editor::catenary_span_hit() const return result; } +// the anchor whose device is under the cursor: the line from the face of the pole to the moving pulley, or the column of weights +int catenary_editor::catenary_anchor_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 index = 0; index < catenary_anchor_size(); ++index) + { + double shape[4][3]; + if (false == catenary_anchor_shape(index, shape)) + continue; + glm::dvec3 const face{shape[0][0], shape[0][1], shape[0][2]}, lever{shape[1][0], shape[1][1], shape[1][2]}; + glm::dvec3 const top{shape[2][0], shape[2][1], shape[2][2]}, bottom{shape[3][0], shape[3][1], shape[3][2]}; + if (glm::distance(face, camera) > kCatenaryRange) + continue; + auto const distance{std::min(mouse_to_segment(projection, face, lever), mouse_to_segment(projection, top, bottom))}; + if (distance < best) + { + best = distance; + result = static_cast(index); + } + } + return result; +} + void catenary_editor::catenary_select(int const Index) { auto &tool{m_catenary}; + tool.anchor_selected = -1; tool.selected = Index; tool.probed = false; auto const *data{Index >= 0 ? catenary_scene_get(static_cast(Index)) : nullptr}; @@ -1089,6 +1187,9 @@ void catenary_editor::catenary_apply(catenary_spot const &Spot) data.skin = current->skin; data.overrides = current->overrides; data.umbrella = current->umbrella; + data.has_spread = current->has_spread; + data.shared_pole = current->shared_pole; + std::copy(std::begin(current->spread), std::end(current->spread), data.spread); // 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()) @@ -1128,6 +1229,8 @@ void catenary_editor::catenary_copy(std::vector const &Targets) 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 if (false == drop && pole_sharers(index).size() > 1) + catenary_keep_pole(spot, *current, std::abs(offset), offset >= 0.0); else catenary_compose(spot, tool.clearance, std::abs(offset), offset >= 0.0, drop ? current->pole_type : std::string{}); if (drop) @@ -1144,6 +1247,9 @@ void catenary_editor::catenary_copy(std::vector const &Targets) data.skin = current->skin; data.overrides = current->overrides; data.umbrella = current->umbrella; + data.has_spread = current->has_spread; + data.shared_pole = current->shared_pole; + std::copy(std::begin(current->spread), std::end(current->spread), data.spread); tool.undo.push_back({tool.group, false, index, *current, std::nullopt, false}); catenary_scene_update(index, data); tool.spots[index] = spot; @@ -1194,12 +1300,108 @@ bool catenary_editor::catenary_propose_toward(bool &Toward) return true; } +bool catenary_editor::catenary_insert_at(catenary_spot const &Spot, chain_insert &Insert) const +{ + auto const &tool{m_catenary}; + if (false == tool.insert_spans || false == Spot.data.has_at || (tool.page == 1 && tool.gantry_selected >= 0)) + return false; + auto const wire{plan_of(wire_of(Spot.data))}; + glm::dvec3 const camera{Global.pCamera.Pos}; + auto best{kInsertWidth}; + auto found{false}; + 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 || false == a->has_at || false == b->has_at || glm::distance(wire_of(*a), camera) > kCatenaryRange) + continue; + auto const pa{plan_of(wire_of(*a))}, pb{plan_of(wire_of(*b))}; + auto const span{pb - pa}; + auto const length{glm::length(span)}; + if (length < 2.0 * kInsertEnds) + continue; + auto const along{glm::dot(wire - pa, span / length)}; + // not at the ends: a pole there is the pole of the end, or the next one past it + if (along < kInsertEnds || along > length - kInsertEnds) + continue; + auto const aside{std::abs(glm::dot(wire - pa, glm::dvec2{-span.y, span.x} / length))}; + if (aside < best) + { + best = aside; + Insert.chain = static_cast(chain); + Insert.after = k; + Insert.from = members[k]; + Insert.to = members[k + 1]; + found = true; + } + } + } + return found; +} + +void catenary_editor::catenary_chain_end() +{ + auto &tool{m_catenary}; + tool.chain = -1; + tool.last = -1; + tool.connect_from = -1; + tool.anchor_pick = -1; + tool.status = STR("The chain is ended: the next pole starts a new one"); +} + +bool catenary_editor::catenary_chain_continue(std::size_t const Pole) +{ + auto &tool{m_catenary}; + auto const *data{catenary_scene_get(Pole)}; + if (data == nullptr || false == data->has_at) + return false; + std::size_t position{0}; + auto const chain{catenary_chain_of(Pole, position)}; + if (chain >= 0) + { + auto const members{catenary_chain_members(static_cast(chain))}; + auto const atend{position + 1 == members.size()}; + if (position != 0 && false == atend) + { + tool.status = STR("The pole is in the middle of a chain: the chain goes on from its ends only"); + return false; + } + tool.chain = chain; + tool.append = atend; + tool.chain_selected = chain; + } + else + tool.chain = -1; // a new chain from this pole + // the next poles go to the file of this one: a chain is in one file + std::snprintf(tool.file, sizeof(tool.file), "%s", catenary_scene_file_of(Pole).c_str()); + tool.last = static_cast(Pole); + tool.chaining = true; + tool.connect_from = -1; + tool.anchor_pick = -1; + tool.anchor_selected = -1; + tool.selected = -1; + tool.probed = false; + tool.page = 0; + if (bool toward{tool.toward}; catenary_propose_toward(toward)) + tool.toward = toward; + tool.status = format(STR_C("The next poles go on from %s: LMB by the track; Esc ends the chain"), data->name.empty() ? "-" : data->name.c_str()); + 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}; + // in a span of a chain: the pole goes into the chain between the two, in the file of the chain + chain_insert insert; + auto const inserting{catenary_insert_at(Spot, insert)}; + std::string const file{inserting ? catenary_chain_file(static_cast(insert.chain)) : std::string{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)]) @@ -1212,6 +1414,23 @@ void catenary_editor::catenary_place(catenary_spot const &Spot) 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"); + if (inserting) + { + // the chain going on (tool.last) stays as it was + if (auto const *chain{catenary_chain_get(static_cast(insert.chain))}; chain != nullptr && insert.after + 1 < chain->suspensions.size()) + { + auto changed{*chain}; + tool.undo.push_back({tool.group, true, static_cast(insert.chain), std::nullopt, *chain, false}); + changed.suspensions.insert(changed.suspensions.begin() + static_cast(insert.after + 1), data.name); + auto const name{changed.name}; + catenary_chain_update(static_cast(insert.chain), changed); + tool.chain_selected = insert.chain; + auto const *a{catenary_scene_get(insert.from)}; + auto const *b{catenary_scene_get(insert.to)}; + tool.status += format(STR_C("; put into chain %s between %s and %s"), name.c_str(), a != nullptr ? a->name.c_str() : "-", b != nullptr ? b->name.c_str() : "-"); + } + return; + } // 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) @@ -1259,6 +1478,352 @@ void catenary_editor::catenary_place(catenary_spot const &Spot) tool.toward = toward; } +bool catenary_editor::catenary_umbrella_spot(TTrack &Track, int const Path, catenary_spot &Spot) +{ + auto &tool{m_catenary}; + auto const *source{tool.umbrella_from >= 0 ? catenary_scene_get(static_cast(tool.umbrella_from)) : nullptr}; + auto const *catalog{catenary_scene_catalog()}; + auto const *pole{source != nullptr && catalog != nullptr ? catalog->pole(source->pole_type) : nullptr}; + if (pole == nullptr || false == source->has_pole || false == source->has_at) + return false; + auto const foot{pole_of(*source)}; + { + // square to the pole on this track, on the side of the pole; not onto a drop of the selected gantry + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(Track, Path, foot, foot, 0, Spot)) + return false; + } + // the cantilever on the same pole (or drop): its distance is the one of the pole, the panel gives the rest + auto const right{right_of(Spot.forward) * static_cast(Spot.side)}; + auto const clearance{glm::dot(plan_of(foot) - plan_of(Spot.axis), right) - 0.5 * pole->depth(0.0)}; + auto const stagger{Spot.curve ? std::max(static_cast(tool.stagger), catenary_scene_layout().curve_stagger()) : static_cast(tool.stagger)}; + catenary_compose(Spot, clearance, stagger, Spot.toward, source->pole_type); + std::copy(std::begin(source->pole), std::end(source->pole), Spot.data.pole); + if (Spot.card == 0) + return true; + // the second cantilever goes the other way from the pole than the first one + auto const mine{plan_of(wire_of(Spot.data)) - plan_of(foot)}; + auto const other{plan_of(wire_of(*source)) - plan_of(foot)}; + if (glm::dot(mine, other) > 0.0) + { + Spot.card = 0; + Spot.problem = STR("This track is on the same side of the pole as its cantilever: the second cantilever of an umbrella goes to a track on the other side"); + } + return true; +} + +void catenary_editor::catenary_umbrella_place(catenary_spot const &Spot) +{ + auto &tool{m_catenary}; + auto const from{static_cast(tool.umbrella_from)}; + auto const *source{tool.umbrella_from >= 0 ? catenary_scene_get(from) : nullptr}; + tool.umbrella_from = -1; + if (source == nullptr) + return; + ++tool.group; + auto const file{catenary_scene_file_of(from)}; + auto sourcedata{*source}; + tool.undo.push_back({tool.group, false, from, sourcedata, std::nullopt, false}); + if (sourcedata.name.empty()) + sourcedata.name = catenary_scene_new_name(file); // a pole from before the editor had no name + // a name of the umbrella no other suspension has + std::string name; + for (int k = 1; name.empty(); ++k) + { + auto const candidate{"parasol_" + sourcedata.name + (k > 1 ? "_" + std::to_string(k) : std::string{})}; + auto used{false}; + for (std::size_t index = 0; index < catenary_scene_size() && false == used; ++index) + if (auto const *data{catenary_scene_get(index)}; data != nullptr && data->umbrella == candidate) + used = true; + if (false == used) + name = candidate; + } + // the first one named first, so that the second gets a name of its own + sourcedata.umbrella = name; + catenary_scene_update(from, sourcedata); + if (auto found{tool.spots.find(from)}; found != tool.spots.end()) + found->second.data = sourcedata; + auto data{Spot.data}; + data.name = catenary_scene_new_name(file); + data.skin = sourcedata.skin; + data.umbrella = name; + 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}); + catenary_select(static_cast(index)); + tool.status = format(STR_C("Umbrella %s: the second cantilever (card %d execution %d, A = %.2f m) on pole %s of %s; hang its wires with the button below or C"), name.c_str(), Spot.card, data.execution, Spot.reach, data.pole_type.c_str(), sourcedata.name.c_str()); + if (catenary_scene_parts(index) == 0) + tool.status += STR(" - the cantilever didn't assemble, see the log"); +} + +void catenary_editor::catenary_keep_pole(catenary_spot &Spot, catenary::suspension const &Current, double const Stagger, bool const Toward) const +{ + auto const *catalog{catenary_scene_catalog()}; + auto const *own{catalog != nullptr ? catalog->pole(Current.pole_type) : nullptr}; + if (own == nullptr) + { + Spot.card = 0; + Spot.problem = STR("No pole: the catalogue (data/traction_cantilevers.txt, data/traction_supports.txt) isn't loaded or has no poles"); + return; + } + // the plane of the cantilever: through the pole, or through its mount on the console of spread cantilevers + auto const plane{Current.has_spread ? glm::dvec3{Current.spread[0], Current.pole[1], Current.spread[2]} : pole_of(Current)}; + auto const right{right_of(Spot.forward) * static_cast(Spot.side)}; + catenary_compose(Spot, glm::dot(plan_of(plane) - plan_of(Spot.axis), right) - 0.5 * own->depth(0.0), Stagger, Toward, Current.pole_type); + std::copy(std::begin(Current.pole), std::end(Current.pole), Spot.data.pole); + Spot.data.has_spread = Current.has_spread; + std::copy(std::begin(Current.spread), std::end(Current.spread), Spot.data.spread); +} + +void catenary_editor::catenary_spread_create(std::size_t const Index) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Index)}; + auto const *found{catenary_spot_of(Index)}; + auto const *catalog{catenary_scene_catalog()}; + auto const *pole{current != nullptr && catalog != nullptr ? catalog->pole(current->pole_type) : nullptr}; + // a cantilever without strut and stay (spread steady arms 0167) goes on the beam 4690, the others on the console 4890 + auto const *scheme{current != nullptr && catalog != nullptr ? catalog->scheme(current->card, current->execution) : nullptr}; + auto const steadyonly{scheme != nullptr && scheme->roles.count("strut") == 0 && scheme->roles.count("stay") == 0}; + auto const consolename{pole == nullptr ? std::string{} : steadyonly && false == pole->steady_console.empty() ? pole->steady_console : pole->console}; + auto const *console{false == consolename.empty() ? catalog->mount(consolename) : nullptr}; + if (current == nullptr || found == nullptr || false == current->has_pole || false == current->umbrella.empty()) + return; + if (console == nullptr || false == console->console) + { + tool.status = format(STR_C("Pole %s carries no console for spread cantilevers (\"console\" in its entry in traction_supports.txt)"), current->pole_type.c_str()); + return; + } + auto const sourcedata{*current}; + auto const spot{*found}; + auto const foot{pole_of(sourcedata)}; + // the axis of the console mounts (the X axis of the cantilevers): along the track, square to the way from the track to the pole + auto const out{right_of(spot.forward) * static_cast(spot.side)}; + glm::dvec3 const xaxis{-out.y, 0.0, out.x}; + auto const right{right_of(spot.forward) * static_cast(spot.side)}; + auto const offset{glm::dot(plan_of(wire_of(sourcedata)) - plan_of(spot.axis), right)}; + catenary_spot spots[2]; + for (int k = 0; k < 2; ++k) + { + auto const point{foot + xaxis * console->console_x[k]}; + { + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(*spot.track, spot.path, point, foot, spot.side, spots[k])) + return; + } + spots[k].curve = spot.curve; + catenary::suspension planned{sourcedata}; + planned.has_spread = true; + planned.spread[0] = point.x; + planned.spread[1] = foot.y; + planned.spread[2] = point.z; + catenary_keep_pole(spots[k], planned, std::abs(offset), offset >= 0.0); + if (spots[k].card == 0) + { + tool.status = spots[k].problem; + return; + } + } + ++tool.group; + auto const file{catenary_scene_file_of(Index)}; + tool.undo.push_back({tool.group, false, Index, sourcedata, std::nullopt, false}); + auto first{spots[0].data}; + first.name = sourcedata.name.empty() ? catenary_scene_new_name(file) : sourcedata.name; + first.range_max = sourcedata.range_max; + first.range_min = sourcedata.range_min; + first.skin = sourcedata.skin; + first.overrides = sourcedata.overrides; + std::string name; + for (int k = 1; name.empty(); ++k) + { + auto const candidate{"rozstaw_" + first.name + (k > 1 ? "_" + std::to_string(k) : std::string{})}; + auto used{false}; + for (std::size_t index = 0; index < catenary_scene_size() && false == used; ++index) + if (auto const *data{catenary_scene_get(index)}; data != nullptr && data->umbrella == candidate) + used = true; + if (false == used) + name = candidate; + } + first.umbrella = name; + catenary_scene_update(Index, first); + tool.spots[Index] = spots[0]; + tool.spots[Index].data = first; + auto second{spots[1].data}; + second.name = catenary_scene_new_name(file); + second.skin = sourcedata.skin; + second.umbrella = name; + auto const added{catenary_scene_add(file, second)}; + tool.spots[added] = spots[1]; + tool.spots[added].data = second; + tool.undo.push_back({tool.group, false, added, std::nullopt, std::nullopt, false}); + catenary_select(static_cast(added)); + tool.status = format(STR_C("Spread cantilevers %s on console %s: %s and %s, %.2f m apart; give each its chain, \"Messenger held by\" splits the messenger and the wires between them"), + name.c_str(), console->name.c_str(), first.name.c_str(), second.name.c_str(), console->console_x[0] - console->console_x[1]); + if (catenary_scene_parts(added) == 0 || catenary_scene_parts(Index) == 0) + tool.status += STR(" - the cantilever didn't assemble, see the log"); +} + +void catenary_editor::catenary_third_create(std::size_t const Index) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Index)}; + auto const *found{catenary_spot_of(Index)}; + if (current == nullptr || found == nullptr || found->track == nullptr || false == current->has_pole) + return; + auto const sourcedata{*current}; + auto const source{*found}; + auto const foot{pole_of(sourcedata)}; + // in the axis of the pole, on its own mounts, to the same track; the pole is stood by the spread pair + catenary_spot spot; + { + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(*source.track, source.path, foot, foot, source.side, spot)) + return; + } + spot.curve = source.curve; + catenary::suspension planned{sourcedata}; + planned.has_spread = false; + planned.umbrella.clear(); + planned.shared_pole = true; + catenary_keep_pole(spot, planned, tool.stagger, tool.toward); + if (spot.card == 0) + { + tool.status = spot.problem; + return; + } + auto data{spot.data}; + auto const file{catenary_scene_file_of(Index)}; + data.name = catenary_scene_new_name(file); + data.skin = sourcedata.skin; + data.shared_pole = true; + ++tool.group; + auto const added{catenary_scene_add(file, data)}; + tool.spots[added] = spot; + tool.spots[added].data = data; + tool.undo.push_back({tool.group, false, added, std::nullopt, std::nullopt, false}); + catenary_select(static_cast(added)); + tool.status = format(STR_C("Cantilever %s in the axis of pole %s (card %d execution %d): set its card (a messenger only one: 1002-1015, 1290-1297), hang it in a chain, an umbrella to the other track can go on it too"), + data.name.c_str(), data.pole_type.c_str(), data.card, data.execution); + if (catenary_scene_parts(added) == 0) + tool.status += STR(" - the cantilever didn't assemble, see the log"); +} + +void catenary_editor::catenary_pair_move(std::size_t const Selected, glm::dvec3 const &Foot, bool const Panel) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Selected)}; + if (current == nullptr || false == current->has_pole) + return; + auto const oldfoot{pole_of(*current)}; + std::vector> moved; + for (auto const index : pole_sharers(Selected)) + { + auto const *data{catenary_scene_get(index)}; + if (data == nullptr) + continue; + auto const *found{catenary_spot_of(index)}; + if (found == nullptr || found->track == nullptr) + return; + auto const found_copy{*found}; + // each keeps its place against the pole (its console mount) and its stagger, on its own track + auto const plane{data->has_spread ? glm::dvec3{data->spread[0], data->pole[1], data->spread[2]} : pole_of(*data)}; + auto planenew{Foot + (plane - oldfoot)}; + planenew.y = Foot.y; + auto const right{right_of(found_copy.forward) * static_cast(found_copy.side)}; + auto offset{glm::dot(plan_of(wire_of(*data)) - plan_of(found_copy.axis), right)}; + // from the panel: the selected one takes its stagger from there + if (Panel && index == Selected) + offset = tool.toward ? static_cast(tool.stagger) : -static_cast(tool.stagger); + catenary_spot spot; + { + without_gantry const plain{tool.gantry_selected}; + if (false == catenary_spot_at(*found_copy.track, found_copy.path, planenew, Foot, found_copy.side, spot)) + return; + } + spot.curve = found_copy.curve; + catenary::suspension planned{*data}; + planned.pole[0] = Foot.x; + planned.pole[1] = Foot.y; + planned.pole[2] = Foot.z; + planned.spread[0] = planenew.x; + planned.spread[1] = Foot.y; + planned.spread[2] = planenew.z; + catenary_keep_pole(spot, planned, std::abs(offset), offset >= 0.0); + if (spot.card == 0) + { + tool.status = spot.problem; + return; + } + auto &result{spot.data}; + result.name = data->name; + result.range_max = data->range_max; + result.range_min = data->range_min; + result.skin = data->skin; + result.overrides = data->overrides; + result.umbrella = data->umbrella; + result.shared_pole = data->shared_pole; + moved.emplace_back(index, spot); + } + // one undo group for the whole drag (of the pole, or of the slider of its distance) + auto record{false}; + int group{0}; + if (Panel) + { + record = tool.shift_group == 0 || tool.undo.empty() || tool.undo.back().group != tool.shift_group; + if (record) + tool.shift_group = ++tool.group; + group = tool.shift_group; + } + else + { + record = tool.drag_undo == 0; + if (record) + tool.drag_undo = ++tool.group; + group = tool.drag_undo; + } + for (auto const &[index, spot] : moved) + { + if (record) + tool.undo.push_back({group, false, index, *catenary_scene_get(index), std::nullopt, false}); + catenary_scene_update(index, spot.data); + tool.spots[index] = spot; + } + tool.probed = false; +} + +void catenary_editor::catenary_umbrella_break(std::size_t const Index) +{ + auto &tool{m_catenary}; + auto const *current{catenary_scene_get(Index)}; + if (current == nullptr || current->umbrella.empty()) + return; + auto const name{current->umbrella}; + ++tool.group; + // another one on the pole stands it already (the third one by spread cantilevers stands none) + auto placed{false}; + for (auto const other : pole_sharers(Index)) + if (auto const *data{catenary_scene_get(other)}; data != nullptr && data->umbrella != name && false == data->shared_pole) + placed = true; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *data{catenary_scene_get(index)}; + if (data == nullptr || data->umbrella != name) + continue; + auto changed{*data}; + tool.undo.push_back({tool.group, false, index, changed, std::nullopt, false}); + changed.umbrella.clear(); + changed.has_spread = false; // a cantilever off its console pair stands by the pole as one of its own + changed.shared_pole = changed.shared_pole || placed; // the pole stands once + placed = placed || false == changed.shared_pole; + catenary_scene_update(index, changed); + if (auto found{tool.spots.find(index)}; found != tool.spots.end()) + found->second.data = changed; + } + tool.probed = false; + tool.status = format(STR_C("Umbrella %s separated: two cantilevers on their own mounts"), name.c_str()); +} + // 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) @@ -1409,6 +1974,14 @@ void catenary_editor::catenary_press(track_intent const &Intent) tool.pressed = {mouse.x, mouse.y}; if (is_intent(Intent, intent_detail::select)) { + if (tool.anchor_pick >= 0) + { + // the tensioning device on this pole + if (catenary_anchor_create(static_cast(tool.anchor_pick), tool.anchor_pick_start, Intent.index, tool.anchor_pick_contact)) + tool.anchor_pick = -1; + return; + } + tool.umbrella_from = -1; if (tool.connect_from >= 0) { // hanging the wires on: the span from the last pole to this one @@ -1428,6 +2001,7 @@ void catenary_editor::catenary_press(track_intent const &Intent) { tool.gantry_selected = Intent.index; tool.selected = -1; + tool.anchor_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 @@ -1523,24 +2097,37 @@ void catenary_editor::catenary_press(track_intent const &Intent) } 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()); - } + tool.anchor_selected = -1; + catenary_chain_show(Intent.index); + return; + } + if (is_intent(Intent, intent_detail::anchor)) + { + // the anchor: its chain on the wires page, the anchor marked in the panel + bool start{false}; + auto const chain{catenary_anchor_chain(static_cast(Intent.index), start)}; + catenary_chain_show(chain); + tool.anchor_selected = Intent.index; + tool.anchor_scroll = true; + if (auto const *anchor{catenary_anchor_get(static_cast(Intent.index))}; anchor != nullptr) + tool.status = chain >= 0 ? format(STR_C("Anchor %s selected: its settings are below, Del removes it"), anchor->name.c_str()) + : format(STR_C("Anchor %s isn't at the end of any chain"), anchor->name.c_str()); return; } if (Intent.track == nullptr) return; catenary_spot spot; + if (tool.umbrella_from >= 0) + { + // the second cantilever of an umbrella on the pole of the first one + if (false == catenary_umbrella_spot(*Intent.track, Intent.path, spot)) + return; + if (spot.card == 0) + tool.status = spot.problem; + else + catenary_umbrella_place(spot); + return; + } 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) @@ -1551,6 +2138,24 @@ void catenary_editor::catenary_press(track_intent const &Intent) catenary_place(spot); } +void catenary_editor::catenary_chain_show(int const Chain) +{ + auto &tool{m_catenary}; + tool.selected = -1; + tool.page = 2; + tool.chain_selected = Chain; + if (auto const *chain{Chain >= 0 ? catenary_chain_get(static_cast(Chain)) : nullptr}; 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()); + } +} + // the panel settings of the chain go to the selected chain void catenary_editor::catenary_chain_apply() { @@ -1578,6 +2183,35 @@ bool catenary_editor::catenary_delete_selected() { auto &tool{m_catenary}; tool.connect_from = -1; + tool.anchor_pick = -1; + if (tool.selected < 0 && tool.anchor_selected >= 0) + { + // the anchor picked in the view + auto const picked{static_cast(tool.anchor_selected)}; + tool.anchor_selected = -1; + auto const *anchor{catenary_anchor_get(picked)}; + if (anchor == nullptr) + return false; + bool start{false}; + if (auto const chain{catenary_anchor_chain(picked, start)}; chain >= 0) + { + auto const *owner{catenary_chain_get(static_cast(chain))}; + auto const contact{owner != nullptr && (start ? owner->anchor_start_contact : owner->anchor_end_contact) == anchor->name}; + catenary_anchor_delete(static_cast(chain), start, contact); + } + else + { + catenary_change removed; + removed.group = ++tool.group; + removed.anchor = true; + removed.index = picked; + removed.anchor_before = *anchor; + tool.undo.push_back(removed); + catenary_anchor_remove(picked); + } + tool.status = STR("Anchor removed"); + return true; + } if (tool.selected < 0) return false; auto const index{static_cast(tool.selected)}; @@ -1590,9 +2224,44 @@ bool catenary_editor::catenary_delete_selected() { 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()) + && (std::find(data->suspensions.begin(), data->suspensions.end(), current->name) != data->suspensions.end() + || std::any_of(data->messenger_from.begin(), data->messenger_from.end(), [&](auto const &Pair) { return Pair.first == current->name || Pair.second == current->name; }))) tool.undo.push_back({tool.group, true, chain, std::nullopt, *data, false}); } + // the other cantilever of its umbrella stays alone + if (false == current->umbrella.empty()) + for (std::size_t other = 0; other < catenary_scene_size(); ++other) + { + auto const *data{catenary_scene_get(other)}; + if (other == index || data == nullptr || data->umbrella != current->umbrella) + continue; + auto changed{*data}; + tool.undo.push_back({tool.group, false, other, changed, std::nullopt, false}); + changed.umbrella.clear(); // a spread one stays alone on its console + catenary_scene_update(other, changed); + if (auto found{tool.spots.find(other)}; found != tool.spots.end()) + found->second.data = changed; + } + current = catenary_scene_get(index); + // it stood the pole and others stay on it: one of them stands it now (not one of a pair, which the pair stands itself) + if (false == current->shared_pole) + { + std::vector rest; + for (auto const other : pole_sharers(index)) + if (other != index) + rest.push_back(other); + auto const standing{std::any_of(rest.begin(), rest.end(), [](std::size_t const Other) { auto const *data{catenary_scene_get(Other)}; return data != nullptr && false == data->shared_pole; })}; + if (false == rest.empty() && false == standing) + { + auto changed{*catenary_scene_get(rest.front())}; + tool.undo.push_back({tool.group, false, rest.front(), changed, std::nullopt, false}); + changed.shared_pole = false; + catenary_scene_update(rest.front(), changed); + if (auto found{tool.spots.find(rest.front())}; found != tool.spots.end()) + found->second.data = changed; + } + current = catenary_scene_get(index); + } tool.undo.push_back({tool.group, false, index, *current, std::nullopt, true}); catenary_scene_remove(index); tool.selected = -1; @@ -1606,6 +2275,7 @@ bool catenary_editor::catenary_undo() { auto &tool{m_catenary}; tool.connect_from = -1; + tool.anchor_pick = -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; @@ -1738,9 +2408,9 @@ void catenary_editor::render_catenary_chain_ui() } } -std::size_t catenary_editor::catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool const Start) const +std::size_t catenary_editor::catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool const Start, bool const Contact) const { - auto const &name{Start ? Chain.anchor_start : Chain.anchor_end}; + auto const &name{Contact ? (Start ? Chain.anchor_start_contact : Chain.anchor_end_contact) : (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) @@ -1766,6 +2436,114 @@ int weight_color_index(std::string const &Name) } // namespace +bool catenary_editor::catenary_anchor_create(std::size_t const Chain, bool const Start, int const On, bool const Contact) +{ + auto &tool{m_catenary}; + auto const *chain{catenary_chain_get(Chain)}; + if (chain == nullptr) + return false; + auto const file{catenary_chain_file(Chain)}; + auto const members{catenary_chain_members(Chain)}; + if (members.size() < 2) + return false; + auto const *last{catenary_scene_get(Start ? members.front() : members.back())}; + auto const *previous{catenary_scene_get(Start ? members[1] : members[members.size() - 2])}; + if (last == nullptr || previous == nullptr || false == last->has_at || false == previous->has_at) + return false; + catenary::anchor data; + data.name = catenary_scene_new_anchor_name(file); + data.network = chain->type; + if (On >= 0) + { + // on an existing pole: it has to stand (a pole entry) and be in the same file as the chain + auto const *carrier{catenary_scene_get(static_cast(On))}; + if (carrier == nullptr || false == carrier->has_pole || carrier->name.empty() || catenary_scene_file_of(static_cast(On)) != file) + { + tool.status = STR("This pole can't carry the anchor: it has to be a pole (not a drop or a point) from the file of the chain"); + return false; + } + if (static_cast(On) == (Start ? members.front() : members.back())) + { + tool.status = STR("The anchor goes on another pole than the last one of the chain"); + return false; + } + data.on = carrier->name; + data.pole_type = carrier->pole_type; + std::copy(std::begin(carrier->pole), std::end(carrier->pole), data.pole); + } + else + { + // the anchor pole 24 m past the last pole, in line with it (catalogue 22 m; Glowacki 24 m for big tensions) + glm::dvec2 along{last->at[0] - previous->at[0], last->at[2] - previous->at[2]}; + if (glm::length(along) < 0.1) + return false; + 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 at{base + along * 24.0}; + data.pole_type = tool.poles.empty() ? std::string{} : tool.poles[std::clamp(tool.pole, 0, static_cast(tool.poles.size()) - 1)]; + data.pole[0] = at.x; + data.pole[1] = last->has_pole ? last->pole[1] : last->at[1] - static_cast(tool.height); + 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}; + ++tool.group; + if (Contact) + { + // the contact wires by themselves (0404, or the wires of a split one); the anchor of the messenger keeps the messenger only + auto const main{catenary_anchor_of(*chain, file, Start)}; + auto const *messenger{main < catenary_anchor_size() ? catenary_anchor_get(main) : nullptr}; + if (messenger == nullptr) + return false; + auto const *devices{catenary_scene_tensioners()}; + auto const *device{devices != nullptr ? devices->device(messenger->device) : nullptr}; + auto const fixed{device == nullptr || device->fixed}; + data.device = fixed ? "0404" : "0482-c"; + if (device == nullptr || device->only != "messenger") + { + auto changedmessenger{*messenger}; + changedmessenger.device = fixed ? "0405" : "0482-m"; + catenary_change before; + before.group = tool.group; + before.anchor = true; + before.index = main; + before.anchor_before = *messenger; + tool.undo.push_back(before); + catenary_anchor_update(main, changedmessenger); + } + (Start ? changed.anchor_start_contact : changed.anchor_end_contact) = data.name; + } + else + (Start ? changed.anchor_start : changed.anchor_end) = data.name; + 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.anchor_pick_contact = false; + tool.status = Contact ? STR("The contact wires are anchored on their own; the other anchor holds the messenger only") : STR("Anchored: the last span goes without droppers to the tensioning device"); + return true; +} + void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) { auto &tool{m_catenary}; @@ -1777,16 +2555,122 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const 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) + // middle anchor (cards 63-0406 / 0408): at one of the inner poles of the chain, the anchor rope goes to the poles on both sides { + auto changed{*chain}; + std::vector labels{STR("none")}; + int chosen{0}; + for (std::size_t k = 1; k + 1 < chain->suspensions.size(); ++k) + { + labels.push_back(format(STR_C("pole %d (%s)"), static_cast(k + 1), chain->suspensions[k].c_str())); + if (chain->suspensions[k] == chain->middle) + chosen = static_cast(labels.size()) - 1; + } + ImGui::PushItemWidth(180.0f); + auto edited{false}; + if (ImGui::BeginCombo(STR_C("Middle anchor"), labels[chosen].c_str())) + { + for (std::size_t k = 0; k < labels.size(); ++k) + if (ImGui::Selectable(labels[k].c_str(), static_cast(k) == chosen)) + { + changed.middle = k == 0 ? std::string{} : chain->suspensions[k]; + changed.middle_heights[0] = changed.middle_heights[1] = 0.0; + edited = true; + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The anchor rope is clamped on the messenger 1 m on each side of this pole and goes to the poles next to it; in the middle of both spans a V ties it to the contact wire. The weight anchors measure their distance from it")); + if (false == chain->middle.empty()) + { + char const *const cards[] = {"0406", "0408", "0418", "0416"}; + int insulated{0}; + for (int k = 0; k < 4; ++k) + if (chain->middle_device == cards[k]) + insulated = k; + std::string const kinds[] = {STR("with insulation (card 0406)"), STR("without insulation (card 0408)"), STR("insulated on one side (card 0418)"), STR("to the beam of a gantry (card 0416)")}; + char const *const items[] = {kinds[0].c_str(), kinds[1].c_str(), kinds[2].c_str(), kinds[3].c_str()}; + if (ImGui::Combo(STR_C("Middle anchor kind"), &insulated, items, 4)) + { + changed.middle_device = cards[std::clamp(insulated, 0, 3)]; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("0416: the middle pole is a drop of a gantry; the anchor ropes go from the beam above the messenger to clamps on it about 3 m on both sides, the poles next to it carry nothing")); + // heights of the anchor rope on the poles before and after (when the cantilever is in the way) + auto const where{std::find(chain->suspensions.begin(), chain->suspensions.end(), chain->middle) - chain->suspensions.begin()}; + auto const members{catenary_chain_members(Chain)}; + for (int side = 0; side < 2 && chain->middle_device != "0416"; ++side) + { + auto const k{static_cast(where) + (side == 0 ? -1 : 1)}; + auto const *neighbour{k < members.size() ? catenary_scene_get(members[k]) : nullptr}; + if (neighbour == nullptr || false == neighbour->has_pole) + continue; + auto const automatic{(neighbour->has_rope ? neighbour->rope[1] : neighbour->at[1] + 1.70) - neighbour->pole[1]}; + auto height{static_cast(chain->middle_heights[side] > 0.0 ? chain->middle_heights[side] : automatic)}; + ImGui::PushID(side); + if (ImGui::SliderFloat(side == 0 ? STR_C("Anchor rope on the pole before (m)") : STR_C("Anchor rope on the pole after (m)"), &height, 4.0f, 10.0f, "%.2f")) + { + changed.middle_heights[side] = height; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Height of the anchor of the middle anchor rope on that pole above the rail heads; by default at the messenger. Move it when it hits the cantilever")); + ImGui::PopID(); + } + if ((chain->middle_heights[0] > 0.0 || chain->middle_heights[1] > 0.0) && ImGui::Button(STR_C("Anchor rope at the messenger"))) + { + changed.middle_heights[0] = changed.middle_heights[1] = 0.0; + edited = true; + } + } + ImGui::PopItemWidth(); + if (edited) + { + // one undo step for one drag of a slider + auto const &last{tool.undo.empty() ? catenary_change{} : tool.undo.back()}; + if (tool.undo.empty() || false == last.chain || last.index != Chain || false == ImGui::IsMouseDragging(0)) + tool.undo.push_back({++tool.group, true, Chain, std::nullopt, *chain, false}); + catenary_chain_update(Chain, changed); + chain = catenary_chain_get(Chain); + if (chain == nullptr) + return; + } + } + ImGui::TextUnformatted(STR_C("Anchors")); + // each end: the anchor of the network (or of the messenger only), then the one of the contact wires when they're anchored apart + for (int slot = 0; slot < 4; ++slot) + { + auto const end{slot / 2}; + auto const contact{slot % 2 == 1}; auto const start{end == 0}; - ImGui::PushID(end); + auto const separate{false == (start ? chain->anchor_start_contact : chain->anchor_end_contact).empty()}; + ImGui::PushID(slot); // 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 index{catenary_anchor_of(*chain, file, start, contact)}; auto const *anchor{index < catenary_anchor_size() ? catenary_anchor_get(index) : nullptr}; + if (contact && anchor == nullptr) + { + // the contact wires apart from the messenger (0405 + 0404, or the two halves of a split one): on another pole + if (catenary_anchor_of(*chain, file, start) < catenary_anchor_size()) + { + auto const picking{tool.anchor_pick == static_cast(Chain) && tool.anchor_pick_start == start && tool.anchor_pick_contact}; + if (ImGui::SmallButton(picking ? STR_C("Stop picking the pole") : STR_C("Contact wires anchored apart: on another pole"))) + { + tool.anchor_pick = picking ? -1 : static_cast(Chain); + tool.anchor_pick_start = start; + tool.anchor_pick_contact = false == picking; + tool.connect_from = -1; + tool.status = picking ? std::string{} : STR("LMB on the pole the anchor of the contact wires goes on"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The messenger stays on the anchor above (it becomes one of the messenger only: 0405 or the messenger of a split one), the contact wires go to an anchor of their own on the pole picked (0404 or the wires of a split one)")); + } + ImGui::PopID(); + continue; + } if (last == nullptr || previous == nullptr || false == last->has_at || false == previous->has_at) { ImGui::PopID(); @@ -1805,55 +2689,78 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) 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"); - } + catenary_anchor_create(Chain, start, -1); 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::SameLine(); + auto const picking{tool.anchor_pick == static_cast(Chain) && tool.anchor_pick_start == start}; + if (ImGui::Button(picking ? STR_C("Stop picking the pole") : STR_C("to an existing pole"))) + { + tool.anchor_pick = picking ? -1 : static_cast(Chain); + tool.anchor_pick_start = start; + tool.anchor_pick_contact = false; + tool.connect_from = -1; + tool.status = picking ? std::string{} : STR("LMB on the pole the tensioning device goes on"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The tensioning device goes on a pole already standing (with its cantilever); the pole stays as it is")); 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 const picked{tool.anchor_selected == static_cast(index)}; + ImGui::TextColored(picked ? ImVec4(1.0f, 0.85f, 0.2f, 1.0f) : 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()) + (contact ? " - " + STR("contact wires") : separate ? " - " + STR("messenger") : std::string{}) + + (picked ? " " + STR("(selected in the view)") : std::string{})).c_str()); + if (picked && tool.anchor_scroll) + { + // picked in the view just now: the panel goes to its settings + ImGui::SetScrollHereY(0.2f); + tool.anchor_scroll = false; + } auto data{*anchor}; auto edited{false}; ImGui::PushItemWidth(180.0f); + // kind of the anchor: the card of the device (weight tensioning 0461, fixed 0403); variants for network types go by themselves + auto fixed{false}; + { + auto const devices{catenary_anchor_devices(contact ? "contact" : separate ? "messenger" : "")}; + auto card{data.device.substr(0, data.device.find('-'))}; + for (auto const &item : devices) + if (item.card == data.device) + card = data.device; // a variant listed by itself (0461-2x2) + std::vector labels; + int chosen{-1}; + for (std::size_t k = 0; k < devices.size(); ++k) + { + if (false == devices[k].title.empty()) + labels.push_back(devices[k].title + " (" + devices[k].card + ")"); + else + labels.push_back(format(devices[k].fixed ? STR_C("fixed (card %s)") : devices[k].split ? STR_C("weights, messenger and contact wires separately (card %s)") : devices[k].series ? STR_C("weights, pulleys in series (card %s)") : STR_C("weight tensioning (card %s)"), devices[k].card.c_str())); + if (devices[k].card == card) + { + chosen = static_cast(k); + fixed = devices[k].fixed; + } + } + if (ImGui::BeginCombo(STR_C("Kind"), chosen >= 0 ? labels[chosen].c_str() : data.device.c_str())) + { + for (std::size_t k = 0; k < devices.size(); ++k) + if (ImGui::Selectable(labels[k].c_str(), static_cast(k) == chosen)) + { + data.device = devices[k].card; + data.height = 0.0; + data.rope_height = 0.0; + fixed = devices[k].fixed; + edited = true; + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Weight tensioning keeps the tension with the air temperature; a fixed anchor holds the wires straight on the pole (the other end of the half-section has the weights)")); + } + if (false == data.on.empty()) + ImGui::TextDisabled("%s", format(STR_C("On the pole of %s"), data.on.c_str()).c_str()); + else { // the anchor pole along the line of the last two poles glm::dvec2 const pole{data.pole[0], data.pole[2]}; @@ -1866,7 +2773,7 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) edited = true; } } - if (ImGui::BeginCombo(STR_C("Anchor pole"), data.pole_type.c_str())) + if (data.on.empty() && 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())) @@ -1876,6 +2783,9 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) } ImGui::EndCombo(); } + if (false == fixed && false == chain->middle.empty()) + ImGui::TextDisabled("%s", STR_C("Distance from the middle anchor: along the chain")); + else if (false == fixed) { auto distance{static_cast(data.distance)}; if (ImGui::SliderFloat(STR_C("From the middle anchor (m)"), &distance, 50.0f, 900.0f, "%.0f")) @@ -1888,6 +2798,7 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) } 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()}; + if (false == fixed) { auto color{weight_color_index(data.color)}; if (ImGui::Combo(STR_C("Weights"), &color, colors, 4)) @@ -1902,23 +2813,71 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) edited = true; } } + if (false == fixed) { - auto missing{data.missing}; - if (ImGui::SliderInt(STR_C("Weights missing"), &missing, 0, 10)) + // the insulators of the chains to the wires: resin 7150 or the old roller ESN-2025 + static char const *const values[] = {"", "new", "old", "messenger", "contact"}; + std::string const names[] = {STR("by the wires (roller ones when tarnished)"), STR("resin 7150"), STR("roller ESN-2025 on both"), STR("roller on the messenger only"), STR("roller on the contact wires only")}; + char const *const items[] = {names[0].c_str(), names[1].c_str(), names[2].c_str(), names[3].c_str(), names[4].c_str()}; + int chosen{0}; + for (int i = 0; i < 5; ++i) + if (data.insulators == values[i]) + chosen = i; + if (ImGui::Combo(STR_C("Insulators"), &chosen, items, 5)) { - data.missing = missing; + data.insulators = values[chosen]; 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")) + ImGui::SetTooltip("%s", STR_C("Old assemblies have the roller insulator ESN-2025 straight in the lever hole, then the turnbuckle and the clamp; by default the roller ones come with tarnished wires, as the 5110 clamps do")); + } + { + if (false == fixed) { - data.elongation = elongation / 100.0; + 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")); + } + auto const *solved{catenary_anchor_result(index)}; + auto height{static_cast(data.height > 0.0 ? data.height : (solved != nullptr && solved->height > 0.0 ? solved->height : 6.70))}; + if (ImGui::SliderFloat(fixed ? STR_C("Contact wire anchor above the rail heads (m)") : STR_C("Pulleys above the rail heads (m)"), &height, 4.5f, 9.0f, "%.2f")) + { + data.height = height; 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::SetTooltip("%s", fixed ? STR_C("Height of the anchor of the contact wires on the mast (the messenger one goes as much higher as at the last pole); by default at the height of the wires at the last pole. Drag or Ctrl+click to type") + : STR_C("Height of the pulley axles on the mast; the weights move up or down with them. The mounting table gives 6.70 m. Drag or Ctrl+click to type")); + if (fixed) + { + auto rope{static_cast(data.rope_height > 0.0 ? data.rope_height : (solved != nullptr && solved->rope_height > 0.0 ? solved->rope_height : 7.40))}; + if (ImGui::SliderFloat(STR_C("Messenger anchor above the rail heads (m)"), &rope, 5.0f, 10.0f, "%.2f")) + { + data.rope_height = rope; + edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Height of the anchor of the messenger on the mast; by default as at the last pole. Drag or Ctrl+click to type")); + if ((data.height > 0.0 || data.rope_height > 0.0) && ImGui::Button(STR_C("Heights as at the last pole"))) + { + data.height = 0.0; + data.rope_height = 0.0; + edited = true; + } + } } ImGui::PopItemWidth(); if (edited) @@ -1938,35 +2897,94 @@ void catenary_editor::render_catenary_anchor_ui(std::size_t const Chain) } 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()); + if (result->fixed) + ImGui::TextDisabled("%s", format(STR_C("Fixed anchor: messenger %.2f m, contact wires %.2f m over the rails"), result->rope_height, result->height).c_str()); + else + 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); + catenary_anchor_delete(Chain, start, contact); + if (tool.anchor_selected == static_cast(index)) + tool.anchor_selected = -1; } ImGui::PopID(); } } +void catenary_editor::catenary_anchor_delete(std::size_t const Chain, bool const Start, bool const Contact) +{ + auto &tool{m_catenary}; + auto const *chain{catenary_chain_get(Chain)}; + if (chain == nullptr) + return; + auto const file{catenary_chain_file(Chain)}; + auto const index{catenary_anchor_of(*chain, file, Start, Contact)}; + auto const *anchor{index < catenary_anchor_size() ? catenary_anchor_get(index) : nullptr}; + if (anchor == nullptr) + return; + auto changed{*chain}; + ++tool.group; + if (Contact) + { + changed.anchor_start_contact = Start ? std::string{} : changed.anchor_start_contact; + changed.anchor_end_contact = Start ? changed.anchor_end_contact : std::string{}; + // the anchor of the messenger takes the contact wires again: the whole network on it + auto const main{catenary_anchor_of(*chain, file, Start)}; + if (auto const *messenger{main < catenary_anchor_size() ? catenary_anchor_get(main) : nullptr}; messenger != nullptr) + { + auto const *devices{catenary_scene_tensioners()}; + auto const *device{devices != nullptr ? devices->device(messenger->device) : nullptr}; + if (device != nullptr && device->only == "messenger") + { + auto whole{*messenger}; + whole.device = device->fixed ? "0403" : "0461"; + catenary_change before; + before.group = tool.group; + before.anchor = true; + before.index = main; + before.anchor_before = *messenger; + tool.undo.push_back(before); + catenary_anchor_update(main, whole); + } + } + } + else + { + (Start ? changed.anchor_start : changed.anchor_end).clear(); + // the contact wires anchored apart go with it + auto const contact{catenary_anchor_of(*chain, file, Start, true)}; + if (auto const *wires{contact < catenary_anchor_size() ? catenary_anchor_get(contact) : nullptr}; wires != nullptr) + { + catenary_change gone; + gone.group = tool.group; + gone.anchor = true; + gone.index = contact; + gone.anchor_before = *wires; + tool.undo.push_back(gone); + catenary_anchor_remove(contact); + (Start ? changed.anchor_start_contact : changed.anchor_end_contact).clear(); + } + } + 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); +} + // 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() @@ -2107,6 +3125,81 @@ bool catenary_editor::render_catenary_catalog_ui() return changed; } +void catenary_editor::render_catenary_chain_state() +{ + auto &tool{m_catenary}; + auto const colour{ImVec4(0.3f, 0.85f, 1.0f, 1.0f)}; + ImGui::Separator(); + // the selected pole: the chain can go on from it + if (tool.selected >= 0) + { + auto const pole{static_cast(tool.selected)}; + std::size_t position{0}; + auto const chain{catenary_chain_of(pole, position)}; + auto const *data{catenary_chain_get(chain >= 0 ? static_cast(chain) : catenary_chain_size())}; + auto const middle{data != nullptr && position != 0 && position + 1 != data->suspensions.size()}; + if (middle) + { + ImGui::TextDisabled("%s", format(STR_C("In the middle of chain %s (pole %zu of %zu)"), data->name.c_str(), position + 1, data->suspensions.size()).c_str()); + return; + } + if (ImGui::Button(data != nullptr ? STR_C("Go on with the chain from this pole (C)") : STR_C("Start a chain from this pole (C)"))) + tool.continue_pending = tool.selected; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The poles placed next by the track are joined to this one with spans, one after another; Esc ends the chain")); + if (data != nullptr) + { + ImGui::SameLine(); + ImGui::TextDisabled("%s", format(position == 0 ? STR_C("start of chain %s") : STR_C("end of chain %s"), data->name.c_str()).c_str()); + } + return; + } + 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")); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("Into spans"), &tool.insert_spans); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A pole placed under the span of a chain (its wire point within 1.5 m of the span, not at its ends) goes into that chain between the two poles")); + if (false == tool.chaining) + return; + auto const *last{tool.last >= 0 ? catenary_scene_get(static_cast(tool.last)) : nullptr}; + if (last == nullptr) + { + ImGui::TextDisabled("%s", STR_C("The next pole starts a new chain; LMB on a pole and C goes on from it")); + return; + } + auto const *chain{tool.chain >= 0 ? catenary_chain_get(static_cast(tool.chain)) : nullptr}; + auto const lastname{last->name.empty() ? std::string{"-"} : last->name}; + if (chain != nullptr) + ImGui::TextColored(colour, "%s", format(tool.append ? STR_C("Next pole: a span from %s, at the end of chain %s (%zu poles)") : STR_C("Next pole: a span from %s, at the start of chain %s (%zu poles)"), + lastname.c_str(), chain->name.c_str(), chain->suspensions.size()).c_str()); + else + ImGui::TextColored(colour, "%s", format(STR_C("Next pole: a new chain from %s"), lastname.c_str()).c_str()); + if (ImGui::Button(STR_C("End the chain (Esc)"))) + catenary_chain_end(); + if (chain != nullptr && chain->suspensions.size() >= 2) + { + auto const index{static_cast(tool.chain)}; + auto const start{false == tool.append}; + if (catenary_anchor_of(*chain, catenary_chain_file(index), start) >= catenary_anchor_size()) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Anchor here and end"))) + { + if (catenary_anchor_create(index, start, -1)) + { + catenary_chain_end(); + tool.chain_selected = static_cast(index); + tool.status = STR("Anchored and ended: the anchor pole is 24 m past the last pole; its settings are on the Wires page"); + } + } + 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 chain is ended")); + } + } +} + // 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() { @@ -2124,6 +3217,7 @@ void catenary_editor::render_catenary_wires_ui() } 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_state(); } render_catenary_chain_ui(); } @@ -2139,6 +3233,8 @@ void catenary_editor::catenary_choose_page(int const Page) tool.gantry_single = false; tool.gantry_first.reset(); tool.connect_from = -1; + tool.anchor_pick = -1; + tool.anchor_selected = -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(); @@ -2156,18 +3252,7 @@ void catenary_editor::render_catenary_cantilever_ui() 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; - } - } + render_catenary_chain_state(); } 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()); @@ -2262,13 +3347,34 @@ void catenary_editor::render_catenary_cantilever_ui() 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 if (false == drop && own != nullptr && pole_sharers(static_cast(tool.selected)).size() > 1) + { + // an umbrella or spread cantilevers: the pole stays, unless its face from the track was changed - then it goes + // across the track and the other cantilever goes with it + auto const plane{current->has_spread ? glm::dvec3{current->spread[0], current->pole[1], current->spread[2]} : pole_of(*current)}; + auto const right{right_of(spot.forward) * static_cast(spot.side)}; + auto const clearance{glm::dot(plan_of(plane) - plan_of(spot.axis), right) - 0.5 * own->depth(0.0)}; + if (std::abs(clearance - static_cast(tool.clearance)) > 1e-4) + { + catenary_compose(spot, tool.clearance, tool.stagger, tool.toward); + if (spot.card != 0) + { + auto foot{glm::dvec3{spot.data.pole[0], spot.data.pole[1], spot.data.pole[2]} - (plane - pole_of(*current))}; + foot.y = current->pole[1]; + catenary_pair_move(static_cast(tool.selected), foot, true); + spot.card = -1; // done + } + } + else + catenary_keep_pole(spot, *current, tool.stagger, tool.toward); + } 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) + if (spot.card > 0) catenary_apply(spot); - else + else if (spot.card == 0) tool.status = spot.problem; } } @@ -2322,6 +3428,67 @@ void catenary_editor::render_catenary_cantilever_ui() } 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")); + // an umbrella: the second cantilever on this pole (or drop), to the track on its other side + if (data->umbrella.empty()) + { + if (ImGui::Button(tool.umbrella_from >= 0 ? STR_C("Stop the umbrella") : STR_C("Umbrella: a cantilever to the other track"))) + { + tool.umbrella_from = tool.umbrella_from >= 0 ? -1 : tool.selected; + tool.connect_from = -1; + tool.anchor_pick = -1; + tool.status = tool.umbrella_from >= 0 ? STR("LMB by the track on the other side of the pole: the second cantilever goes on this pole, with the settings of the panel") : std::string{}; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Two cantilevers on one pole or drop, to the tracks on both sides of it; on adjustable mounts (band round the pole) they share the double mounts 4640-4670 at one height")); + auto const *ownscheme{catalog->scheme(data->card, data->execution)}; + auto const steadyonly{ownscheme != nullptr && ownscheme->roles.count("strut") == 0 && ownscheme->roles.count("stay") == 0}; + if (auto const *own{catalog->pole(data->pole_type)}; own != nullptr && (steadyonly ? false == own->steady_console.empty() : false == own->console.empty())) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Spread cantilevers"))) + { + catenary_spread_create(static_cast(tool.selected)); + data = tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr; + if (data == nullptr) + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Two cantilevers to this track on consoles 4890 (cards 0517/0518), 1.19 m apart along it: this one goes onto the first mount, the second onto the other; each can then hold any chain or only its messenger or its wires")); + } + } + else + { + std::string partner; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + if (auto const *other{catenary_scene_get(index)}; other != nullptr && static_cast(index) != tool.selected && other->umbrella == data->umbrella) + partner = other->name; + ImGui::TextUnformatted(format(data->has_spread ? STR_C("Spread cantilevers %s with %s") : STR_C("Umbrella %s with %s"), data->umbrella.c_str(), partner.empty() ? "-" : partner.c_str()).c_str()); + if (data->has_spread) + { + // a third one on the same pole, in its axis on its own mounts (messenger only by the spread ones holding the wires) + if (ImGui::SmallButton(STR_C("A cantilever in the axis of the pole"))) + { + catenary_third_create(static_cast(tool.selected)); + data = tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr; + if (data == nullptr) + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A third cantilever to this track on the same pole, in its axis on the usual mounts (move them apart from the consoles by hand if they meet); the pole stays one")); + ImGui::SameLine(); + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Separate"))) + { + catenary_umbrella_break(static_cast(tool.selected)); + data = catenary_scene_get(static_cast(tool.selected)); + if (data == nullptr) + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The two cantilevers stay where they are, each on its own single mounts")); + } + render_catenary_chain_state(); // copying to the neighbours in the chain std::size_t position{0}; auto const chain{catenary_chain_of(static_cast(tool.selected), position)}; @@ -2350,6 +3517,7 @@ void catenary_editor::render_catenary_cantilever_ui() 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")); + render_catenary_messenger_from(static_cast(chain)); } if (ImGui::Button(STR_C("Remove the pole"))) catenary_delete_selected(); @@ -2360,10 +3528,103 @@ void catenary_editor::render_catenary_cantilever_ui() } } +// the messenger of the chain at the selected suspension held by another cantilever on the same pole or next to it (spread +// cantilevers on the console 4890, a cantilever of the messenger only by a pull-off of the wires): chain entry messenger_from +void catenary_editor::render_catenary_messenger_from(std::size_t const Chain) +{ + auto &tool{m_catenary}; + auto const *chain{catenary_chain_get(Chain)}; + auto const *data{tool.selected >= 0 ? catenary_scene_get(static_cast(tool.selected)) : nullptr}; + if (chain == nullptr || data == nullptr || data->name.empty()) + return; + auto const file{catenary_chain_file(Chain)}; + auto const holder{catenary::messenger_holder(*chain, data->name)}; + // a cantilever of the messenger only in the chain between two others of its pole (spread steady arms 0167 with a messenger + // cantilever in the axis of the pole): it leaves the list, the wires go straight between the two, the messenger through it + { + auto const &list{chain->suspensions}; + auto const found{std::find(list.begin(), list.end(), data->name)}; + auto const position{static_cast(found - list.begin())}; + if (found != list.end() && position > 0 && position + 1 < list.size() && chain->middle != data->name) + { + auto const sharers{pole_sharers(static_cast(tool.selected))}; + auto const onpole = [&](std::string const &Name) { + return std::any_of(sharers.begin(), sharers.end(), [&](std::size_t const Index) { + auto const *other{catenary_scene_get(Index)}; + return other != nullptr && other->name == Name; + }); + }; + if (onpole(list[position - 1]) && onpole(list[position + 1])) + { + if (ImGui::Button(STR_C("Messenger only: the wires straight past it"))) + { + auto changed{*chain}; + tool.undo.push_back({++tool.group, true, Chain, std::nullopt, *chain, false}); + auto const before{list[position - 1]}, after{list[position + 1]}; + auto &pairs{changed.messenger_from}; + pairs.erase(std::remove_if(pairs.begin(), pairs.end(), [&](auto const &Pair) { return Pair.first == before || Pair.first == after || Pair.first == data->name; }), pairs.end()); + pairs.emplace_back(before, data->name); + pairs.emplace_back(after, data->name); + changed.suspensions.erase(changed.suspensions.begin() + static_cast(position)); + tool.status = format(STR_C("The wires of the chain go straight from %s to %s, the messenger through %s"), before.c_str(), after.c_str(), data->name.c_str()); + catenary_chain_update(Chain, changed); + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("This cantilever holds only the messenger: it leaves the chain, the contact wires go straight between the two cantilevers of the pole next to it in the chain, the messenger goes through this one and their cords hang from it")); + } + } + } + // the cantilevers up to 3 m from this pole (in the plan), in the file of the chain + constexpr double kHolderRange{3.0}; + std::vector names{data->name}; + for (std::size_t index = 0; index < catenary_scene_size(); ++index) + { + auto const *other{catenary_scene_get(index)}; + if (other == nullptr || static_cast(index) == tool.selected || other->name.empty() || false == other->has_rope || catenary_scene_file_of(index) != file) + continue; + if (glm::length(plan_of(pole_of(*other)) - plan_of(pole_of(*data))) <= kHolderRange) + names.push_back(other->name); + } + if (names.size() < 2 && holder == data->name) + return; + ImGui::PushItemWidth(160.0f); + auto const label{holder == data->name ? format(STR_C("%s (this one)"), holder.c_str()) : holder}; + if (ImGui::BeginCombo(STR_C("Messenger held by"), label.c_str())) + { + for (auto const &name : names) + { + auto const text{name == data->name ? format(STR_C("%s (this one)"), name.c_str()) : name}; + if (ImGui::Selectable(text.c_str(), name == holder) && name != holder) + { + auto changed{*chain}; + tool.undo.push_back({++tool.group, true, Chain, std::nullopt, *chain, false}); + auto &pairs{changed.messenger_from}; + pairs.erase(std::remove_if(pairs.begin(), pairs.end(), [&](auto const &Pair) { return Pair.first == data->name; }), pairs.end()); + if (name != data->name) + pairs.emplace_back(data->name, name); + tool.status = name == data->name ? STR("The messenger goes through this cantilever again") + : format(STR_C("The wires of the chain go through %s, its messenger through %s"), data->name.c_str(), name.c_str()); + catenary_chain_update(Chain, changed); + break; + } + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Another cantilever up to 3 m from this pole holds the messenger of this chain here, this one only its wires: spread cantilevers on a console, a cantilever of the messenger only, chains crossing over in a tension span")); + ImGui::PopItemWidth(); +} + void catenary_editor::render_catenary_ui() { auto &tool{m_catenary}; catenary_scan(); + if (tool.continue_pending >= 0) + { + catenary_chain_continue(static_cast(tool.continue_pending)); + tool.continue_pending = -1; + } auto const *catalog{catenary_scene_catalog()}; if (catalog == nullptr || tool.poles.empty()) { @@ -2883,6 +4144,51 @@ void catenary_editor::draw_catenary_overlay() const drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str()); } } + // anchors: the device from the face of the pole to the moving pulley and the column of weights; not assembled - red at the pole + for (std::size_t index = 0; index < catenary_anchor_size(); ++index) + { + auto const *anchor{catenary_anchor_get(index)}; + if (anchor == nullptr) + continue; + auto const selected{static_cast(index) == tool.anchor_selected}; + auto const hovered{is_intent(e.m_intent, intent_detail::anchor) && e.m_intent.index == static_cast(index)}; + double shape[4][3]; + ImVec2 head; + if (false == catenary_anchor_shape(index, shape)) + { + if (false == anchor->on.empty()) + continue; // on the pole of a suspension: that one shows what's wrong with it + glm::dvec3 const foot{anchor->pole[0], anchor->pole[1], anchor->pole[2]}; + if (glm::distance(foot, camera) > kCatenaryRange) + continue; + projection.line(drawlist, foot, foot + glm::dvec3{0.0, 8.0, 0.0}, overlay_color::invalid, 2.0f); + if (projection.project(foot + glm::dvec3{0.0, 8.3, 0.0}, head)) + drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), overlay_color::invalid, (anchor->name + " " + STR("doesn't assemble")).c_str()); + continue; + } + glm::dvec3 const face{shape[0][0], shape[0][1], shape[0][2]}, lever{shape[1][0], shape[1][1], shape[1][2]}; + glm::dvec3 const top{shape[2][0], shape[2][1], shape[2][2]}, bottom{shape[3][0], shape[3][1], shape[3][2]}; + if (glm::distance(face, camera) > kCatenaryRange) + continue; + auto const *result{catenary_anchor_result(index)}; + auto const warned{result != nullptr && false == result->warnings.empty()}; + auto const colour{selected ? overlay_color::selected : hovered ? overlay_color::highlight : warned ? overlay_color::invalid : IM_COL32(120, 200, 255, 170)}; + auto const width{selected || hovered ? 3.0f : 1.5f}; + projection.line(drawlist, face, lever, colour, width); + projection.line(drawlist, top, bottom, colour, width); + ImVec2 point; + if (projection.project(lever, point)) + drawlist->AddCircleFilled(point, width + 1.5f, colour); + if ((selected || hovered || warned) && projection.project(top + glm::dvec3{0.0, 0.4, 0.0}, head)) + { + auto label{anchor->name}; + if (result != nullptr) + label += format(" %d x, %.0f daN", result->weights, result->tension); + if (warned) + label += " (" + result->warnings.front() + ")"; + 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)) @@ -2910,7 +4216,22 @@ void catenary_editor::draw_catenary_overlay() const 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) + chain_insert insert; + if (catenary_insert_at(spot, insert)) + { + // into the span: the two new spans to its ends instead of the old one + auto const *a{catenary_scene_get(insert.from)}; + auto const *b{catenary_scene_get(insert.to)}; + if (a != nullptr && b != nullptr) + { + projection.line(drawlist, wire_of(*a), wire_of(spot.data), overlay_color::grip, 2.5f); + projection.line(drawlist, wire_of(spot.data), wire_of(*b), overlay_color::grip, 2.5f); + ImVec2 point; + if (projection.project(wire_of(spot.data) + glm::dvec3{0.0, 0.6, 0.0}, point)) + drawlist->AddText(ImVec2(point.x + 6.0f, point.y - 14.0f), overlay_color::grip, format(STR_C("into the span %s - %s"), a->name.c_str(), b->name.c_str()).c_str()); + } + } + else 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); } @@ -2955,6 +4276,17 @@ void catenary_editor::catenary_drag_update() 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 (pole_sharers(static_cast(tool.selected)).size() > 1) + { + // an umbrella or spread cantilevers: the pole goes where this one puts it, the other cantilever goes with it + if (spot.card == 0) + return; + auto const plane{current->has_spread ? glm::dvec3{current->spread[0], current->pole[1], current->spread[2]} : pole_of(*current)}; + auto foot{glm::dvec3{spot.data.pole[0], spot.data.pole[1], spot.data.pole[2]} - (plane - pole_of(*current))}; + foot.y = spot.data.pole[1]; + catenary_pair_move(static_cast(tool.selected), foot, false); + return; + } if (spot.card != 0) catenary_apply(spot); return; @@ -3185,10 +4517,20 @@ bool catenary_editor::catenary_intent(track_intent &Intent) Intent.detail = static_cast(intent_detail::mast); return true; } + // the tensioning device of an anchor (not while a pole is being picked for an anchor or a span) + if (m_catenary.anchor_pick < 0 && m_catenary.connect_from < 0) + if (auto const hit{catenary_anchor_hit()}; hit >= 0) + { + Intent.index = hit; + Intent.action = STR("Click: select the anchor"); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::anchor); + 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.action = m_catenary.anchor_pick >= 0 ? STR("Click: the tensioning device on this pole") : 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; @@ -3209,6 +4551,22 @@ bool catenary_editor::catenary_intent(track_intent &Intent) Intent.detail = static_cast(intent_detail::chain); return true; } + // the second cantilever of an umbrella: by any track, onto the pole of the first one + if (m_catenary.umbrella_from >= 0 && e.m_hover.track != nullptr && m_catenary.page != 2) + { + catenary_spot spot; + if (catenary_umbrella_spot(*e.m_hover.track, e.m_hover.path, spot)) + { + Intent.track = e.m_hover.track; + Intent.path = e.m_hover.path; + Intent.position = spot.axis; + Intent.action = spot.card == 0 ? format(STR_C("Umbrella: %s"), spot.problem.c_str()) + : format(STR_C("Click: the second cantilever of the umbrella, card %04d execution %d, A = %.2f m"), spot.card, spot.data.execution, spot.reach); + Intent.what = kind::catenary; + Intent.detail = static_cast(intent_detail::place); + 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)) { @@ -3230,27 +4588,68 @@ bool catenary_editor::catenary_intent(track_intent &Intent) 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"); + return m_catenary.anchor_pick >= 0 ? STR_C("Wires: LMB on the pole the tensioning device goes on") : 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.umbrella_from >= 0) + return STR_C("Umbrella: LMB by the track on the other side of the pole puts the second cantilever on it; Esc stops"); if (m_catenary.selected >= 0) - return STR_C("Catenary: the selected pole follows the settings below"); + return STR_C("Catenary: the selected pole follows the settings below; C goes on with the chain from it"); + if (m_catenary.chaining && m_catenary.last >= 0) + return STR_C("Catenary: LMB by the track places the next pole of the chain; Esc ends the chain"); 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"}}; + Hints.push_back({"LMB on a tensioning device", "selects the anchor and its chain"}); + Hints.push_back({"LMB under a span", "puts the pole into the chain between the two poles"}); + if (m_catenary.chaining && m_catenary.last >= 0 && m_catenary.selected < 0) + Hints.push_back({"Esc", "ends the chain: the next pole starts a new one"}); if (m_catenary.selected >= 0) + { + if (m_catenary.umbrella_from >= 0) + Hints.push_back({"LMB by the other track", "the second cantilever of the umbrella on this pole"}); + Hints.push_back({"C", "the chain goes on from the selected pole"}); Hints.push_back({"Del", "removes the selected pole"}); + } + else if (m_catenary.anchor_selected >= 0) + Hints.push_back({"Del", "removes the selected anchor"}); } bool catenary_editor::catenary_on_key(int const Key, int const Action, int const Mods) { + auto &tool{m_catenary}; + if (Action != GLFW_PRESS || (Mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SHIFT)) != 0 || ImGui::GetIO().WantTextInput) + return false; + if (Key == GLFW_KEY_ESCAPE) + { + // first what's being picked, then the chain going on; with nothing of that the editor has the key + if (tool.anchor_pick >= 0 || tool.connect_from >= 0 || tool.umbrella_from >= 0) + { + tool.anchor_pick = -1; + tool.connect_from = -1; + tool.umbrella_from = -1; + tool.status.clear(); + return true; + } + if (tool.chaining && tool.last >= 0 && tool.selected < 0) + { + catenary_chain_end(); + return true; + } + return false; + } + if (Key == GLFW_KEY_C && tool.selected >= 0) + { + catenary_chain_continue(static_cast(tool.selected)); + return true; + } return false; } diff --git a/application/editormodecatenary.h b/application/editormodecatenary.h index c17abab0..1964c45d 100644 --- a/application/editormodecatenary.h +++ b/application/editormodecatenary.h @@ -29,7 +29,7 @@ struct catenary_editor 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 }; + enum class intent_detail { place, select, chain, gantry, mast, anchor }; static bool is_intent(track_intent const &Intent, intent_detail const Detail) { return Intent.what == track_intent::kind::catenary && Intent.detail == static_cast(Detail); @@ -46,6 +46,7 @@ struct catenary_editor 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 + int inward{0}; // the centre of the bend: +1 on the right of the direction of the path, -1 on the left, 0 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 @@ -90,11 +91,19 @@ struct catenary_editor int selected{-1}; // chains: the poles placed one after another are joined by spans bool chaining{true}; + int continue_pending{-1}; // the chain goes on from this pole, from the next frame of the panel (the button is in the middle of it) + bool insert_spans{true}; // a pole placed in a span of a chain goes into the chain between the two poles of the span 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 + int umbrella_from{-1}; // an umbrella from this suspension: the next LMB by a track puts the second cantilever on its pole (or drop) + int anchor_pick{-1}; // anchoring this chain to an existing pole: the next pole clicked carries the tensioning device + bool anchor_pick_start{false}; // ... at the start of the chain, otherwise at its end + bool anchor_pick_contact{false}; // ... the contact wires only (anchored apart from the messenger) + int anchor_selected{-1}; // the anchor picked in the view: its chain is shown, the panel marks it, Del removes it + bool anchor_scroll{false}; // the panel scrolls to the anchor picked in the view, once char power[64]{"none"}; float voltage{3000.0f}; int material{0}; // 0 cu, 1 al @@ -157,6 +166,10 @@ struct catenary_editor 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; + // superelevation of the track at the spot (radians, unsigned) and the horizontal shift of the axis of a leaning + // pantograph at Height above the rail heads, towards the inside of the bend (table 23-0007, curve with superelevation) + static double catenary_cant(catenary_spot const &Spot); + static glm::dvec3 catenary_cant_shift(catenary_spot const &Spot, double Height); 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); @@ -168,7 +181,38 @@ struct catenary_editor void catenary_copy(std::vector const &Targets); bool catenary_propose_toward(bool &Toward); void catenary_place(catenary_spot const &Spot); + // the second cantilever of an umbrella on the pole (or drop) of tool.umbrella_from, towards this track; false when it can't + // be put there (Spot.problem says why when Spot.card is 0) + bool catenary_umbrella_spot(TTrack &Track, int Path, catenary_spot &Spot); + void catenary_umbrella_place(catenary_spot const &Spot); + // the two cantilevers of the umbrella of this suspension become separate ones + void catenary_umbrella_break(std::size_t Index); + // a cantilever of an umbrella or of spread cantilevers: composed with its pole (and its console mount) where they are + void catenary_keep_pole(catenary_spot &Spot, catenary::suspension const &Current, double Stagger, bool Toward) const; + // spread cantilevers on the console of the pole: this one onto the first mount, a second one onto the other, same track + void catenary_spread_create(std::size_t Index); + // the pole of an umbrella or of spread cantilevers moved to Foot (rail heads): both cantilevers go with it + // (Panel: from the distance slider - the selected one takes the stagger of the panel, the slider is one undo group) + void catenary_pair_move(std::size_t Selected, glm::dvec3 const &Foot, bool Panel); + // a third cantilever on the pole of spread cantilevers, in the axis of the pole, to the same track (shared_pole) + void catenary_third_create(std::size_t Index); + // the span of a chain the spot falls in (in the plan: the wire point near the line of the span, between its ends) + struct chain_insert + { + int chain{-1}; + std::size_t after{0}; // the new pole goes after this position of the chain + std::size_t from{0}, to{0}; // the suspensions at both ends of the span + }; + bool catenary_insert_at(catenary_spot const &Spot, chain_insert &Insert) const; + // the next pole placed starts a new chain + void catenary_chain_end(); + // the next poles placed go on from this pole: at the end (or the start) of its chain, or a new chain from it; + // false when the pole is in the middle of a chain + bool catenary_chain_continue(std::size_t Pole); + // the chain block of the poles page: what the next click does, end, anchor, go on from the selected pole + void render_catenary_chain_state(); void catenary_chain_apply(); + void render_catenary_messenger_from(std::size_t Chain); 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; @@ -194,7 +238,15 @@ struct catenary_editor // 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; + std::size_t catenary_anchor_of(catenary::chain const &Chain, std::string const &File, bool Start, bool Contact = false) const; + // anchors the start or the end of the chain: on a new pole past the last one, or (On >= 0) on that existing pole + bool catenary_anchor_create(std::size_t Chain, bool Start, int On, bool Contact = false); + // removes the anchor at the start or the end of the chain (one undo step with the chain) + void catenary_anchor_delete(std::size_t Chain, bool Start, bool Contact = false); + // the anchor whose tensioning device is under the cursor; -1 if none + int catenary_anchor_hit() const; + // shows the chain in the panel as a click on its span does + void catenary_chain_show(int Chain); void render_catenary_cantilever_ui(); void render_catenary_wires_ui(); void catenary_choose_page(int const Page); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 17e0fdbe..ddad587c 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5894,3 +5894,360 @@ msgstr "żółte, pędzlowane" msgid "grey, shabby" msgstr "szare, brzydkie" + +msgid "Stop picking the pole" +msgstr "Przerwij wybieranie słupa" + +msgid "to an existing pole" +msgstr "do istniejącego słupa" + +msgid "LMB on the pole the tensioning device goes on" +msgstr "LPM na słup, na którym ma stanąć urządzenie naprężające" + +msgid "The tensioning device goes on a pole already standing (with its cantilever); the pole stays as it is" +msgstr "Urządzenie naprężające na słupie, który już stoi (z wysięgnikiem); słup zostaje jak jest" + +msgid "On the pole of %s" +msgstr "Na słupie %s" + +msgid "This pole can't carry the anchor: it has to be a pole (not a drop or a point) from the file of the chain" +msgstr "Na tym słupie nie da się zakotwić: musi to być słup (nie zwis ani punkt) z pliku łańcucha" + +msgid "The anchor goes on another pole than the last one of the chain" +msgstr "Kotwienie musi być na innym słupie niż ostatni słup łańcucha" + +msgid "Click: the tensioning device on this pole" +msgstr "Klik: urządzenie naprężające na tym słupie" + +msgid "Wires: LMB on the pole the tensioning device goes on" +msgstr "Druty: LPM na słup, na którym ma stanąć urządzenie naprężające" + +msgid "Pulleys above the rail heads (m)" +msgstr "Rolki nad główką szyny (m)" + +msgid "Height of the pulley axles on the mast; the weights move up or down with them. The mounting table gives 6.70 m. Drag or Ctrl+click to type" +msgstr "Wysokość osi rolek na słupie; ciężary przesuwają się razem z nimi. Tablica montażowa podaje 6,70 m. Przeciągnij albo Ctrl+klik, żeby wpisać" + +msgid ", superelevation %.0f mm" +msgstr ", przechyłka %.0f mm" + +msgid "Click: select the anchor" +msgstr "Klik: zaznacz kotwienie" + +msgid "Anchor %s selected: its settings are below, Del removes it" +msgstr "Zaznaczone kotwienie %s: ustawienia poniżej, Del je usuwa" + +msgid "Anchor %s isn't at the end of any chain" +msgstr "Kotwienie %s nie kończy żadnego łańcucha" + +msgid "Anchor removed" +msgstr "Kotwienie usunięte" + +msgid "(selected in the view)" +msgstr "(zaznaczone w oknie)" + +msgid "LMB on a tensioning device" +msgstr "LPM na urządzeniu naprężającym" + +msgid "selects the anchor and its chain" +msgstr "zaznacza kotwienie i jego łańcuch" + +msgid "removes the selected anchor" +msgstr "usuwa zaznaczone kotwienie" + +msgid "The chain is ended: the next pole starts a new one" +msgstr "Łańcuch zakończony: następny słup zaczyna nowy" + +msgid "The pole is in the middle of a chain: the chain goes on from its ends only" +msgstr "Słup jest w środku łańcucha: łańcuch można ciągnąć tylko od jego końców" + +msgid "The next poles go on from %s: LMB by the track; Esc ends the chain" +msgstr "Następne słupy idą od %s: LPM przy torze; Esc kończy łańcuch" + +msgid "; put into chain %s between %s and %s" +msgstr "; wstawiony do łańcucha %s między %s a %s" + +msgid "into the span %s - %s" +msgstr "w przęsło %s - %s" + +msgid "In the middle of chain %s (pole %zu of %zu)" +msgstr "W środku łańcucha %s (słup %zu z %zu)" + +msgid "Go on with the chain from this pole (C)" +msgstr "Ciągnij łańcuch od tego słupa (C)" + +msgid "Start a chain from this pole (C)" +msgstr "Zacznij łańcuch od tego słupa (C)" + +msgid "The poles placed next by the track are joined to this one with spans, one after another; Esc ends the chain" +msgstr "Kolejne słupy stawiane przy torze łączą się z tym przęsłami, jeden za drugim; Esc kończy łańcuch" + +msgid "start of chain %s" +msgstr "początek łańcucha %s" + +msgid "end of chain %s" +msgstr "koniec łańcucha %s" + +msgid "Into spans" +msgstr "W przęsła" + +msgid "A pole placed under the span of a chain (its wire point within 1.5 m of the span, not at its ends) goes into that chain between the two poles" +msgstr "Słup postawiony pod przęsłem łańcucha (punkt drutu do 1,5 m od przęsła, nie przy jego końcach) wchodzi do łańcucha między te dwa słupy" + +msgid "The next pole starts a new chain; LMB on a pole and C goes on from it" +msgstr "Następny słup zaczyna nowy łańcuch; LPM na słupie i C – ciągnięcie od niego" + +msgid "Next pole: a span from %s, at the end of chain %s (%zu poles)" +msgstr "Następny słup: przęsło od %s, na końcu łańcucha %s (%zu słupów)" + +msgid "Next pole: a span from %s, at the start of chain %s (%zu poles)" +msgstr "Następny słup: przęsło od %s, na początku łańcucha %s (%zu słupów)" + +msgid "Next pole: a new chain from %s" +msgstr "Następny słup: nowy łańcuch od %s" + +msgid "End the chain (Esc)" +msgstr "Zakończ łańcuch (Esc)" + +msgid "Anchor here and end" +msgstr "Zakotwij tu i zakończ" + +msgid "Anchored and ended: the anchor pole is 24 m past the last pole; its settings are on the Wires page" +msgstr "Zakotwiono i zakończono: słup kotwowy 24 m za ostatnim słupem; ustawienia na stronie Druty" + +msgid "An anchor pole 24 m past the last pole, in line with it, with the weight tensioning device; the chain is ended" +msgstr "Słup kotwowy 24 m za ostatnim słupem, w jego linii, z urządzeniem naprężającym; łańcuch zostaje zakończony" + +msgid "Catenary: the selected pole follows the settings below; C goes on with the chain from it" +msgstr "Sieć trakcyjna: zaznaczony słup idzie za ustawieniami poniżej; C ciągnie od niego łańcuch" + +msgid "Catenary: LMB by the track places the next pole of the chain; Esc ends the chain" +msgstr "Sieć trakcyjna: LPM przy torze stawia następny słup łańcucha; Esc kończy łańcuch" + +msgid "LMB under a span" +msgstr "LPM pod przęsłem" + +msgid "puts the pole into the chain between the two poles" +msgstr "wstawia słup do łańcucha między te dwa słupy" + +msgid "ends the chain: the next pole starts a new one" +msgstr "kończy łańcuch: następny słup zaczyna nowy" + +msgid "the chain goes on from the selected pole" +msgstr "łańcuch idzie dalej od zaznaczonego słupa" + +msgid "by the wires (roller ones when tarnished)" +msgstr "wg drutów (rolkowe przy zaśniedziałych)" + +msgid "resin 7150" +msgstr "żywiczne 7150" + +msgid "roller ESN-2025 on both" +msgstr "rolkowe ESN-2025 na obu" + +msgid "roller on the messenger only" +msgstr "rolkowy tylko na linie nośnej" + +msgid "roller on the contact wires only" +msgstr "rolkowe tylko na przewodach" + +msgid "Insulators" +msgstr "Izolatory" + +msgid "Old assemblies have the roller insulator ESN-2025 straight in the lever hole, then the turnbuckle and the clamp; by default the roller ones come with tarnished wires, as the 5110 clamps do" +msgstr "Stare złożenia mają izolator rolkowy ESN-2025 wprost w otworze dźwigni, za nim śrubę rzymską i uchwyt; domyślnie rolkowe są przy drutach zaśniedziałych, tak jak uchwyty 5110" + +msgid "Anchors" +msgstr "Kotwienia" + +msgid "fixed (card %s)" +msgstr "stałe (karta %s)" + +msgid "weight tensioning (card %s)" +msgstr "ciężarowe (karta %s)" + +msgid "Weight tensioning keeps the tension with the air temperature; a fixed anchor holds the wires straight on the pole (the other end of the half-section has the weights)" +msgstr "Ciężarowe trzyma naciąg przy każdej temperaturze; stałe trzyma druty wprost na słupie (ciężary są na drugim końcu połówki odcinka)" + +msgid "Contact wire anchor above the rail heads (m)" +msgstr "Kotwienie przewodów nad główką szyny (m)" + +msgid "Height of the anchor of the contact wires on the mast (the messenger one goes as much higher as at the last pole); by default at the height of the wires at the last pole. Drag or Ctrl+click to type" +msgstr "Wysokość zakotwienia przewodów jezdnych na słupie (lina nośna o tyle wyżej, co przy ostatnim słupie); domyślnie na wysokości drutów przy ostatnim słupie. Przeciągnij albo Ctrl+klik, żeby wpisać" + +msgid "Fixed anchor: messenger %.2f m, contact wires %.2f m over the rails" +msgstr "Kotwienie stałe: lina %.2f m, przewody %.2f m nad główką szyny" + +msgid "weights, pulleys in series (card %s)" +msgstr "ciężarowe, rolki szeregowe (karta %s)" + +msgid "weights, messenger and contact wires separately (card %s)" +msgstr "ciężarowe rozdzielone, lina i przewody osobno (karta %s)" + +msgid "Messenger anchor above the rail heads (m)" +msgstr "Kotwienie liny nośnej nad główką szyny (m)" + +msgid "Height of the anchor of the messenger on the mast; by default as at the last pole. Drag or Ctrl+click to type" +msgstr "Wysokość zakotwienia liny nośnej na słupie; domyślnie jak przy ostatnim słupie. Przeciągnij albo Ctrl+klik, żeby wpisać" + +msgid "Heights as at the last pole" +msgstr "Wysokości jak przy ostatnim słupie" + +msgid "pole %d (%s)" +msgstr "słup %d (%s)" + +msgid "Middle anchor" +msgstr "Kotwienie środkowe" + +msgid "The anchor rope is clamped on the messenger 1 m on each side of this pole and goes to the poles next to it; in the middle of both spans a V ties it to the contact wire. The weight anchors measure their distance from it" +msgstr "Lina kotwienia jest zaciśnięta na linie nośnej 1 m po obu stronach tego słupa i idzie do słupów obok; w połowie obu przęseł V wiąże ją z przewodem jezdnym. Kotwienia ciężarowe liczą odległość od niego" + +msgid "Middle anchor kind" +msgstr "Rodzaj kotwienia środkowego" + +msgid "with insulation (card 0406)" +msgstr "z izolacją (karta 0406)" + +msgid "without insulation (card 0408)" +msgstr "bez izolacji (karta 0408)" + +msgid "none" +msgstr "brak" + +msgid "Distance from the middle anchor: along the chain" +msgstr "Odległość od kotwienia środkowego: wzdłuż łańcucha" + +msgid "Anchor rope on the pole before (m)" +msgstr "Lina kotwienia na słupie przed (m)" + +msgid "Anchor rope on the pole after (m)" +msgstr "Lina kotwienia na słupie za (m)" + +msgid "Height of the anchor of the middle anchor rope on that pole above the rail heads; by default at the messenger. Move it when it hits the cantilever" +msgstr "Wysokość zakotwienia liny kotwienia środkowego na tym słupie nad główką szyny; domyślnie przy linie nośnej. Przesuń, gdy wchodzi w wysięgnik" + +msgid "Anchor rope at the messenger" +msgstr "Lina kotwienia przy linie nośnej" + +msgid "This track is on the same side of the pole as its cantilever: the second cantilever of an umbrella goes to a track on the other side" +msgstr "Ten tor jest po tej samej stronie słupa co jego wysięgnik: drugi wysięgnik parasola idzie do toru po drugiej stronie" + +msgid "Umbrella %s: the second cantilever (card %d execution %d, A = %.2f m) on pole %s of %s; hang its wires with the button below or C" +msgstr "Parasol %s: drugi wysięgnik (karta %d wykonanie %d, A = %.2f m) na słupie %s podwieszenia %s; druty rozwieś przyciskiem niżej albo C" + +msgid "Umbrella %s separated: two cantilevers on their own mounts" +msgstr "Parasol %s rozdzielony: dwa wysięgniki na własnych mocowaniach" + +msgid "Umbrella: %s" +msgstr "Parasol: %s" + +msgid "Click: the second cantilever of the umbrella, card %04d execution %d, A = %.2f m" +msgstr "Kliknij: drugi wysięgnik parasola, karta %04d wykonanie %d, A = %.2f m" + +msgid "Umbrella: LMB by the track on the other side of the pole puts the second cantilever on it; Esc stops" +msgstr "Parasol: LPM przy torze po drugiej stronie słupa stawia na nim drugi wysięgnik; Esc przerywa" + +msgid "Stop the umbrella" +msgstr "Przerwij parasol" + +msgid "Umbrella: a cantilever to the other track" +msgstr "Parasol: wysięgnik do drugiego toru" + +msgid "LMB by the track on the other side of the pole: the second cantilever goes on this pole, with the settings of the panel" +msgstr "LPM przy torze po drugiej stronie słupa: drugi wysięgnik staje na tym słupie, z ustawieniami panelu" + +msgid "Two cantilevers on one pole or drop, to the tracks on both sides of it; on adjustable mounts (band round the pole) they share the double mounts 4640-4670 at one height" +msgstr "Dwa wysięgniki na jednym słupie albo zwisie, do torów po obu jego stronach; przy mocowaniach regulowanych (opaska wokół słupa) mają wspólne mocowania dwustronne 4640–4670 na jednej wysokości" + +msgid "Umbrella %s with %s" +msgstr "Parasol %s z %s" + +msgid "Separate" +msgstr "Rozdziel" + +msgid "The two cantilevers stay where they are, each on its own single mounts" +msgstr "Oba wysięgniki zostają na miejscu, każdy na własnych mocowaniach jednostronnych" + +msgid "LMB by the other track" +msgstr "LPM przy drugim torze" + +msgid "the second cantilever of the umbrella on this pole" +msgstr "drugi wysięgnik parasola na tym słupie" + +msgid "%s (this one)" +msgstr "%s (ten)" + +msgid "Messenger held by" +msgstr "Linę nośną trzyma" + +msgid "The messenger goes through this cantilever again" +msgstr "Lina nośna znowu idzie przez ten wysięgnik" + +msgid "The wires of the chain go through %s, its messenger through %s" +msgstr "Przewody jezdne łańcucha idą przez %s, lina nośna przez %s" + +msgid "Another cantilever up to 3 m from this pole holds the messenger of this chain here, this one only its wires: spread cantilevers on a console, a cantilever of the messenger only, chains crossing over in a tension span" +msgstr "Linę nośną łańcucha trzyma tu inny wysięgnik do 3 m od tego słupa, ten tylko przewody jezdne: wysięgniki rozstawione na konsoli, wysięgnik samej liny, łańcuchy krzyżujące się w przęśle naprężenia" + +msgid "Two cantilevers share this pole (umbrella or spread cantilevers): separate them to move it" +msgstr "Na tym słupie są dwa wysięgniki (parasol albo rozstawione): rozdziel je, żeby go przesunąć" + +msgid "Pole %s carries no console for spread cantilevers (\"console\" in its entry in traction_supports.txt)" +msgstr "Słup %s nie ma konsoli wysięgników rozstawionych (\"console\" w jego wpisie w traction_supports.txt)" + +msgid "Spread cantilevers %s on console %s: %s and %s, %.2f m apart; give each its chain, \"Messenger held by\" splits the messenger and the wires between them" +msgstr "Wysięgniki rozstawione %s na konsoli %s: %s i %s, %.2f m od siebie; każdemu daj jego łańcuch, „Linę nośną trzyma” dzieli linę i przewody między nie" + +msgid "Spread cantilevers" +msgstr "Wysięgniki rozstawione" + +msgid "Two cantilevers to this track on consoles 4890 (cards 0517/0518), 1.19 m apart along it: this one goes onto the first mount, the second onto the other; each can then hold any chain or only its messenger or its wires" +msgstr "Dwa wysięgniki do tego toru na konsolach 4890 (karty 0517/0518), 1,19 m od siebie wzdłuż toru: ten idzie na pierwsze mocowanie, drugi na drugie; potem każdy może trzymać dowolny łańcuch albo samą jego linę nośną lub same przewody" + +msgid "Spread cantilevers %s with %s" +msgstr "Wysięgniki rozstawione %s z %s" + +msgid "Cantilever %s in the axis of pole %s (card %d execution %d): set its card (a messenger only one: 1002-1015, 1290-1297), hang it in a chain, an umbrella to the other track can go on it too" +msgstr "Wysięgnik %s w osi słupa %s (karta %d wykonanie %d): ustaw mu kartę (samej liny: 1002–1015, 1290–1297), rozwieś w łańcuchu; może też dostać parasol do drugiego toru" + +msgid "A cantilever in the axis of the pole" +msgstr "Wysięgnik w osi słupa" + +msgid "A third cantilever to this track on the same pole, in its axis on the usual mounts (move them apart from the consoles by hand if they meet); the pole stays one" +msgstr "Trzeci wysięgnik do tego toru na tym samym słupie, w jego osi na zwykłych mocowaniach (jeśli wejdą w konsole, rozsuń ręcznie); słup zostaje jeden" + +msgid "insulated on one side (card 0418)" +msgstr "z jednostronną izolacją (karta 0418)" + +msgid "to the beam of a gantry (card 0416)" +msgstr "do dźwigara bramki (karta 0416)" + +msgid "0416: the middle pole is a drop of a gantry; the anchor ropes go from the beam above the messenger to clamps on it about 3 m on both sides, the poles next to it carry nothing" +msgstr "0416: podwieszenie środkowe jest na zwisie bramki; liny kotwienia idą od dźwigara nad liną nośną do zacisków na niej ok. 3 m z obu stron, słupy sąsiednie nic nie niosą" + +msgid "Contact wires anchored apart: on another pole" +msgstr "Przewody jezdne zakotwione osobno: na innym słupie" + +msgid "LMB on the pole the anchor of the contact wires goes on" +msgstr "LPM na słupie, na którym ma być kotwienie przewodów jezdnych" + +msgid "The messenger stays on the anchor above (it becomes one of the messenger only: 0405 or the messenger of a split one), the contact wires go to an anchor of their own on the pole picked (0404 or the wires of a split one)" +msgstr "Lina nośna zostaje na kotwieniu wyżej (staje się kotwieniem samej liny: 0405 albo liną rozdzielonego), przewody jezdne idą na własne kotwienie na wskazanym słupie (0404 albo przewody rozdzielonego)" + +msgid "The contact wires are anchored on their own; the other anchor holds the messenger only" +msgstr "Przewody jezdne zakotwione osobno; drugie kotwienie trzyma samą linę nośną" + +msgid "contact wires" +msgstr "przewody jezdne" + +msgid "messenger" +msgstr "lina nośna" + +msgid "Messenger only: the wires straight past it" +msgstr "Sama lina: przewody prosto obok" + +msgid "The wires of the chain go straight from %s to %s, the messenger through %s" +msgstr "Przewody łańcucha idą prosto od %s do %s, lina nośna przez %s" + +msgid "This cantilever holds only the messenger: it leaves the chain, the contact wires go straight between the two cantilevers of the pole next to it in the chain, the messenger goes through this one and their cords hang from it" +msgstr "Ten wysięgnik trzyma tylko linę nośną: wypada z listy łańcucha, przewody jezdne idą prosto między dwoma wysięgnikami tego słupa obok niego w łańcuchu, lina nośna przez niego, a ich linki wiszą na niej" diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index f4dd0212..03afe621 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -4887,6 +4887,13 @@ void opengl33_renderer::Render_Alpha(TTraction *Traction) draw(solid.fittings); ++m_renderpass.draw_stats.drawcalls; } + // lina urządzenia naprężającego przy końcu przęsła (stal) + if (solid.rope != null_handle) + { + set_metal(catenary_rope_metal()); + draw(solid.rope); + ++m_renderpass.draw_stats.drawcalls; + } ::glDepthMask(depthwrite); } diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index a4bd8f61..c9f2650e 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -3673,6 +3673,13 @@ opengl_renderer::Render_Alpha( TTraction *Traction ) { m_geometry.draw( solid.fittings ); ++m_renderpass.draw_stats.drawcalls; } + // lina urządzenia naprężającego przy końcu przęsła (stal) + if( solid.rope != null_handle ) { + auto const &steel { catenary_rope_metal() }; + ::glColor4fv( glm::value_ptr( glm::vec4{ steel.color[ 0 ], steel.color[ 1 ], steel.color[ 2 ], 1.f } ) ); + m_geometry.draw( solid.rope ); + ++m_renderpass.draw_stats.drawcalls; + } ::glDepthMask( depthwrite ); } // debug data diff --git a/world/Traction.cpp b/world/Traction.cpp index 0d46f518..387b855c 100644 --- a/world/Traction.cpp +++ b/world/Traction.cpp @@ -140,6 +140,9 @@ TTraction::Load( cParser *parser, glm::dvec3 const &pOrigin ) { std::string token { parser->getToken() }; std::string catenarytype; + double catenarytension[ 2 ] { 0.0, 0.0 }; + glm::dvec3 catenaryrope2[ 2 ]; + bool catenaryrope2set[ 2 ] { false, false }; while( false == token.empty() && token != "endtraction" ) { @@ -152,11 +155,24 @@ TTraction::Load( cParser *parser, glm::dvec3 const &pOrigin ) { // symbol typu sieci, np. YwsC120-2C; starsze wersje symulatora pomijają ten dopisek catenarytype = parser->getToken( false ); } + else if( token == "rope2" ) { + // koniec drugiej liny nośnej przy kotwieniu: "rope2 start|end x y z" + auto const which { parser->getToken() }; + auto const point { LoadPoint( *parser ) + pOrigin }; + auto const end { which == "end" ? 1 : 0 }; + catenaryrope2[ end ] = point; + catenaryrope2set[ end ] = true; + } + else if( token == "tension" ) { + // naciąg liny nośnej i przewodów jezdnych w daN inny niż w typie sieci (brakujące ciężary w kotwieniu) + parser->getTokens( 2 ); + *parser >> catenarytension[ 0 ] >> catenarytension[ 1 ]; + } token = parser->getToken(); } Init(); // przeliczenie parametrów - catenary_span_assign( *this, catenarytype ); + catenary_span_assign( *this, catenarytype, catenarytension[ 0 ], catenarytension[ 1 ], catenaryrope2, catenaryrope2set ); // calculate traction location location( glm::mix( pPoint2, pPoint1, 0.5 ) ); diff --git a/world/catenary_cantilever.cpp b/world/catenary_cantilever.cpp index b08f135a..2cd0177c 100644 --- a/world/catenary_cantilever.cpp +++ b/world/catenary_cantilever.cpp @@ -280,6 +280,7 @@ void cantilever_catalog::parse( std::vector const &Tokens, std::str 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 == "width" ) { number( pole.width, 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 ); } @@ -287,10 +288,13 @@ void cantilever_catalog::parse( std::vector const &Tokens, std::str else if( key == "strut" ) { text( pole.strut ); } else if( key == "stay" ) { text( pole.stay ); } else if( key == "steady" ) { text( pole.steady ); } + else if( key == "console" ) { text( pole.console ); } + else if( key == "steady_console" ) { text( pole.steady_console ); } else { report( entry, "unknown keyword \"" + key + "\"" ); good = false; --index; } } else { if( key == "back" ) { text( mount.back ); } + else if( key == "console" ) { mount.console = true; number( mount.console_x[ 0 ], good ); number( mount.console_x[ 1 ], good ); number( mount.console_face, good ); } 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 ); } @@ -2071,6 +2075,20 @@ bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_sch return solve_cantilever_on_pole( Catalog, Scheme, Pole, Distance, RailHead, Messenger, Overrides, cantilever_umbrella(), Result ); } +void spread_mounts( cantilever_pole const &Pole, cantilever_result const &First, cantilever_result const &Second, + cantilever_umbrella &FirstSide, cantilever_umbrella &SecondSide ) { + + FirstSide.shared.clear(); + SecondSide.shared.clear(); + for( auto const *role : { "stay", "strut", "steady" } ) { + auto const a { First.values.find( std::string( "mount." ) + role ) }, b { Second.values.find( std::string( "mount." ) + role ) }; + if( a == First.values.end() || b == Second.values.end() ) { continue; } + auto shared { 0.5 * ( a->second + b->second ) }; + if( Pole.mount_max != 0.0 ) { shared = std::min( shared, std::max( Pole.mount_max, std::min( a->second, b->second ) ) ); } + FirstSide.shared[ role ] = SecondSide.shared[ role ] = shared; + } +} + 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 ) { @@ -2142,17 +2160,40 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co 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 }; + // wysięgniki rozstawione: mocowania -06 tej samej karty (4610-06, 4620-06, 4630-06) na konsoli słupa (4890); rozstawione + // wysięgi pomocnicze (0167, bez ukośnika i odciągu) na belce 4690 (steady_console, karta 33-0519), gdy słup ją ma + auto const onconsole = [ & ]( std::string const &Name ) { + if( false == Umbrella.spread || Name.empty() ) { return Name; } + return Name.substr( 0, Name.find( '-' ) ) + "-06"; + }; + auto const steadyonly { Scheme.roles.count( "strut" ) == 0 && Scheme.roles.count( "stay" ) == 0 }; + std::map consoles; + if( Umbrella.spread ) { + for( auto const *role : { "stay", "strut", "steady" } ) { + if( ( std::string( role ) != "steady" && steadyonly ) || ( std::string( role ) == "steady" && false == Scheme.steady_mounted ) ) { continue; } + // sam wysięg pomocniczy (0167) na belce 4690; wysięg wysięgnika trzyzaczepowego na tej samej konsoli 4890, co reszta + auto const &name { steadyonly && false == Pole.steady_console.empty() ? Pole.steady_console : Pole.console }; + auto const *found { name.empty() ? nullptr : Catalog.mount( name ) }; + if( found == nullptr || false == found->console ) { + return fail( name.empty() ? "the pole has no console for spread cantilevers (\"console\" / \"steady_console\" in its entry)" : "console \"" + name + "\" isn't in the catalogue as a console" ); + } + consoles[ role ] = found; + } + } + auto const *strut { mountpart( onconsole( Pole.strut ), "strut" ) }; + auto const *stay { mountpart( onconsole( Pole.stay ), "stay" ) }; + auto const *steady { Scheme.steady_mounted ? mountpart( onconsole( Pole.steady ), "steady arm" ) : nullptr }; if( strut == nullptr || stay == nullptr || ( Scheme.steady_mounted && steady == nullptr ) ) { return false; } + // oś słupa wzdłuż toru: przy wysięgnikach rozstawionych przesunięta od płaszczyzny wysięgnika + auto const polex { Umbrella.spread ? Umbrella.console_x : 0.0 }; std::vector poleparts; - auto const add = [ & ]( std::string const &Role, std::string const &Part, std::string const &Model, double const Y, double const Z ) { + auto const add = [ & ]( std::string const &Role, std::string const &Part, std::string const &Model, double const Y, double const Z, double const X = 0.0 ) { cantilever_placement placement; placement.role = Role; placement.part = Part; placement.model = Model; + placement.position[ 0 ] = X; placement.position[ 1 ] = Y; placement.position[ 2 ] = Z; poleparts.push_back( placement ); @@ -2163,15 +2204,25 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co 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 twin { false == Umbrella.spread && shared != Umbrella.shared.end() && false == Mount.twin.empty() }; + if( twin || ( Umbrella.spread && shared != Umbrella.shared.end() ) ) { 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 }; + auto face { Distance - 0.5 * depth }; + auto const found { consoles.find( Role ) }; + auto const *console { found != consoles.end() ? found->second : nullptr }; + if( console != nullptr ) { + // konsola na słupie (jedna na rolę, stawia ją jedna strona), mocowanie na jej punkcie, bliżej toru o jej grubość + if( Umbrella.console_parts ) { + add( Role + "_console", console->name, "", face, Bolts, polex ); + if( false == console->back.empty() ) { add( Role + "_console_back", console->back, "", face + depth, Bolts, polex ); } + } + face += console->console_face; + } 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 == twin && console == nullptr && 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() }; @@ -2179,7 +2230,7 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co // słup bramki stawia bramka if( Umbrella.pole && false == Pole.gantry ) { - add( "pole", Pole.name, Pole.model, Distance, RailHead ); + add( "pole", Pole.name, Pole.model, Distance, RailHead, polex ); 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 }; @@ -2188,7 +2239,7 @@ bool solve_on_pole_once( cantilever_catalog const &Catalog, cantilever_scheme co 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 ); } + if( false == Pole.foundation.empty() ) { add( "foundation", Pole.name + "_foundation", Pole.foundation, Distance, RailHead, polex ); } } auto const raise { Messenger.z - Scheme.height }; auto staybolts { Scheme.mount_stay + raise - stay->attach_z() }; diff --git a/world/catenary_cantilever.h b/world/catenary_cantilever.h index a9b540ae..113e91e9 100644 --- a/world/catenary_cantilever.h +++ b/world/catenary_cantilever.h @@ -114,6 +114,7 @@ struct cantilever_pole { 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 width { 0.0 }; // wzdłuż toru (słup kratowy 161x: wysokość ceownika, karta 33-1611-E wymiar a); 0 = nieznana 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 }; @@ -122,6 +123,8 @@ struct cantilever_pole { 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) + std::string console; // konsola wysięgników rozstawionych (wpis "mount ... console"); pusta = słup ich nie nosi + std::string steady_console; // belka 4690 rozstawionych wysięgów pomocniczych (karta 33-0519, wysięgi 0167); mocowanie wysięgu na niej // 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 }; @@ -140,6 +143,11 @@ struct cantilever_mount { 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 + // konsola wysięgników rozstawionych (4890, karty 0517/0518): dwa punkty mocowań w X (wzdłuż toru) i ich odsunięcie od + // płaszczyzny słupa (Y, ujemne - w stronę toru); na konsoli mocowania -06 (4610-06, 4620-06, 4630-06) + bool console { false }; + double console_x[ 2 ] { 0.0, 0.0 }; + double console_face { 0.0 }; 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 @@ -293,6 +301,11 @@ 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) + // wysięgniki rozstawione: oba po tej samej stronie słupa, na konsolach (konsola słupa), mocowania -06 na wspólnych + // wysokościach z shared; płaszczyzna wysięgnika przechodzi przez punkt mocowania na konsoli, oś słupa leży w X = ConsoleX + bool spread { false }; + double console_x { 0.0 }; + bool console_parts { true }; // czy stawiać konsole (stawia je 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, @@ -306,6 +319,11 @@ bool solve_cantilever_on_pole( cantilever_catalog const &Catalog, cantilever_sch 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 ); +// wspólne wysokości mocowań wysięgników rozstawionych: każda rola obu wysięgników na jednej konsoli, na średniej wysokości +// (nie ponad mount_max słupa). Wypełnia shared obu stron +void spread_mounts( 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 diff --git a/world/catenary_network.cpp b/world/catenary_network.cpp index 2be44403..7628bf4d 100644 --- a/world/catenary_network.cpp +++ b/world/catenary_network.cpp @@ -99,7 +99,10 @@ void type_registry::load_tables() { 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 }; + // dalej opcjonalnie: przekrój mm² i moduł sprężystości GPa + double area { 0.0 }, modulus { 0.0 }; + if( stream >> area ) { stream >> modulus; } + m_wires[ key ] = { mass, diameter * 0.001, std::max( 0.0, area ) * 1e-6, std::max( 0.0, modulus ) * 1e9 }; } else if( kind == "spacing" ) { int span { 0 }; @@ -267,25 +270,34 @@ bool solve_span_points( network_definition const &Definition, std::vector 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; + auto solved { false }; + // nietypowe przęsła (niska wysokość konstrukcyjna jednego końca, naciąg liny obniżony brakującymi ciężarami poniżej naciągu + // linki Y): zamiast rysowania po staremu najpierw bez linek Y, potem jako odcinek bez wieszaków + for( int attempt = 0; attempt < 3 && false == solved; ++attempt ) { + try { + if( attempt == 2 || Hangergaps.empty() ) { result = solve_free_span( type, length, input.height_start, input.height_end ); } + else { + if( attempt == 1 ) { input.y_at_start = input.y_at_end = false; } + result = solve_span( type, input, 4.0 ); } + solved = true; + } + catch( std::exception const &exception ) { + Error = exception.what(); } } + if( false == solved ) { return false; } + // lina poniżej przewodu jezdnego (przęsło za długie albo za mała wysokość konstrukcyjna dla tego typu sieci): podciągnięta + // nad przewód, wieszaki skrócone - przęsło wygląda dziwnie, ale się rysuje + // (końce zostają tam, gdzie są punkty przęsła: najniżej na prostej między nimi) + auto const lowest { ( type.messenger_count > 1 ? type.messenger_stack : 0.0 ) + 0.02 }; + for( auto *line : { &result.messenger, &result.y_start, &result.y_end } ) { + for( auto &point : *line ) { + auto const chord { input.height_start + ( input.height_end - input.height_start ) * std::clamp( point.x / length, 0.0, 1.0 ) }; + point.y = std::max( point.y, std::min( lowest, chord ) ); + } + } + for( auto &hanger : result.hangers ) { hanger.length = std::max( hanger.length, 0.0 ); } Solution.length = length; Solution.result = std::move( result ); return true; @@ -435,10 +447,27 @@ void build_geometry( network_definition const &Definition, span_solution const & if( type.messenger_count > 1 ) { std::vector middle; if( false == start.empty() ) { middle.push_back( start.back() ); } + // końce dolnej liny podane osobno (kotwienie): przesunięcie rozłożone liniowo wzdłuż przęsła + vec3 shiftstart { 0.0, 0.0, 0.0 }, shiftend { 0.0, 0.0, 0.0 }; + if( Points.messenger2_start_set && false == result.messenger.empty() ) { + auto const from { rope( result.messenger.front().x, result.messenger.front().y - type.messenger_stack ) }; + shiftstart = vec3 { Points.messenger2_start.x - from.x, Points.messenger2_start.y - from.y, Points.messenger2_start.z - from.z }; + } + if( Points.messenger2_end_set && false == result.messenger.empty() ) { + auto const to { rope( result.messenger.back().x, result.messenger.back().y - type.messenger_stack ) }; + shiftend = vec3 { Points.messenger2_end.x - to.x, Points.messenger2_end.y - to.y, Points.messenger2_end.z - to.z }; + } 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 == instart && false == inend ) { + auto at { rope( point.x, point.y - type.messenger_stack ) }; + auto const t { length > 1e-9 ? point.x / length : 0.0 }; + at.x += shiftstart.x * ( 1.0 - t ) + shiftend.x * t; + at.y += shiftstart.y * ( 1.0 - t ) + shiftend.y * t; + at.z += shiftstart.z * ( 1.0 - t ) + shiftend.z * t; + middle.push_back( at ); + } } if( false == end.empty() ) { middle.push_back( end.front() ); } polyline( middle, element_kind::messenger ); diff --git a/world/catenary_network.h b/world/catenary_network.h index e0806838..c50ed5ee 100644 --- a/world/catenary_network.h +++ b/world/catenary_network.h @@ -16,6 +16,8 @@ namespace catenary { struct wire_data { double mass { 0.0 }; // kg/m double diameter { 0.0 }; // m + double area { 0.0 }; // m², przekrój czynny (do wydłużeń sprężystych); 0 = nieznany + double modulus { 0.0 }; // Pa, moduł sprężystości przewodu (liny skręcane: niższy niż drutu); 0 = nieznany }; // pełny opis typu sieci: dane dla solvera i dane potrzebne tylko do rysowania @@ -67,6 +69,9 @@ struct vec3 { double x, y, z; }; struct span_points { vec3 contact_start, contact_end; vec3 messenger_start, messenger_end; + // druga (dolna) lina nośna kończy się gdzie indziej niż o odstęp lin pod pierwszą (uchwyt krańcowy w kotwieniu z dwiema liniami) + bool messenger2_start_set { false }, messenger2_end_set { false }; + vec3 messenger2_start, messenger2_end; }; // policzone przęsło diff --git a/world/catenary_scene.cpp b/world/catenary_scene.cpp index 5bf8bcad..a09cf2fa 100644 --- a/world/catenary_scene.cpp +++ b/world/catenary_scene.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "world/catenary_tensioner.h" #include "world/Traction.h" #include "model/AnimModel.h" +#include #include "simulation/simulation.h" #include "utilities/Globals.h" @@ -55,6 +56,12 @@ struct scene_suspension { 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 + // siodło liny nośnej (4160) ostatniego złożenia: obejma liny kotwienia środkowego staje obok niego na teowniku + bool has_saddle { false }; + std::string saddle_model; + catenary::cantilever_placement saddle; + double saddle_heading { 0.0 }; + glm::dvec3 saddle_origin { 0.0 }; }; std::vector Suspensions; @@ -75,6 +82,10 @@ struct scene_chain { 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 }; + // kotwienie środkowe: części na słupach sąsiednich podwieszeń, bryły (lina, V, zaciski) dla przęsła przed i za środkowym + std::vector middle_parts; + std::vector middle_solid[ 2 ]; + std::size_t middle_span { static_cast( -1 ) }; // przęsło przed podwieszeniem środkowym (za nim: +1) }; std::vector Chains; @@ -96,8 +107,13 @@ struct scene_anchor { 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) + bool has_messenger2 { false }; // druga lina nośna kończy się na swoim uchwycie (przęsło dociąga do niego dolną linę) + double messenger2[ 3 ] { 0.0, 0.0, 0.0 }; double temperature { 0.0 }; // temperatura, przy której złożono catenary::tensioner_result result; + std::vector rope; // lina jako bryła proceduralna (trójkąty, współrzędne scenerii); rysuje ją przęsło do kotwienia + // obrys urządzenia dla edytora (współrzędne scenerii): czoło słupa i rolka ruchoma na wysokości rolek, góra i spód kolumny ciężarów + double shape[ 4 ][ 3 ] { { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 } }; }; std::vector Anchors; @@ -391,7 +407,15 @@ assemble( scene_suspension &Suspension, catenary::cantilever_catalog const &Cata // 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 }; + Suspension.has_saddle = false; for( auto const &placement : result.parts ) { + if( false == Suspension.has_saddle && placement.role == "messenger_clamp" ) { + Suspension.has_saddle = true; + Suspension.saddle = placement; + Suspension.saddle_model = ( placement.model.empty() ? Catalog.models() + placement.part : placement.model ); + Suspension.saddle_heading = heading; + Suspension.saddle_origin = glm::dvec3 { Wire.x, Wire.y, Wire.z }; + } 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 " @@ -421,18 +445,35 @@ entry_points( catenary::suspension const &Data, glm::dvec3 &Wire, glm::dvec3 &Ro 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 ] } ); } +bool held_rope_at( std::size_t Index, glm::dvec3 &Rope ); + // 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 ); + // linę nośną łańcucha w tym miejscu trzyma inny wysięgnik: linki i zaczepy celują w nią + auto const held { held_rope_at( Index, rope ) }; + auto const *ropepoint { held || suspension.data.has_rope ? &rope : nullptr }; + // słup stawia inny wpis (trzeci wysięgnik przy rozstawionych) + catenary::cantilever_umbrella own; + own.pole = false == suspension.data.shared_pole; + if( suspension.data.has_spread ) { + // sam jeden na konsoli: płaszczyzna wysięgnika przez punkt mocowania na konsoli, konsole stawia on + auto const &point { suspension.data.spread }; + glm::dvec3 const plane { point[ 0 ], attach.y, point[ 2 ] }; + auto const heading { std::atan2( plane.x - wire.x, plane.z - wire.z ) }; + own.spread = true; + own.console_x = glm::dot( attach - plane, glm::dvec3 { -std::cos( heading ), 0.0, std::sin( heading ) } ); + return assemble( suspension, Catalog, wire, ropepoint, plane, &own ); + } + return assemble( suspension, Catalog, wire, ropepoint, attach, ( suspension.data.shared_pole ? &own : nullptr ) ); } // 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 ) { +assemble_umbrella( std::string const &Name, std::vector const &List, catenary::cantilever_catalog const &Catalog, std::size_t &Assembled, std::size_t &Parts, bool const Report = true ) { auto const alone = [ & ]( std::size_t const Index ) { auto const count { assemble_alone( Index, Catalog ) }; if( count > 0 ) { ++Assembled; Parts += count; } @@ -441,32 +482,60 @@ assemble_umbrella( std::string const &Name, std::vector const &List 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" ); + // w edytorze (kolejne zmiany obu wpisów pary) to stan chwilowy: komunikat tylko przy wczytaniu + if( Report ) { 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" ); + // w edytorze (kolejne zmiany obu wpisów pary) to stan chwilowy: komunikat tylko przy wczytaniu + if( Report ) { 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 ]; + // wysięgniki rozstawione: oba wpisy z punktem na konsoli; jeden bez niego - błąd, składane osobno + auto const spread { first.has_spread && Suspensions[ List.back() ].data.has_spread }; + if( spread != ( first.has_spread || Suspensions[ List.back() ].data.has_spread ) ) { + // w edytorze (kolejne zmiany obu wpisów pary) to stan chwilowy: komunikat tylko przy wczytaniu + if( Report ) { ErrorLog( "Bad scenario: umbrella \"" + Name + "\" has \"spread\" in one suspension only; they're assembled separately" ); } + for( auto const index : List ) { alone( index ); } + return; + } + glm::dvec3 wires[ 2 ], ropes[ 2 ], poles[ 2 ], attach[ 2 ]; + bool held[ 2 ] { false, false }; // punkt liny podany (wprost albo z wysięgnika, który ją tu trzyma) catenary::cantilever_result probes[ 2 ]; + catenary::cantilever_umbrella sides[ 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; + held[ side ] = held_rope_at( List[ side ], ropes[ side ] ) || suspension.data.has_rope; + attach[ side ] = poles[ 0 ]; + if( spread ) { + // płaszczyzna wysięgnika przez punkt mocowania na konsoli; oś słupa przesunięta od niej wzdłuż osi X wysięgnika + auto const &point { suspension.data.spread }; + attach[ side ] = { point[ 0 ], poles[ 0 ].y, point[ 2 ] }; + auto const heading { std::atan2( attach[ side ].x - wires[ side ].x, attach[ side ].z - wires[ side ].z ) }; + glm::dvec3 const xaxis { -std::cos( heading ), 0.0, std::sin( heading ) }; + sides[ side ].spread = true; + sides[ side ].console_x = glm::dot( poles[ 0 ] - attach[ side ], xaxis ); + } + auto probe { sides[ side ] }; + probe.console_parts = false; + probe.pole = false; + probed = probed && assemble( suspension, Catalog, wires[ side ], ( held[ side ] ? &ropes[ side ] : nullptr ), attach[ side ], ( spread ? &probe : 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 ] ); + if( spread ) { catenary::spread_mounts( *pole, probes[ 0 ], probes[ 1 ], sides[ 0 ], sides[ 1 ] ); } + else { catenary::umbrella_mounts( Catalog, *pole, probes[ 0 ], probes[ 1 ], sides[ 0 ], sides[ 1 ] ); } + sides[ 0 ].pole = false == first.shared_pole && false == Suspensions[ List.back() ].data.shared_pole; sides[ 1 ].pole = false; + sides[ 1 ].console_parts = 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 ] ) }; + auto const count { assemble( suspension, Catalog, wires[ side ], ( held[ side ] ? &ropes[ side ] : nullptr ), attach[ side ], &sides[ side ] ) }; if( count > 0 ) { ++Assembled; Parts += count; } } } @@ -596,6 +665,19 @@ assemble_gantry( scene_gantry &Gantry ) { return count; } +std::size_t suspension_named( std::string const &File, std::string const &Name ); +// czy łańcuch kończy się na tym kotwieniu (lina z całością albo same przewody: anchor_*_contact); Start - na początku łańcucha +inline bool +anchors_chain( catenary::chain const &Chain, std::string const &Name, bool *Start = nullptr ) { + if( Name.empty() ) { return false; } + auto const start { Chain.anchor_start == Name || Chain.anchor_start_contact == Name }; + if( Start != nullptr ) { *Start = start; } + return start || Chain.anchor_end == Name || Chain.anchor_end_contact == Name; +} +void follow_anchor( scene_anchor const &Anchor ); +void add_tube( std::vector &Triangles, std::vector const &Points, double Radius ); +void rope_of( catenary::suspension const &Data, double *Rope ); + // urządzenia naprężające (traction_tensioners.txt); czytane przy pierwszym użyciu catenary::tensioner_catalog const * tensioners() { @@ -630,23 +712,201 @@ assemble_anchor( scene_anchor &Anchor ) { 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; + // na istniejącym słupie: typ i punkt z podwieszenia, które go stawia + auto poletype { data.pole_type }; + double polepoint[ 3 ] { data.pole[ 0 ], data.pole[ 1 ], data.pole[ 2 ] }; + double cantilever[ 2 ] { 0.0, 0.0 }; // na istniejącym słupie: kierunek jego wysięgnika w planie + if( false == data.on.empty() ) { + auto const index { suspension_named( Anchor.file, data.on ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_pole ) { + ErrorLog( label + "is on the pole of suspension \"" + data.on + "\", which isn't in the file or has no pole" ); + return 0; } - else { ErrorLog( label + "uses network type \"" + data.network + "\" which has no file" ); } + poletype = Suspensions[ index ].data.pole_type; + std::copy( std::begin( Suspensions[ index ].data.pole ), std::end( Suspensions[ index ].data.pole ), polepoint ); + // płaszczyzna wysięgnika tego słupa (od słupa do drutu): oś słupa wzdłuż toru jest do niej prostopadła + if( Suspensions[ index ].data.has_at ) { + cantilever[ 0 ] = Suspensions[ index ].data.at[ 0 ] - polepoint[ 0 ]; + cantilever[ 1 ] = Suspensions[ index ].data.at[ 2 ] - polepoint[ 2 ]; + } + } + auto const *pole { catalog->pole( poletype ) }; + if( pole == nullptr ) { ErrorLog( label + "uses pole \"" + poletype + "\" which isn't in the supports catalogue" ); return 0; } + catenary::tensioner_input input; + // typ sieci: łańcucha, który się na tym kotwieniu kończy (zmiana typu w łańcuchu zmienia kotwienie), inaczej z wpisu + auto network { data.network }; + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Anchor.file || data.name.empty() || chain.data.type.empty() ) { continue; } + if( anchors_chain( chain.data, data.name ) ) { network = chain.data.type; break; } + } + if( false == network.empty() ) { + if( auto const *type { catenary::types().find( network ) }; type != nullptr ) { + input.type = devices->code_for( type->type.symbol, type->type.code ); + input.contacts = type->type.contact_count; + input.messengers = type->type.messenger_count; + input.messenger_stack = type->type.messenger_stack; + // sztywności i ciężar sieci do skracania przy brakujących ciężarach (przekrój i moduł z traction_tables.txt) + auto const &net { type->type }; + input.messenger_stiffness = net.messenger_count * type->messenger_wire.area * type->messenger_wire.modulus * 0.1; + input.contact_stiffness = net.contact_count * type->contact_wire.area * type->contact_wire.modulus * 0.1; + input.load = ( net.messenger_count * net.messenger_mass + net.contact_count * net.contact_mass ) * input.gravity; + // urządzenie bez danych dla tego typu (np. 0461 dla dwóch lin nośnych, 0403 dla sieci 32): najpierw z tej samej karty + // (nazwa do "-": 0461-2x2, 0403-32), potem dowolne tego samego rodzaju (ciężarowe / stałe) + if( device->type( input.type ) == nullptr ) { + auto const card { data.device.substr( 0, data.device.find( '-' ) ) }; + catenary::tensioner_device const *same { nullptr }, *kind { nullptr }; + for( auto const &other : devices->devices() ) { + if( other.fixed != device->fixed || other.only != device->only || other.type( input.type ) == nullptr ) { continue; } + if( same == nullptr && other.name.substr( 0, other.name.find( '-' ) ) == card ) { same = &other; } + if( kind == nullptr ) { kind = &other; } + } + if( same != nullptr ) { device = same; } + else if( kind != nullptr ) { device = kind; } + // nikt nie ma danych dla tej sieci (brak wpisu "network ... as"): pierwsze dane urządzenia, żeby kotwienie stało + if( device->type( input.type ) == nullptr && false == device->types.empty() ) { + WriteLog( ( label + "network type \"" + network + "\" has no data in the anchor devices; device " + device->name + " uses the data of network type " + std::to_string( device->types.front().code ) ).c_str() ); + input.type = device->types.front().code; + } + } + } + else { ErrorLog( label + "uses network type \"" + 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.height = data.height; + input.rope_height = data.rope_height; + // uchwyty do słupa: tył po drugiej stronie słupa; zero urządzenia na czole przy wysokości rolek, słup zbieżny + input.pole = poletype; + { + auto const *devicetable { devices->table( device->table ) }; + auto const rollers { data.height > 0.0 ? data.height : devicetable != nullptr ? devicetable->height : 6.7 }; + // słup z szerokością wzdłuż toru (161x): urządzenie i uchwyty na ścianach wzdłuż drutów, śruby przez szerokość + input.depth = [ pole, rollers ]( double const Height ) { return pole->width > 0.0 ? pole->width : 0.5 * pole->depth( rollers ) + 0.5 * pole->depth( Height ); }; + } input.color = data.color; input.top_color = data.top_color; + // stare złożenie: łańcuch z tym kotwieniem ma druty zaśniedziałe (stan 1); średnia rozpiętość jego przęseł między podwieszeniami + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Anchor.file || data.name.empty() ) { continue; } + if( anchors_chain( chain.data, data.name ) ) { + input.old_fittings = ( chain.data.state == 1 ); + // łańcuch z kotwieniem środkowym: odległość od niego liczona wzdłuż łańcucha (podwieszenia aż do słupa kotwienia) + if( false == chain.data.middle.empty() ) { + auto const &names { chain.data.suspensions }; + auto const middle { std::find( names.begin(), names.end(), chain.data.middle ) }; + if( middle != names.end() ) { + bool start { false }; + anchors_chain( chain.data, data.name, &start ); + auto const from { static_cast( middle - names.begin() ) }; + auto const to { start ? std::ptrdiff_t { 0 } : static_cast( names.size() ) - 1 }; + double along { 0.0 }; + double previous[ 2 ]; + bool has { false }, good { true }; + for( auto k { from }; good; k += ( start ? -1 : 1 ) ) { + auto const index { suspension_named( Anchor.file, names[ k ] ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { good = false; break; } + auto const *at { Suspensions[ index ].data.at }; + if( has ) { along += std::hypot( at[ 0 ] - previous[ 0 ], at[ 2 ] - previous[ 1 ] ); } + previous[ 0 ] = at[ 0 ]; previous[ 1 ] = at[ 2 ]; + has = true; + if( k == to ) { break; } + } + if( good && has ) { + along += std::hypot( polepoint[ 0 ] - previous[ 0 ], polepoint[ 2 ] - previous[ 1 ] ); + input.distance = along; + } + } + } + double total { 0.0 }; + int spans { 0 }; + double previous[ 3 ]; + bool has { false }; + for( auto const &name : chain.data.suspensions ) { + auto const index { suspension_named( Anchor.file, name ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { has = false; continue; } + auto const *at { Suspensions[ index ].data.at }; + if( has ) { total += std::hypot( at[ 0 ] - previous[ 0 ], at[ 2 ] - previous[ 2 ] ); ++spans; } + std::copy( at, at + 3, previous ); + has = true; + } + if( spans > 0 ) { input.span = total / spans; } + } + } + // izolatory: rolkowe ESN-2025 z wyboru, a bez wyboru przy drutach zaśniedziałych (jak uchwyty 5110) + if( data.insulators.empty() ) { input.old_messenger = input.old_contact = input.old_fittings; } + else { + input.old_messenger = ( data.insulators == "old" || data.insulators == "messenger" ); + input.old_contact = ( data.insulators == "old" || data.insulators == "contact" ); + } + // kierunek drutów w planie; zero urządzenia na czole słupa + glm::dvec3 const axis { polepoint[ 0 ], polepoint[ 1 ], polepoint[ 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 ) }; + // czoło: kotwienie ciągnie wzdłuż toru, więc słup kratowy z szerokością wzdłuż toru ma czoło w połowie szerokości + auto const face { pole->width > 0.0 ? 0.5 * pole->width : 0.5 * pole->depth( data.height > 0.0 ? data.height : table != nullptr ? table->height : 6.7 ) }; + // oś słupa wzdłuż toru (w stronę kotwionego przęsła): własny słup kotwowy stoi w linii drutów; istniejący - prostopadle + // do swojego wysięgnika. Uchwyty, prowadnica i stos są przy słupie, urządzenie od przegubu ciągnie do drutów + auto ax { dx }, az { dz }; + if( auto const reach { std::hypot( cantilever[ 0 ], cantilever[ 1 ] ) }; reach > 0.1 ) { + ax = -cantilever[ 1 ] / reach; + az = cantilever[ 0 ] / reach; + if( ax * dx + az * dz < 0.0 ) { ax = -ax; az = -az; } + } + glm::dvec3 const origin { axis.x + ax * face, axis.y, axis.z + az * face }; + { + // urządzenie od czoła w stronę ostatniego podwieszenia + auto const ex { data.toward[ 0 ] - origin.x }, ez { data.toward[ 2 ] - origin.z }; + auto const reach { std::hypot( ex, ez ) }; + if( reach > 0.5 ) { dx = ex / reach; dz = ez / reach; } + } + 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 ] }; + }; + // łańcuchy do drutów wycelowane w ostatnie podwieszenie przed kotwieniem (przewody podchodzą pod kątem) + { + // punkt sceny -> układ urządzenia: osie urządzenia w scenie z toscene, układ prostokątny + double const zero[ 3 ] { 0.0, 0.0, 0.0 }; + auto const base { toscene( zero ) }; + glm::dvec3 axes[ 3 ]; + for( int a = 0; a < 3; ++a ) { + double unit[ 3 ] { 0.0, 0.0, 0.0 }; + unit[ a ] = 1.0; + axes[ a ] = toscene( unit ) - base; + } + auto const todevice = [ & ]( double const *Point, double *Out ) { + glm::dvec3 const d { Point[ 0 ] - base.x, Point[ 1 ] - base.y, Point[ 2 ] - base.z }; + for( int a = 0; a < 3; ++a ) { Out[ a ] = glm::dot( d, axes[ a ] ); } + }; + // oś słupa w układzie urządzenia + glm::dvec3 const structure { ax, 0.0, az }; + input.structure_angle = std::atan2( glm::dot( structure, axes[ 1 ] ), glm::dot( structure, axes[ 0 ] ) ); + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Anchor.file || data.name.empty() || chain.data.suspensions.empty() ) { continue; } + std::string neighbour; + bool start { false }; + if( false == anchors_chain( chain.data, data.name, &start ) ) { continue; } + neighbour = ( start ? chain.data.suspensions.front() : chain.data.suspensions.back() ); + auto const index { suspension_named( Anchor.file, neighbour ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].data.has_at ) { break; } + double rope[ 3 ]; + rope_of( Suspensions[ index ].data, rope ); + todevice( Suspensions[ index ].data.at, input.contact_target ); + todevice( rope, input.messenger_target ); + input.aimed = true; + break; + } + } 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 ); } @@ -655,29 +915,45 @@ assemble_anchor( scene_anchor &Anchor ) { 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 ); + // lina: ucha jako części, reszta jedną bryłą proceduralną (bez łączeń na łukach rolek), przeliczoną na scenerię; + // rysuje ją z bliska przęsło dochodzące do kotwienia (rebuild_span) + { + std::vector tube; + catenary::tensioner_rope_solid( result, 0.5 * device->rope_diameter, 8, device->rope_eye, device->rope_eye_length, tube, device->rope_spacer, device->rope_spacer_length ); + // kotwienie rozdzielone: druga lina (urządzenie przewodów), z uchami + if( false == result.rope2.empty() ) { + catenary::tensioner_result second; + second.rope = result.rope2; + catenary::tensioner_rope_solid( second, 0.5 * device->rope_diameter, 8, device->rope_eye, device->rope_eye_length, tube, device->rope_spacer, device->rope_spacer_length ); + for( auto part : second.parts ) { part.role = "contact_device." + part.role; result.parts.push_back( part ); } + } + double const zero[ 3 ] { 0.0, 0.0, 0.0 }; + auto const base { toscene( zero ) }; + glm::dvec3 axes[ 3 ]; + for( int a = 0; a < 3; ++a ) { + double unit[ 3 ] { 0.0, 0.0, 0.0 }; + unit[ a ] = 1.0; + axes[ a ] = toscene( unit ) - base; + } + // przeliczenie na scenerię może odbijać (zamiana osi): wtedy odwrócić kolejność wierzchołków trójkątów + auto const &ax { axes[ 0 ] }, &ay { axes[ 1 ] }, &az { axes[ 2 ] }; + auto const mirrored { ax.x * ( ay.y * az.z - ay.z * az.y ) + ax.y * ( ay.z * az.x - ay.x * az.z ) + ax.z * ( ay.x * az.y - ay.y * az.x ) < 0.0 }; + Anchor.rope.clear(); + Anchor.rope.reserve( tube.size() ); + for( std::size_t k = 0; k < tube.size(); ++k ) { + auto const &item { tube[ mirrored ? ( k - k % 3 ) + ( 2 - k % 3 ) : k ] }; + auto const at { toscene( item.position ) }; + catenary::solid_vertex vertex; + vertex.position = catenary::vec3 { at.x, at.y, at.z }; + vertex.normal = catenary::vec3 { + ax.x * item.normal[ 0 ] + ay.x * item.normal[ 1 ] + az.x * item.normal[ 2 ], + ax.y * item.normal[ 0 ] + ay.y * item.normal[ 1 ] + az.y * item.normal[ 2 ], + ax.z * item.normal[ 0 ] + ay.z * item.normal[ 1 ] + az.z * item.normal[ 2 ] }; + Anchor.rope.push_back( vertex ); + } } - 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 }; @@ -692,8 +968,32 @@ assemble_anchor( scene_anchor &Anchor ) { Anchor.messenger[ 0 ] = messenger.x; Anchor.messenger[ 1 ] = messenger.y; Anchor.messenger[ 2 ] = messenger.z; + // druga lina nośna: koniec na jej uchwycie krańcowym, przęsło do kotwienia dociąga tam dolną linę (dopisek "rope2") + auto const second { result.messenger2_end[ 0 ] != 0.0 || result.messenger2_end[ 1 ] != 0.0 || result.messenger2_end[ 2 ] != 0.0 }; + Anchor.has_messenger2 = second && input.messengers >= 2; + if( Anchor.has_messenger2 ) { + auto const at { toscene( result.messenger2_end ) }; + Anchor.messenger2[ 0 ] = at.x; + Anchor.messenger2[ 1 ] = at.y; + Anchor.messenger2[ 2 ] = at.z; + } for( int k = 0; k < 3; ++k ) { Anchor.contact[ k ] = contact[ k ] / ends; } } + { + // obrys dla edytora: czoło słupa i rolka ruchoma na wysokości rolek, kolumna ciężarów od wolnego końca liny do spodu + auto const column { result.rope.empty() ? std::vector { 0.0, 0.0, result.height } : result.rope.back() }; + double const points[ 4 ][ 3 ] { + { 0.0, 0.0, result.height }, + { result.b, 0.0, result.height }, + { column[ 0 ], column[ 1 ], column[ 2 ] }, + { column[ 0 ], column[ 1 ], result.s } }; + for( int k = 0; k < 4; ++k ) { + auto const at { toscene( points[ k ] ) }; + Anchor.shape[ k ][ 0 ] = at.x; + Anchor.shape[ k ][ 1 ] = at.y; + Anchor.shape[ k ][ 2 ] = at.z; + } + } Anchor.solved = true; auto const skin { data.skin.empty() ? std::string( "none" ) : data.skin }; @@ -714,8 +1014,9 @@ assemble_anchor( scene_anchor &Anchor ) { ++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") - { + // 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"); + // istniejący słup stawia jego podwieszenie + if( data.on.empty() ) { 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 ) }; @@ -784,6 +1085,8 @@ 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 held_rope( scene_chain const &Chain, std::size_t Position, double *Rope ); + bool chain_point( scene_chain const &Chain, std::size_t Point, double *Wire, double *Rope, bool &Anchored ) { auto const &data { Chain.data }; @@ -799,14 +1102,107 @@ chain_point( scene_chain const &Chain, std::size_t Point, double *Wire, double * } if( Anchored ) { if( anchor == nullptr || false == anchor->solved ) { return false; } - std::copy( std::begin( anchor->contact ), std::end( anchor->contact ), Wire ); + // przewody zakotwione osobno: ich koniec z drugiego kotwienia + auto const &contactname { Point == 0 && false == data.anchor_start.empty() ? data.anchor_start_contact : data.anchor_end_contact }; + auto const *contact { contactname.empty() ? anchor : anchor_named( Chain.file, contactname ) }; + if( contact == nullptr || false == contact->solved ) { return false; } + std::copy( std::begin( contact->contact ), std::end( contact->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 ); + // lina nośna z innego wysięgnika (rozstawione, sama lina): gdy go nie ma, z tego samego + if( false == held_rope( Chain, Point, Rope ) ) { rope_of( Suspensions[ index ].data, Rope ); } + return true; +} + +// lina nośna łańcucha w miejscu Position listy, gdy trzyma ją inny wysięgnik (messenger_from): punkt liny trzymającego, +// przesunięty wzdłuż łańcucha do miejsca tego podwieszenia, na wysokości liny trzymającego (rozstawione wysięgi pomocnicze +// 0167 i wysięgnik samej liny w osi słupa między nimi: lina idzie prosto przez siodło, linki obu wysięgów trafiają w nią). +// false, gdy linę trzyma to samo podwieszenie +bool +held_rope( scene_chain const &Chain, std::size_t const Position, double *Rope ) { + auto const &names { Chain.data.suspensions }; + if( Position >= names.size() ) { return false; } + auto const &holder { catenary::messenger_holder( Chain.data, names[ Position ] ) }; + if( holder == names[ Position ] ) { return false; } + auto const own { suspension_named( Chain.file, names[ Position ] ) }, other { suspension_named( Chain.file, holder ) }; + if( own >= Suspensions.size() || other >= Suspensions.size() || false == Suspensions[ other ].data.has_at ) { return false; } + auto const &listed { Suspensions[ own ].data }, &holding { Suspensions[ other ].data }; + rope_of( holding, Rope ); + // przesunięcie tylko, gdy ten sam wysięgnik trzyma linę także w sąsiednim punkcie listy (para wysięgów pomocniczych z + // wysięgnikiem samej liny między nimi); sam jeden (rozstawione na konsoli) - lina łamie się w punkcie trzymającego + auto const sameholder = [ & ]( std::size_t const K ) { return K < names.size() && K != Position && catenary::messenger_holder( Chain.data, names[ K ] ) == holder; }; + if( false == sameholder( Position + 1 ) && ( Position == 0 || false == sameholder( Position - 1 ) ) ) { return true; } + // kierunek łańcucha w tym miejscu: od poprzedniego do następnego podwieszenia listy + auto const at = [ & ]( std::size_t const K ) { + auto const index { suspension_named( Chain.file, names[ K ] ) }; + auto const &data { index < Suspensions.size() && Suspensions[ index ].data.has_at ? Suspensions[ index ].data : listed }; + return glm::dvec2 { data.at[ 0 ], data.at[ 2 ] }; + }; + auto const way { at( Position + 1 < names.size() ? Position + 1 : Position ) - at( Position > 0 ? Position - 1 : Position ) }; + if( glm::length( way ) < 0.1 ) { return true; } + auto const along { glm::normalize( way ) }; + auto const shift { along * glm::dot( glm::dvec2 { listed.at[ 0 ] - holding.at[ 0 ], listed.at[ 2 ] - holding.at[ 2 ] }, along ) }; + Rope[ 0 ] += shift.x; + Rope[ 2 ] += shift.y; + return true; +} + +// to samo dla podwieszenia Index (pierwszy łańcuch jego pliku, w którym linę w jego miejscu trzyma inny wysięgnik) +bool +held_rope_at( std::size_t const Index, glm::dvec3 &Rope ) { + if( Index >= Suspensions.size() || Suspensions[ Index ].data.name.empty() ) { return false; } + auto const &name { Suspensions[ Index ].data.name }; + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != Suspensions[ Index ].file ) { continue; } + auto const &names { chain.data.suspensions }; + auto const found { std::find( names.begin(), names.end(), name ) }; + if( found == names.end() ) { continue; } + double point[ 3 ]; + if( held_rope( chain, static_cast( found - names.begin() ), point ) ) { + Rope = glm::dvec3 { point[ 0 ], point[ 1 ], point[ 2 ] }; + return true; + } + } + return false; +} + +// naciąg przęsła k łańcucha od kotwienia z niepełnym stosem ciężarów (daN): przęsła od tego kotwienia do kotwienia +// środkowego albo stałego (distance wzdłuż łańcucha); false, gdy przęsło ma naciąg typu sieci. Bliższe kotwienie wygrywa, +// ciężary na gruncie zostawiają naciąg typu (sieć bez naciągu urządzenia nie da się policzyć) +bool +span_tension( scene_chain const &Chain, std::size_t const Span, double &Messenger, double &Contact ) { + auto const *type { Chain.data.type.empty() ? nullptr : catenary::types().find( Chain.data.type ) }; + if( type == nullptr ) { return false; } + auto const points { chain_points( Chain ) }; + if( Span + 1 >= points ) { return false; } + // odległości wzdłuż łańcucha od początku do środka przęsła i od środka przęsła do końca + std::vector along( points, 0.0 ); + double previous[ 3 ], wire[ 3 ], rope[ 3 ]; + bool anchored; + for( std::size_t point = 0; point < points; ++point ) { + if( false == chain_point( Chain, point, wire, rope, anchored ) ) { return false; } + if( point > 0 ) { along[ point ] = along[ point - 1 ] + std::hypot( wire[ 0 ] - previous[ 0 ], wire[ 2 ] - previous[ 2 ] ); } + std::copy( wire, wire + 3, previous ); + } + auto const middle { 0.5 * ( along[ Span ] + along[ Span + 1 ] ) }; + double best { 1e30 }, ratio { 1.0 }; + for( auto const end : { 0, 1 } ) { + auto const &name { end == 0 ? Chain.data.anchor_start : Chain.data.anchor_end }; + if( name.empty() ) { continue; } + auto const *anchor { anchor_named( Chain.file, name ) }; + if( anchor == nullptr || false == anchor->solved || anchor->result.grounded || anchor->result.nominal_tension <= 0.0 ) { continue; } + auto const reach { end == 0 ? middle : along.back() - middle }; + if( reach > anchor->data.distance || reach >= best ) { continue; } + best = reach; + ratio = anchor->result.tension / anchor->result.nominal_tension; + } + if( std::abs( ratio - 1.0 ) < 1e-3 || ratio <= 0.0 ) { return false; } + Messenger = 0.1 * type->type.messenger_tension * ratio; + Contact = 0.1 * type->type.contact_tension * ratio; return true; } @@ -823,8 +1219,20 @@ span_body( scene_chain const &Chain, std::size_t const Span ) { 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.type.empty() ) { + body += " type " + data.type; + // brakujące ciężary w kotwieniu: mniejszy naciąg, większy zwis (starsze wersje symulatora pomijają ten dopisek) + double messenger, contact; + if( span_tension( Chain, Span, messenger, contact ) ) { body += " tension " + text_number( messenger, 0 ) + " " + text_number( contact, 0 ); } + } if( false == data.parallel.empty() ) { body += " parallel " + data.parallel; } + // kotwienie z dwiema liniami: dolna lina przęsła kończy się na swoim uchwycie krańcowym + for( auto const end : { 0, 1 } ) { + if( ( end == 0 ? anchoreda : anchoredb ) == false ) { continue; } + auto const *anchor { anchor_named( Chain.file, end == 0 ? ( Span == 0 ? data.anchor_start : std::string() ) : data.anchor_end ) }; + if( anchor == nullptr || false == anchor->has_messenger2 ) { continue; } + body += std::string( " rope2 " ) + ( end == 0 ? "start " : "end " ) + point_text( anchor->messenger2 ); + } return body + " endtraction"; } @@ -833,6 +1241,355 @@ 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 ) ); } +// prostopadłościan (bryła zastępcza osprzętu bez modelu): środek, oś wzdłuż Along, długość i przekrój +void +add_box( std::vector &Triangles, glm::dvec3 const &Center, glm::dvec3 Along, double const Length, double const Size ) { + Along = glm::length( Along ) > 1e-9 ? glm::normalize( Along ) : glm::dvec3 { 1.0, 0.0, 0.0 }; + auto side { glm::cross( Along, glm::dvec3 { 0.0, 1.0, 0.0 } ) }; + if( glm::length( side ) < 1e-6 ) { side = glm::dvec3 { 1.0, 0.0, 0.0 }; } + side = glm::normalize( side ); + auto const up { glm::normalize( glm::cross( side, Along ) ) }; + glm::dvec3 const axes[ 3 ] { Along * ( 0.5 * Length ), side * ( 0.5 * Size ), up * ( 0.5 * Size ) }; + // sześć ścian: dla każdej osi strona + i -; wierzchołki przeciwnie do ruchu wskazówek patrząc z zewnątrz + for( int a = 0; a < 3; ++a ) { + for( auto const sign : { 1.0, -1.0 } ) { + auto const normal { axes[ a ] * sign }; + auto const &u { axes[ ( a + 1 ) % 3 ] }, &v { axes[ ( a + 2 ) % 3 ] }; + auto const c { Center + normal }; + glm::dvec3 const corner[ 4 ] { c - u - v, c + u - v, c + u + v, c - u + v }; + int const order[ 2 ][ 6 ] { { 0, 1, 2, 0, 2, 3 }, { 0, 2, 1, 0, 3, 2 } }; + auto const n { glm::normalize( normal ) }; + for( auto const k : order[ sign > 0.0 ? 0 : 1 ] ) { + catenary::solid_vertex vertex; + vertex.position = catenary::vec3 { corner[ k ].x, corner[ k ].y, corner[ k ].z }; + vertex.normal = catenary::vec3 { n.x, n.y, n.z }; + Triangles.push_back( vertex ); + } + } + } +} + +// rura po łamanej (lina kotwienia środkowego, ramiona V), współrzędne scenerii +void +add_tube( std::vector &Triangles, std::vector const &Points, double const Radius ) { + catenary::tensioner_result path; + for( auto const &point : Points ) { path.rope.push_back( { point.x, point.y, point.z } ); } + std::vector tube; + catenary::tensioner_rope_solid( path, Radius, 6, std::string(), 0.0, tube ); + for( auto const &item : tube ) { + catenary::solid_vertex vertex; + vertex.position = catenary::vec3 { item.position[ 0 ], item.position[ 1 ], item.position[ 2 ] }; + vertex.normal = catenary::vec3 { item.normal[ 0 ], item.normal[ 1 ], item.normal[ 2 ] }; + Triangles.push_back( vertex ); + } +} + +// czy przęsło k łańcucha ma linkę Y przy końcu End (0: punkt k, 1: punkt k+1) - tak samo jak y_at w catenary_span.cpp: +// oba przęsła przy podwieszeniu mają wieszaki i tę samą długość Y z tablicy typu +bool +span_y_at( scene_chain const &Chain, catenary::network_definition const &Type, std::size_t const Span, int const End ) { + if( Type.type.y_tension <= 0.0 ) { return false; } + auto const ylength = [ & ]( std::size_t const K, double &Out ) { + if( K + 1 >= chain_points( Chain ) ) { return false; } + double wirea[ 3 ], ropea[ 3 ], wireb[ 3 ], ropeb[ 3 ]; + bool anchoreda, anchoredb; + if( false == chain_point( Chain, K, wirea, ropea, anchoreda ) || false == chain_point( Chain, K + 1, wireb, ropeb, anchoredb ) ) { return false; } + if( anchoreda || anchoredb || Chain.data.hangers[ 1 ] <= 0.0 ) { return false; } // przęsło bez wieszaków + auto const length { std::hypot( wireb[ 0 ] - wirea[ 0 ], wireb[ 2 ] - wirea[ 2 ] ) }; + auto const y { Type.type.y_length( length ) }; + Out = ( y < length ? y : 0.0 ); + return Out > 0.0; + }; + double mine, other; + if( false == ylength( Span, mine ) ) { return false; } + if( End == 0 && Span == 0 ) { return false; } + if( false == ylength( End == 0 ? Span - 1 : Span + 1, other ) ) { return false; } + return std::abs( mine - other ) <= 0.01; +} + +// kotwienie środkowe łańcucha (karty 63-0406 / 0408): przy podwieszeniu "middle" lina kotwienia zaciśnięta na linie nośnej 1,00 m +// z każdej strony, dalej do słupów sąsiednich podwieszeń (tam 4430 w uchwytach i łańcuch z izolatorem - urządzenie +// i -2 z regulacją), w połowie obu przęseł V 1,50 + 1,50 m do przewodu jezdnego. Lina, V i zaciski jako bryły (bez modeli) +void +assemble_middle( scene_chain &Chain ) { + for( auto *part : Chain.middle_parts ) { catenary_part_delete( part ); } + Chain.middle_parts.clear(); + Chain.middle_solid[ 0 ].clear(); + Chain.middle_solid[ 1 ].clear(); + Chain.middle_span = static_cast( -1 ); + auto const &data { Chain.data }; + if( Chain.removed || data.middle.empty() ) { return; } + auto const label { "Bad scenario: chain \"" + ( data.name.empty() ? std::string( "none" ) : data.name ) + "\" " }; + auto const found { std::find( data.suspensions.begin(), data.suspensions.end(), data.middle ) }; + if( found == data.suspensions.end() ) { ErrorLog( label + "has its middle anchor at \"" + data.middle + "\", which isn't in the chain" ); return; } + auto const position { static_cast( found - data.suspensions.begin() ) }; + if( position == 0 || position + 1 >= data.suspensions.size() ) { ErrorLog( label + "has its middle anchor at an end of the chain; it needs a pole on each side" ); return; } + auto const offset { data.anchor_start.empty() ? std::size_t { 0 } : std::size_t { 1 } }; + double wires[ 3 ][ 3 ], ropes[ 3 ][ 3 ]; + bool anchored; + for( int k = 0; k < 3; ++k ) { + if( false == chain_point( Chain, position + offset + k - 1, wires[ k ], ropes[ k ], anchored ) ) { return; } + } + auto const *catalog { cantilevers() }; + auto const *devices { tensioners() }; + auto const *type { data.type.empty() ? nullptr : catenary::types().find( data.type ) }; + if( catalog == nullptr || devices == nullptr ) { return; } + Chain.middle_span = position + offset - 1; + auto const vec = []( double const *P ) { return glm::dvec3 { P[ 0 ], P[ 1 ], P[ 2 ] }; }; + auto const middlerope { vec( ropes[ 1 ] ) }; + double const ropeheight { 0.0045 }; // promień liny 9831-5 - ZAŁOŻENIE (przekroju brak) + // 63-0416 do dźwigara bramki: podwieszenie środkowe na zwisie, liny od dźwigara (4590 na jego spodzie, nad liną nośną) do + // zacisków na linie nośnej ok. 3,00 m z obu stron; bez urządzeń na słupach sąsiednich + auto tobeam { data.middle_device == "0416" }; + glm::dvec3 beam { middlerope }; + if( tobeam ) { + auto const index { suspension_named( Chain.file, data.middle ) }; + auto const *own { index < Suspensions.size() && Suspensions[ index ].data.has_pole ? catalog->pole( Suspensions[ index ].data.pole_type ) : nullptr }; + if( own == nullptr || own->drop.empty() ) { + ErrorLog( label + "has a middle anchor to a gantry beam (0416) at \"" + data.middle + "\", which doesn't hang on a drop of a gantry; anchored to the poles next to it instead (0406)" ); + tobeam = false; + } + else { beam.y = Suspensions[ index ].data.pole[ 1 ]; } // spód dźwigara (zero zwisu) + } + // wysięgnik teowy na słupie środkowym: lina kotwienia idzie przez obejmę na wysięgniku (4160, stare karty 33-0418, kolprojekt1 + // s. 156 poz. 1), bez zacisków 1,00 m od słupa (Jan 2026-10-23: zaciski rozstawione o 1 m tylko przy rurowych). ZAŁOŻENIE: + // obejmą jest 4160-1 z karty, lina przechodzi przez nią na wysokości liny nośnej + auto tee { false }; + glm::dvec3 collar { middlerope }; // przez nią przechodzi lina kotwienia przy teowniku + if( false == tobeam ) { + auto const index { suspension_named( Chain.file, data.middle ) }; + if( index < Suspensions.size() ) { + auto const &own { Suspensions[ index ] }; + auto const *scheme { catalog->scheme( own.data.card, own.data.execution ) }; + tee = scheme != nullptr && scheme->assembly.rfind( "tee_", 0 ) == 0; + // obejma 4160 na teowniku obok siodła liny nośnej, w stronę słupa (Jan 2026-10-26: lina kotwienia ma swoje mocowanie + // obok, nie w siodle liny); odstęp 0,15 m - ZAŁOŻENIE na oko + if( tee && own.has_saddle ) { + double const beside { 0.15 }; + auto placement { own.saddle }; + placement.position[ 1 ] += beside; + auto const scened { catenary::cantilever_to_scene( placement, own.saddle_heading ) }; + auto const entry { + "node " + text_number( data.range_max > 0.0 ? data.range_max : 1000.0, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " + own.saddle_model + ".t3d none" + + " angles " + text_number( scened.angles[ 0 ] ) + " " + text_number( scened.angles[ 1 ] ) + " " + text_number( scened.angles[ 2 ] ) + " endmodel" }; + glm::dvec3 const at { own.saddle_origin.x + scened.offset[ 0 ], own.saddle_origin.y + scened.offset[ 1 ], own.saddle_origin.z + scened.offset[ 2 ] }; + if( auto *model3d { simulation::State.create_model( entry, "none", at ) }; model3d != nullptr ) { + catenary_part_detach( model3d ); + Chain.middle_parts.push_back( model3d ); + } + // przesunięcie o tyle samo w stronę konstrukcji (oś Y wysięgnika w scenerii) + double const unit[ 3 ] { 0.0, 1.0, 0.0 }; + catenary::cantilever_placement axis; + std::copy( unit, unit + 3, axis.position ); + auto const toward { catenary::cantilever_to_scene( axis, own.saddle_heading ) }; + collar = middlerope + glm::dvec3 { toward.offset[ 0 ], toward.offset[ 1 ], toward.offset[ 2 ] } * beside; + } + } + } + for( int side = 0; side < 2; ++side ) { + auto const neighbour { side == 0 ? 0 : 2 }; + auto &solid { Chain.middle_solid[ side ] }; + // zacisk na linie nośnej 1,00 m od środkowego podwieszenia w stronę sąsiedniego (do dźwigara: ok. 3,00 m; teownik: w obejmie) + auto const towards { vec( ropes[ neighbour ] ) - middlerope }; + auto const clampdistance { tobeam ? 3.0 : tee ? 0.0 : 1.0 }; + auto const clampchord { middlerope + towards * ( clampdistance / std::max( std::max( clampdistance, 1e-3 ), glm::length( glm::dvec2 { towards.x, towards.z } ) ) ) }; + // połowa przęsła: przewód jezdny (prosto między końcami), lina nośna nad nim na wysokości najkrótszego wieszaka + auto const wiremiddle { ( vec( wires[ 1 ] ) + vec( wires[ neighbour ] ) ) * 0.5 }; + auto const alongspan { glm::normalize( glm::dvec3 { wires[ neighbour ][ 0 ] - wires[ 1 ][ 0 ], 0.0, wires[ neighbour ][ 2 ] - wires[ 1 ][ 2 ] } ) }; + // lina nośna w przęśle tak, jak ją rysuje przęsło: solver sieci z odstępami wieszaków z tablicy typu (t - ułamek + // przęsła od podwieszenia środkowego); bez typu - prosto między podwieszeniami + auto const spanlength { glm::length( glm::dvec2 { wires[ neighbour ][ 0 ] - wires[ 1 ][ 0 ], wires[ neighbour ][ 2 ] - wires[ 1 ][ 2 ] } ) }; + catenary::span_solution solution; + auto solved { false }; + if( type != nullptr ) { + // przęsło liczone dokładnie jak rysowane: w jego kierunku (od punktu k do k+1), z tym samym naciągiem (dopisek + // tension) i z linkami Y tylko tam, gdzie przęsło je dostaje (sąsiednie przęsło z wieszakami i tą samą długością Y) + auto const span { position + offset - 1 + side }; + auto definition { *type }; + double messengertension, contacttension; + if( span_tension( Chain, span, messengertension, contacttension ) ) { + if( messengertension > 0.0 ) { definition.type.messenger_tension = 10.0 * messengertension; } + if( contacttension > 0.0 && definition.type.contact_tension > 0.0 ) { definition.type.contact_tension = 10.0 * contacttension; } + } + auto const first { side == 0 ? neighbour : 1 }, second { side == 0 ? 1 : neighbour }; + catenary::span_points points; + points.contact_start = catenary::vec3 { wires[ first ][ 0 ], wires[ first ][ 1 ], wires[ first ][ 2 ] }; + points.contact_end = catenary::vec3 { wires[ second ][ 0 ], wires[ second ][ 1 ], wires[ second ][ 2 ] }; + points.messenger_start = catenary::vec3 { ropes[ first ][ 0 ], ropes[ first ][ 1 ], ropes[ first ][ 2 ] }; + points.messenger_end = catenary::vec3 { ropes[ second ][ 0 ], ropes[ second ][ 1 ], ropes[ second ][ 2 ] }; + std::string error; + auto const gaps { catenary::types().hanger_gaps( type->hanger_table, spanlength ) }; + solved = catenary::solve_span_points( definition, gaps, points, span_y_at( Chain, *type, span, 0 ), span_y_at( Chain, *type, span, 1 ), solution, error ) + && solution.result.messenger.size() >= 2; + } + // punkt liny nośnej jak w przęśle: w planie na cięciwie liny (nie nad osią przewodu jezdnego, który idzie zygzakiem), + // w pionie nad prostą przewodu jezdnego o wysokość z solvera + auto const messengerat = [ & ]( double const T ) { + auto const chord { vec( ropes[ 1 ] ) + ( vec( ropes[ neighbour ] ) - vec( ropes[ 1 ] ) ) * T }; + if( false == solved ) { return chord; } + auto const wirey { wires[ 1 ][ 1 ] + ( wires[ neighbour ][ 1 ] - wires[ 1 ][ 1 ] ) * T }; + auto const &line { solution.result.messenger }; + auto const x { ( side == 0 ? 1.0 - T : T ) * solution.length }; // przęsło przed środkowym liczone od sąsiada + auto above { line.back().y }; + for( std::size_t k = 1; k < line.size(); ++k ) { + if( line[ k ].x >= x ) { + auto const f { line[ k ].x > line[ k - 1 ].x ? ( x - line[ k - 1 ].x ) / ( line[ k ].x - line[ k - 1 ].x ) : 0.0 }; + above = line[ k - 1 ].y + ( line[ k ].y - line[ k - 1 ].y ) * f; + break; + } + } + return glm::dvec3 { chord.x, wirey + above, chord.z }; + }; + // zacisk na linie nośnej tam, gdzie ją rysuje przęsło (lina przy podwieszeniu opada, prosta między podwieszeniami + // mijała ją o kilka cm) + auto const clamp { tee ? collar : solved && spanlength > clampdistance ? messengerat( clampdistance / spanlength ) : clampchord }; + // koniec liny na słupie sąsiedniego podwieszenia: urządzenie kotwienia stałego tylko z liną + auto const index { suspension_named( Chain.file, data.suspensions[ position + ( side == 0 ? -1 : 1 ) ] ) }; + glm::dvec3 ropeend { tobeam ? beam : vec( ropes[ neighbour ] ) }; + if( false == tobeam && index < Suspensions.size() && Suspensions[ index ].data.has_pole ) { + auto const &suspension { Suspensions[ index ].data }; + auto const *pole { catalog->pole( suspension.pole_type ) }; + auto const name { data.middle_device + ( side == 0 ? "" : "-2" ) }; + auto const *device { devices->device( name ) }; + if( device == nullptr ) { device = devices->device( data.middle_device ); } + if( pole != nullptr && device != nullptr ) { + glm::dvec3 const axis { suspension.pole[ 0 ], suspension.pole[ 1 ], suspension.pole[ 2 ] }; + // oś słupa wzdłuż toru, prostopadle do jego wysięgnika, w stronę środkowego słupa + glm::dvec2 cantilever { suspension.has_at ? glm::dvec2 { suspension.at[ 0 ] - axis.x, suspension.at[ 2 ] - axis.z } : glm::dvec2 { 0.0, 0.0 } }; + glm::dvec2 const tomiddle { clamp.x - axis.x, clamp.z - axis.z }; + glm::dvec2 structure { glm::length( cantilever ) > 0.1 ? glm::dvec2 { -cantilever.y, cantilever.x } / glm::length( cantilever ) : glm::normalize( tomiddle ) }; + if( glm::dot( structure, tomiddle ) < 0.0 ) { structure = -structure; } + auto const face { pole->width > 0.0 ? 0.5 * pole->width : 0.5 * pole->depth( ropes[ neighbour ][ 1 ] - axis.y ) }; + glm::dvec3 const origin { axis.x + structure.x * face, axis.y, axis.z + structure.y * face }; + auto const way { glm::normalize( glm::dvec2 { clamp.x - origin.x, clamp.z - origin.z } ) }; + auto const heading { std::atan2( way.y, -way.x ) }; + 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 ] }; + }; + double const zero[ 3 ] { 0.0, 0.0, 0.0 }; + auto const base { toscene( zero ) }; + glm::dvec3 axes[ 3 ]; + for( int a = 0; a < 3; ++a ) { + double unit[ 3 ] { 0.0, 0.0, 0.0 }; + unit[ a ] = 1.0; + axes[ a ] = toscene( unit ) - base; + } + auto const todevice = [ & ]( glm::dvec3 const &Point, double *Out ) { + auto const d { Point - base }; + for( int a = 0; a < 3; ++a ) { Out[ a ] = glm::dot( d, axes[ a ] ); } + }; + catenary::tensioner_input input; + if( type != nullptr ) { + input.type = devices->code_for( type->type.symbol, type->type.code ); + input.contacts = type->type.contact_count; + input.messengers = type->type.messenger_count; + } + if( device->type( input.type ) == nullptr && false == device->types.empty() ) { input.type = device->types.front().code; } + input.pole = suspension.pole_type; + input.depth = [ pole ]( double const Height ) { return pole->width > 0.0 ? pole->width : pole->depth( Height ); }; + input.structure_angle = std::atan2( glm::dot( glm::dvec3 { structure.x, 0.0, structure.y }, axes[ 1 ] ), glm::dot( glm::dvec3 { structure.x, 0.0, structure.y }, axes[ 0 ] ) ); + input.aimed = true; + todevice( clamp, input.messenger_target ); + todevice( vec( wires[ neighbour ] ), input.contact_target ); + input.rope_height = data.middle_heights[ side ] > 0.0 ? data.middle_heights[ side ] : ropes[ neighbour ][ 1 ] - axis.y; + input.height = wires[ neighbour ][ 1 ] - axis.y; + catenary::tensioner_result result; + if( catenary::solve_tensioner( *devices, *device, input, result ) ) { + ropeend = toscene( result.messenger_end ); + auto const skin { std::string( "none" ) }; + 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 ) }; + auto const scened { catenary::cantilever_to_scene( placement, heading ) }; + auto entry { + "node " + text_number( data.range_max > 0.0 ? data.range_max : 1000.0, 1 ) + " " + text_number( data.range_min, 1 ) + " none model 0 0 0 0 " + model + ".t3d " + skin + + " angles " + text_number( scened.angles[ 0 ] ) + " " + text_number( scened.angles[ 1 ] ) + " " + text_number( scened.angles[ 2 ] ) + " endmodel" }; + glm::dvec3 const at { origin.x + scened.offset[ 0 ], origin.y + scened.offset[ 1 ], origin.z + scened.offset[ 2 ] }; + if( auto *model3d { simulation::State.create_model( entry, "none", at ) }; model3d != nullptr ) { + catenary_part_detach( model3d ); + Chain.middle_parts.push_back( model3d ); + } + } + } + else { + for( auto const &warning : result.warnings ) { ErrorLog( label + "middle anchor can't be assembled: " + warning ); } + } + } + } + // lina kotwienia: od końca na sąsiednim słupie do zacisku przy słupie środkowym, zwisa jak lina (parabola, 0,5 m + // na 60 m - ZAŁOŻENIE), nad liną nośną + auto const hanging = [ & ]( glm::dvec3 const &From, glm::dvec3 const &To, double const Sag, int const Steps ) { + std::vector points; + for( int k = 0; k <= Steps; ++k ) { + auto const t { static_cast( k ) / Steps }; + auto point { From + ( To - From ) * t }; + point.y -= 4.0 * Sag * t * ( 1.0 - t ); + points.push_back( point ); + } + return points; + }; + auto const flat = []( glm::dvec3 const &A, glm::dvec3 const &B ) { return glm::length( glm::dvec2 { A.x - B.x, A.z - B.z } ); }; + auto rope { hanging( ropeend, clamp, 0.5 * flat( ropeend, clamp ) / 60.0, 24 ) }; + if( tobeam ) { + // od dźwigara: uchwyt 4590 i izolator 7150 (bryły zastępcze - wymiary izolatora jak model 7150-01, uchwyt ZAŁOŻENIE) + auto const down { clamp - beam }; + auto const along { glm::length( down ) > 1e-6 ? glm::normalize( down ) : glm::dvec3 { 0.0, -1.0, 0.0 } }; + add_box( solid, beam, towards, 0.25, 0.08 ); + add_box( solid, beam + along * ( 0.15 + 0.4162 ), along, 0.8324, 0.06 ); + } + // między zaciskami przy słupie środkowym lina zwisa swobodnie pod uchwytem liny nośnej (Jan 2026-10-25): do połowy + // odległości (słup środkowy) 0,25 m poniżej liny nośnej + if( tee ) { + // obejma na wysięgniku teowym (model 4160 obok siodła; bez siodła w złożeniu - bryła zastępcza nad liną, raz) + if( side == 0 && glm::length( collar - middlerope ) < 1e-6 ) { add_box( solid, middlerope + glm::dvec3 { 0.0, 0.03, 0.0 }, towards, 0.12, 0.06 ); } + } + else if( false == tobeam ) { + auto const below { hanging( clamp, middlerope, 0.0, 6 ) }; + for( std::size_t k = 1; k < below.size(); ++k ) { + auto point { below[ k ] }; + auto const t { static_cast( k ) / ( below.size() - 1 ) }; + point.y -= 0.25 * std::sin( 0.5 * 3.14159265358979323846 * t ); + rope.push_back( point ); + } + } + add_tube( solid, rope, ropeheight ); + // V (złączka 6410-5 na przewodzie w połowie przęsła, ramiona do liny nośnej 1,50 m w obie strony wzdłuż przęsła). + // Linka jest sztywna: z zacisku wychodzi chwilę wzdłuż liny nośnej, potem skośnie w dół i dochodzi do złączki wzdłuż + // przewodu (Jan 2026-10-25, zdjęcie) - krzywa trzeciego stopnia ze styczną do liny i do przewodu, wektory kontrolne + // po 0,6 odległości w poziomie (ZAŁOŻENIE dobrane na oko do zdjęcia) + auto const reach { spanlength > 4.0 ? 1.5 / spanlength : 0.0 }; + auto const a { messengerat( 0.5 - reach ) }, b { messengerat( 0.5 + reach ) }; + auto const unit = []( glm::dvec3 const &V ) { return glm::length( V ) > 1e-9 ? glm::normalize( V ) : glm::dvec3 { 1.0, 0.0, 0.0 }; }; + auto const wiredirection { unit( vec( wires[ neighbour ] ) - vec( wires[ 1 ] ) ) }; + auto const step { spanlength > 4.0 ? 0.1 / spanlength : 0.0 }; + auto const stiff = [ & ]( glm::dvec3 const &From, glm::dvec3 const &Fromdirection, glm::dvec3 const &To, glm::dvec3 const &Todirection ) { + auto const handle { 0.6 * flat( From, To ) }; + auto const p1 { From + Fromdirection * handle }, p2 { To - Todirection * handle }; + std::vector points; + int const steps { 16 }; + for( int k = 0; k <= steps; ++k ) { + auto const t { static_cast( k ) / steps }, u { 1.0 - t }; + points.push_back( From * ( u * u * u ) + p1 * ( 3.0 * u * u * t ) + p2 * ( 3.0 * u * t * t ) + To * ( t * t * t ) ); + } + return points; + }; + add_tube( solid, stiff( a, unit( messengerat( 0.5 - reach + step ) - a ), wiremiddle, wiredirection ), ropeheight ); + add_tube( solid, stiff( wiremiddle, wiredirection, b, unit( b - messengerat( 0.5 + reach - step ) ) ), ropeheight ); + // bryły zastępcze: zaciski 5560/5570 (na linie nośnej przy słupie środkowym i na końcach V), złączka 6410 na przewodzie + if( false == tee ) { add_box( solid, clamp, towards, 0.10, 0.045 ); } + add_box( solid, a, alongspan, 0.08, 0.04 ); + add_box( solid, b, alongspan, 0.08, 0.04 ); + add_box( solid, wiremiddle, alongspan, 0.10, 0.05 ); + } +} + // 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 ) { @@ -844,11 +1601,34 @@ rebuild_span( scene_chain &Chain, std::size_t const Span ) { 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 ); + // przęsło do kotwienia rysuje z bliska jego linę (bryła proceduralna urządzenia naprężającego) + if( auto *span { Chain.spans[ Span ] }; span != nullptr ) { + std::vector rope; + auto const ends { chain_points( Chain ) }; + for( auto const *name : { Span == 0 ? &Chain.data.anchor_start : nullptr, Span + 2 == ends ? &Chain.data.anchor_end : nullptr, + Span == 0 ? &Chain.data.anchor_start_contact : nullptr, Span + 2 == ends ? &Chain.data.anchor_end_contact : nullptr } ) { + if( name == nullptr || name->empty() ) { continue; } + if( auto const *anchor { anchor_named( Chain.file, *name ) }; anchor != nullptr && anchor->solved ) { + rope.insert( rope.end(), anchor->rope.begin(), anchor->rope.end() ); + } + } + // kotwienie środkowe: lina, V i zaciski na przęsłach przed i za podwieszeniem środkowym + if( Span == Chain.middle_span || Span == Chain.middle_span + 1 ) { + auto const &solid { Chain.middle_solid[ Span == Chain.middle_span ? 0 : 1 ] }; + rope.insert( rope.end(), solid.begin(), solid.end() ); + } + if( false == rope.empty() ) { catenary_span_attach( *span, rope ); } + } } // wszystkie przęsła łańcucha od nowa (zmiana typu, materiału, listy podwieszeń) void rebuild_chain( scene_chain &Chain ) { + // kotwienia łańcucha od nowa (stan drutów, kierunek do pierwszego/ostatniego podwieszenia), przed przęsłami do nich + for( auto const *name : { &Chain.data.anchor_start, &Chain.data.anchor_end, &Chain.data.anchor_start_contact, &Chain.data.anchor_end_contact } ) { + if( auto *anchor { anchor_named( Chain.file, *name ) }; anchor != nullptr ) { assemble_anchor( *anchor ); } + } + assemble_middle( 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 ); } @@ -1013,6 +1793,21 @@ catenary_scene_resolve() { if( catalog != nullptr ) { for( auto const &umbrella : umbrellas ) { assemble_umbrella( umbrella.first, umbrella.second, *catalog, assembled, parts ); } } + // kotwienia środkowe przy teownikach od nowa: obejma liny kotwienia staje obok siodła liny nośnej, a wysięgniki złożyły + // się dopiero teraz (łańcuchy powstały przy wczytaniu) + auto const anymiddle { std::any_of( Chains.begin(), Chains.end(), []( scene_chain const &Chain ) { return false == Chain.removed && false == Chain.data.middle.empty(); } ) }; + if( auto const *cantilevercatalog { anymiddle ? cantilevers() : nullptr }; cantilevercatalog != nullptr ) { + for( auto &chain : Chains ) { + if( chain.removed || chain.data.middle.empty() ) { continue; } + auto const index { suspension_named( chain.file, chain.data.middle ) }; + if( index >= Suspensions.size() || false == Suspensions[ index ].has_saddle ) { continue; } + auto const *scheme { cantilevercatalog->scheme( Suspensions[ index ].data.card, Suspensions[ index ].data.execution ) }; + if( scheme == nullptr || scheme->assembly.rfind( "tee_", 0 ) != 0 ) { continue; } + assemble_middle( chain ); + if( chain.middle_span == static_cast( -1 ) ) { continue; } + for( auto const span : { chain.middle_span, chain.middle_span + 1 } ) { rebuild_span( chain, span ); } + } + } 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() ); @@ -1108,25 +1903,65 @@ reassemble( std::size_t const Index ) { std::size_t assembled { 0 }, parts { 0 }; if( false == suspension.data.umbrella.empty() ) { auto const members { umbrella_members( suspension.data.umbrella ) }; + // w edytorze parasol na chwilę ma jeden wysięgnik (drugi dopiero dochodzi albo właśnie odszedł): bez komunikatu + if( members.size() == 1 ) { + drop_parts( suspension ); + assemble_alone( Index, *catalog ); + return; + } for( auto const index : members ) { drop_parts( Suspensions[ index ] ); } - assemble_umbrella( suspension.data.umbrella, members, *catalog, assembled, parts ); + assemble_umbrella( suspension.data.umbrella, members, *catalog, assembled, parts, false ); return; } drop_parts( suspension ); assemble_alone( Index, *catalog ); } -// przęsła łańcuchów przy podwieszeniu idą za nim +// przęsła łańcuchów przy podwieszeniu idą za nim, kotwienia na jego słupie też void follow_chains( std::size_t const Index ) { auto const &suspension { Suspensions[ Index ] }; if( suspension.data.name.empty() ) { return; } + for( auto &anchor : Anchors ) { + if( anchor.removed || anchor.file != suspension.file || anchor.data.on != suspension.data.name ) { continue; } + assemble_anchor( anchor ); + follow_anchor( anchor ); + } + // trzyma linę w miejscu innych podwieszeń: ich linki celują w nią + for( auto const &chain : Chains ) { + if( chain.removed || chain.file != suspension.file ) { continue; } + for( auto const &pair : chain.data.messenger_from ) { + if( pair.second != suspension.data.name || pair.first == pair.second ) { continue; } + if( auto const index { suspension_named( chain.file, pair.first ) }; index < Suspensions.size() && index != Index ) { reassemble( index ); } + } + } 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; } + if( names[ position ] != suspension.data.name && catenary::messenger_holder( chain.data, names[ position ] ) != suspension.data.name ) { continue; } + // skrajne podwieszenie: kotwienie celuje w nie łańcuchami do drutów + std::string const *end { position == 0 ? &chain.data.anchor_start : position + 1 == names.size() ? &chain.data.anchor_end : nullptr }; + if( end != nullptr && false == end->empty() ) { + if( auto *anchor { anchor_named( chain.file, *end ) }; anchor != nullptr ) { assemble_anchor( *anchor ); } + } + // przewody zakotwione osobno + std::string const *endcontact { position == 0 ? &chain.data.anchor_start_contact : position + 1 == names.size() ? &chain.data.anchor_end_contact : nullptr }; + if( endcontact != nullptr && false == endcontact->empty() ) { + if( auto *anchor { anchor_named( chain.file, *endcontact ) }; anchor != nullptr ) { assemble_anchor( *anchor ); } + } auto const point { position + ( chain.data.anchor_start.empty() ? 0 : 1 ) }; + // kotwienie środkowe przy tym podwieszeniu albo przy sąsiednim: od nowa, z oboma przęsłami przy nim + if( false == chain.data.middle.empty() ) { + auto const middle { std::find( names.begin(), names.end(), chain.data.middle ) }; + auto const at { static_cast( middle - names.begin() ) }; + if( middle != names.end() && position + 1 >= at && position <= at + 1 ) { + assemble_middle( chain ); + for( auto const span : { chain.middle_span, chain.middle_span + 1 } ) { + if( span != point && span + 1 != point ) { rebuild_span( chain, span ); } + } + } + } if( point > 0 ) { rebuild_span( chain, point - 1 ); } rebuild_span( chain, point ); } @@ -1203,7 +2038,11 @@ catenary_scene_remove( std::size_t const Index ) { 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; } + // lina nośna trzymana przez nie albo w jego punkcie: wraca do wysięgnika z listy + auto &pairs { chain.data.messenger_from }; + auto const pairsbefore { pairs.size() }; + pairs.erase( std::remove_if( pairs.begin(), pairs.end(), [ & ]( auto const &Pair ) { return Pair.first == suspension.data.name || Pair.second == suspension.data.name; } ), pairs.end() ); + if( names.size() == before && pairs.size() == pairsbefore ) { continue; } if( names.size() < 2 ) { chain.removed = true; } rebuild_chain( chain ); } @@ -1367,8 +2206,16 @@ bool catenary_chain_update( std::size_t const Index, catenary::chain const &Data ) { if( Index >= Chains.size() ) { return false; } auto &chain { Chains[ Index ] }; + // podwieszenia, których linę trzyma (trzymał) inny wysięgnik: ich linki celują w linę, więc składane od nowa + std::set held; + for( auto const *pairs : { &static_cast( chain.data ).messenger_from, &Data.messenger_from } ) { + for( auto const &pair : *pairs ) { if( pair.first != pair.second ) { held.insert( pair.first ); } } + } chain.data = Data; chain.removed = Data.suspensions.size() < 2; + for( auto const &name : held ) { + if( auto const index { suspension_named( chain.file, name ) }; index < Suspensions.size() ) { reassemble( index ); } + } rebuild_chain( chain ); mark_changed( chain.file ); return true; @@ -1382,6 +2229,16 @@ catenary_chain_remove( std::size_t const Index ) { mark_changed( Chains[ Index ].file ); } +bool +catenary_chain_middle( std::size_t const Index, std::size_t &Parts, std::size_t &Triangles ) { + Parts = Triangles = 0; + if( Index >= Chains.size() || Chains[ Index ].removed || Chains[ Index ].data.middle.empty() ) { return false; } + auto const &chain { Chains[ Index ] }; + Parts = chain.middle_parts.size(); + Triangles = ( chain.middle_solid[ 0 ].size() + chain.middle_solid[ 1 ].size() ) / 3; + return chain.middle_span != static_cast( -1 ); +} + int catenary_chain_of( std::size_t const Suspension, std::size_t &Position ) { if( Suspension >= Suspensions.size() || Suspensions[ Suspension ].data.name.empty() ) { return -1; } @@ -1502,13 +2359,14 @@ catenary_scene_new_gantry_name( std::string const &File ) { namespace { -// przęsła do kotwienia (pierwsze albo ostatnie łańcucha) idą za nim +// przęsła łańcucha z kotwieniem idą za nim: przęsło do kotwienia (położenie końców), a pozostałe przez naciąg +// (brakujące ciężary zmieniają zwis wszystkich przęseł do kotwienia środkowego) 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 ); } + if( false == anchors_chain( chain.data, Anchor.data.name ) ) { continue; } + for( std::size_t span = 0; span < chain.spans.size(); ++span ) { rebuild_span( chain, span ); } } } @@ -1541,6 +2399,49 @@ catenary_anchor_result( std::size_t const Index ) { return &Anchors[ Index ].result; } +std::vector +catenary_anchor_devices( std::string const &Only ) { + std::vector result; + // urządzenia kotwienia środkowego (lina kotwienia środkowego) nie są kotwieniem końca łańcucha + static std::set const middle { "0406", "0406-2", "0408", "0408-2", "0418", "0418-2" }; + if( auto const *devices { tensioners() }; devices != nullptr ) { + for( auto const &device : devices->devices() ) { + if( device.only != Only || middle.count( device.name ) ) { continue; } + // odmiany dla typów sieci (-10, -32, ...) dobiera się samo; połówki rozdzielonego (-m, -c) i nazwane - na liście + auto const dash { device.name.find( '-' ) }; + auto const half { dash != std::string::npos && ( device.name.substr( dash ) == "-m" || device.name.substr( dash ) == "-c" ) }; + if( dash == std::string::npos || false == device.title.empty() || ( false == Only.empty() && half ) ) { + result.push_back( { device.name, device.fixed, false == device.small_pulley.empty() || device.split, device.split, device.title } ); + } + } + } + return result; +} + +catenary::tensioner_catalog const * +catenary_scene_tensioners() { + return tensioners(); +} + +bool +catenary_anchor_shape( std::size_t const Index, double ( &Shape )[ 4 ][ 3 ] ) { + if( Index >= Anchors.size() || Anchors[ Index ].removed || false == Anchors[ Index ].solved ) { return false; } + for( int k = 0; k < 4; ++k ) { std::copy( std::begin( Anchors[ Index ].shape[ k ] ), std::end( Anchors[ Index ].shape[ k ] ), Shape[ k ] ); } + return true; +} + +int +catenary_anchor_chain( std::size_t const Index, bool &Start ) { + if( Index >= Anchors.size() || Anchors[ Index ].removed ) { return -1; } + auto const &anchor { Anchors[ Index ] }; + for( std::size_t k = 0; k < Chains.size(); ++k ) { + auto const &chain { Chains[ k ] }; + if( chain.removed || chain.file != anchor.file || anchor.data.name.empty() ) { continue; } + if( anchors_chain( chain.data, anchor.data.name, &Start ) ) { return static_cast( k ); } + } + return -1; +} + std::size_t catenary_anchor_add( std::string const &File, catenary::anchor const &Data ) { if( file_named( File ) == nullptr ) { diff --git a/world/catenary_scene.h b/world/catenary_scene.h index cde4e2e5..35b7397f 100644 --- a/world/catenary_scene.h +++ b/world/catenary_scene.h @@ -30,6 +30,7 @@ void catenary_scene_data( std::string const &Directory ); // Zmiany trafiają do plików sieci dopiero przy catenary_scene_save(). #include +#include #include "world/catenary_suspension.h" #include "world/catenary_cantilever.h" #include "world/catenary_layout.h" @@ -87,6 +88,9 @@ std::size_t catenary_chain_add( std::string const &File, catenary::chain const & // 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 ); +// kotwienie środkowe łańcucha: liczba wstawionych części (na słupach sąsiednich podwieszeń) i trójkątów brył (lina, V, zaciski); +// false, gdy łańcuch go nie ma albo się nie złożyło +bool catenary_chain_middle( std::size_t Index, std::size_t &Parts, std::size_t &Triangles ); // ł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 ); @@ -97,6 +101,23 @@ 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 ); +// obrys złożonego urządzenia dla edytora (współrzędne scenerii): czoło słupa i oś rolki ruchomej na wysokości rolek, góra i spód +// kolumny ciężarów; false gdy usunięte albo niezłożone +bool catenary_anchor_shape( std::size_t Index, double ( &Shape )[ 4 ][ 3 ] ); +// karty urządzeń kotwienia do wyboru (bez odmian dla typów sieci, np. 0461-2x2): nazwa, kotwienie stałe, rolki szeregowe +struct catenary_anchor_card { + std::string card; + bool fixed { false }; + bool series { false }; + bool split { false }; // dwa stosy (lina i przewody osobno) + std::string title; // opis z danych (odmiany pokazane osobno, np. 0461-2x2) +}; +// Only: puste - kotwienia całości; "messenger" / "contact" - samej liny / samych przewodów (przewody zakotwione osobno) +std::vector catenary_anchor_devices( std::string const &Only = {} ); +// katalog urządzeń naprężających (traction_tensioners.txt); nullptr, gdy go nie ma +catenary::tensioner_catalog const *catenary_scene_tensioners(); +// łańcuch, który kończy się tym kotwieniem (Start: na początku), -1 gdy żaden +int catenary_anchor_chain( std::size_t Index, bool &Start ); // 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ą diff --git a/world/catenary_span.cpp b/world/catenary_span.cpp index 01de759d..b012c257 100644 --- a/world/catenary_span.cpp +++ b/world/catenary_span.cpp @@ -10,6 +10,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/catenary_span.h" +#include + #include "world/Traction.h" #include "world/catenary_network.h" #include "utilities/Globals.h" @@ -31,6 +33,10 @@ struct catenary_span { 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 }; + std::vector attached; // bryła dołączona (lina kotwienia), układ scenerii + std::shared_ptr own; // kopia typu z naciągiem z dopisku "tension" + bool rope2set[ 2 ] { false, false }; // końce drugiej liny nośnej z dopisku "rope2" + catenary::vec3 rope2[ 2 ]; }; namespace { @@ -43,11 +49,19 @@ definition( TTraction const &Traction ) { catenary::span_points points( TTraction const &Traction ) { - return { + catenary::span_points result { { 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 } }; + if( Traction.m_catenaryspan ) { + auto const &span { *Traction.m_catenaryspan }; + result.messenger2_start_set = span.rope2set[ 0 ]; + result.messenger2_end_set = span.rope2set[ 1 ]; + result.messenger2_start = span.rope2[ 0 ]; + result.messenger2_end = span.rope2[ 1 ]; + } + return result; } // długość podwieszenia Y, jaką typ sieci przewiduje dla tego przęsła; 0 gdy brak @@ -249,13 +263,17 @@ catenary_span_metal( TTraction const &Traction, bool const Fitting ) { // przypisuje przęsłu typ sieci z dopisku "type" i dobiera rozstaw wieszaków void -catenary_span_assign( TTraction &Traction, std::string const &Symbol ) { +catenary_span_assign( TTraction &Traction, std::string const &Symbol, double const Messenger, double const Contact, glm::dvec3 const *Rope2, bool const *Rope2set ) { Traction.m_catenaryspan.reset(); if( Symbol.empty() ) { return; } auto span { std::make_shared() }; span->symbol = Symbol; + for( int end = 0; end < 2 && Rope2 != nullptr && Rope2set != nullptr; ++end ) { + span->rope2set[ end ] = Rope2set[ end ]; + span->rope2[ end ] = catenary::vec3 { Rope2[ end ].x, Rope2[ end ].y, Rope2[ end ].z }; + } Traction.m_catenaryspan = span; if( false == Global.CatenaryProcedural ) { return; } @@ -265,6 +283,13 @@ catenary_span_assign( TTraction &Traction, std::string const &Symbol ) { } span->definition = registry.find( Symbol ); if( span->definition == nullptr ) { return; } // brak typu został już zgłoszony, przęsło rysuje się po staremu + // naciąg z dopisku "tension" (daN): przęsło liczone z kopią typu + if( Messenger > 0.0 || Contact > 0.0 ) { + span->own = std::make_shared( *span->definition ); + if( Messenger > 0.0 ) { span->own->type.messenger_tension = 10.0 * Messenger; } + if( Contact > 0.0 && span->own->type.contact_tension > 0.0 ) { span->own->type.contact_tension = 10.0 * Contact; } + span->definition = span->own.get(); + } // 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; } @@ -274,6 +299,13 @@ catenary_span_assign( TTraction &Traction, std::string const &Symbol ) { std::hypot( Traction.pPoint2.x - Traction.pPoint1.x, Traction.pPoint2.z - Traction.pPoint1.z ) ); } +// wygląd liny stalowej (ocynk, lekko zmatowiały) +catenary_metal const & +catenary_rope_metal() { + static catenary_metal const steel { { 0.56f, 0.57f, 0.58f }, 0.8f, 0.30f, 0.20f }; + return steel; +} + // symbol typu sieci przypisany przęsłu; pusty gdy brak std::string catenary_span_symbol( TTraction const &Traction ) { @@ -318,6 +350,7 @@ catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camera ) { // kamera daleko: zwolnienie pamięci, linie wystarczą release( span.solid.wires ); release( span.solid.fittings ); + release( span.solid.rope ); span.solid = {}; span.solidloaded = false; return {}; @@ -344,6 +377,10 @@ catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camera ) { auto data { vertices( Traction, fittings ) }; span.solid.fittings = GfxRenderer->Insert( data, userdata, span.solidbank, GL_TRIANGLES ); } + if( false == span.attached.empty() ) { + auto data { vertices( Traction, span.attached ) }; + span.solid.rope = GfxRenderer->Insert( data, userdata, span.solidbank, GL_TRIANGLES ); + } span.solidloaded = ( span.solid.wires != null_handle ); return ( span.solidloaded ? span.solid : catenary_solid() ); } @@ -358,6 +395,7 @@ drop_solid( TTraction &Traction ) { if( false == span.solidloaded ) { return; } release( span.solid.wires ); release( span.solid.fittings ); + release( span.solid.rope ); span.solid = {}; span.solidloaded = false; } @@ -383,3 +421,12 @@ catenary_span_release( TTraction &Traction ) { drop_solid( Traction ); release( Traction.m_geometry ); } + +// bryła spoza przęsła rysowana razem z nim (lina kotwienia); zrobi się przy następnym podejściu kamery +bool +catenary_span_attach( TTraction &Traction, std::vector const &Triangles ) { + if( false == static_cast( Traction.m_catenaryspan ) ) { return false; } + drop_solid( Traction ); + Traction.m_catenaryspan->attached = Triangles; + return ( Traction.m_catenaryspan->definition != nullptr ); +} diff --git a/world/catenary_span.h b/world/catenary_span.h index 1a18931e..0e477243 100644 --- a/world/catenary_span.h +++ b/world/catenary_span.h @@ -14,10 +14,12 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include "rendering/geometrybank.h" class TTraction; +namespace catenary { struct solid_vertex; } // ustawienia rysowania sieci, zmieniane z eu07.ini i z panelu ustawień struct catenary_settings { @@ -27,7 +29,11 @@ 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 ); +// Messenger, Contact: naciąg liny nośnej i przewodów jezdnych w daN zamiast naciągu typu (dopisek "tension"); 0 = jak w typie +// Rope2: końce drugiej liny nośnej podane w wpisie ("rope2 start|end x y z", kotwienie z dwiema liniami): [0] start, [1] koniec, +// Rope2set - które podane +void catenary_span_assign( TTraction &Traction, std::string const &Symbol, double Messenger = 0.0, double Contact = 0.0, + glm::dvec3 const *Rope2 = nullptr, bool const *Rope2set = nullptr ); // 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 @@ -37,6 +43,7 @@ bool catenary_span_lines( TTraction &Traction, gfx::vertex_array &Vertices ); struct catenary_solid { gfx::geometry_handle wires; gfx::geometry_handle fittings; + gfx::geometry_handle rope; // bryła dołączona z zewnątrz (lina urządzenia naprężającego na końcu przęsła), stal }; // 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 ); @@ -44,6 +51,9 @@ catenary_solid catenary_span_solid( TTraction &Traction, glm::dvec3 const &Camer void catenary_span_refresh( TTraction &Traction ); // zwalnia linie i bryły przęsła przed jego usunięciem void catenary_span_release( TTraction &Traction ); +// dołącza do brył przęsła bryłę spoza przęsła (lina kotwienia przy jego końcu; trójkąty w układzie scenerii), rysowaną +// razem z nim z bliska, ze stali. Pusta lista odłącza. false, gdy przęsło nie jest rysowane wg typu sieci +bool catenary_span_attach( TTraction &Traction, std::vector const &Triangles ); // 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 { @@ -55,3 +65,5 @@ struct catenary_metal { // 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 ); +// wygląd liny stalowej (cięgło 4490 urządzenia naprężającego) +catenary_metal const &catenary_rope_metal(); diff --git a/world/catenary_suspension.cpp b/world/catenary_suspension.cpp index b778bdf8..b227b679 100644 --- a/world/catenary_suspension.cpp +++ b/world/catenary_suspension.cpp @@ -33,7 +33,7 @@ std::string normalize_path( std::string 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" }; + static std::set const keywords { "support", "scheme", "messenger", "contact", "insulators", "override", "skin", "at", "attach", "rope", "pole", "umbrella", "spread", "shared_pole", "endsuspension" }; return keywords.find( Token ) != keywords.end(); } @@ -101,6 +101,15 @@ void parse_suspensions( std::vector const &Tokens, std::string cons 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 == "anchor_start_contact" ) { one( line.anchor_start_contact ); } + else if( key == "anchor_end_contact" ) { one( line.anchor_end_contact ); } + else if( key == "middle" ) { one( line.middle ); } + else if( key == "middle_device" ) { one( line.middle_device ); } + else if( key == "middle_height" ) { number( line.middle_heights[ 0 ] ); number( line.middle_heights[ 1 ] ); } + else if( key == "messenger_from" ) { + std::string listed, holder; + if( one( listed ) && one( holder ) ) { line.messenger_from.emplace_back( listed, holder ); } + } 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++ ] ); } @@ -177,12 +186,21 @@ void parse_suspensions( std::vector const &Tokens, std::string cons 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 == "on" ) { line.on = one(); } 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 == "height" ) { number( line.height ); } + else if( key == "rope_height" ) { number( line.rope_height ); } + else if( key == "insulators" ) { + line.insulators = one(); + if( line.insulators != "new" && line.insulators != "old" && line.insulators != "messenger" && line.insulators != "contact" ) { + report( line.name, "\"insulators\" takes new, old, messenger or contact" ); good = false; + } + } else if( key == "skin" ) { line.skin = one(); } else { report( line.name, "unexpected token \"" + key + "\" in an anchor" ); good = false; } } @@ -250,6 +268,14 @@ void parse_suspensions( std::vector const &Tokens, std::string cons } else { report( entry.name, "\"" + key + "\" needs three numbers: a point in scenery coordinates" ); good = false; } } + else if( key == "shared_pole" ) { + if( list.empty() ) { entry.shared_pole = true; } + else { report( entry.name, "\"shared_pole\" takes no values" ); good = false; } + } + else if( key == "spread" ) { + if( list.size() == 3 && to_number( list[ 0 ], entry.spread[ 0 ] ) && to_number( list[ 1 ], entry.spread[ 1 ] ) && to_number( list[ 2 ], entry.spread[ 2 ] ) ) { entry.has_spread = true; } + else { report( entry.name, "\"spread\" needs three numbers: the point on the console in the plane of the cantilever" ); 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; } @@ -270,6 +296,8 @@ void parse_suspensions( std::vector const &Tokens, std::string cons 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; } + if( entry.shared_pole && false == entry.has_pole ) { report( entry.name, "\"shared_pole\" needs a \"pole\"" ); good = false; } + if( entry.has_spread && false == entry.has_pole ) { report( entry.name, "\"spread\" needs a \"pole\"" ); good = false; } } else { if( entry.support.empty() ) { report( entry.name, "missing \"support\"" ); good = false; } @@ -318,9 +346,18 @@ std::string write_suspension( suspension const &Entry ) { 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; } + if( Entry.has_spread ) { text += " spread " + point( Entry.spread ); } + if( Entry.shared_pole ) { text += " shared_pole"; } return text + " endsuspension"; } +std::string const &messenger_holder( chain const &Chain, std::string const &Listed ) { + for( auto const &pair : Chain.messenger_from ) { + if( pair.first == Listed && false == pair.second.empty() ) { return pair.second; } + } + return Listed; +} + std::string write_chain( chain const &Entry ) { auto const number = []( double const Value ) { @@ -337,6 +374,13 @@ std::string write_chain( chain const &Entry ) { 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; } + if( false == Entry.anchor_start_contact.empty() ) { text += " anchor_start_contact " + Entry.anchor_start_contact; } + if( false == Entry.anchor_end_contact.empty() ) { text += " anchor_end_contact " + Entry.anchor_end_contact; } + if( false == Entry.middle.empty() ) { + text += " middle " + Entry.middle + ( Entry.middle_device != "0406" ? " middle_device " + Entry.middle_device : std::string() ); + if( Entry.middle_heights[ 0 ] > 0.0 || Entry.middle_heights[ 1 ] > 0.0 ) { text += " middle_height " + number( Entry.middle_heights[ 0 ] ) + " " + number( Entry.middle_heights[ 1 ] ); } + } + for( auto const &pair : Entry.messenger_from ) { text += " messenger_from " + pair.first + " " + pair.second; } text += " suspensions"; for( auto const &name : Entry.suspensions ) { text += " " + name; } return text + " endchain"; @@ -378,12 +422,17 @@ std::string write_anchor( anchor const &Entry ) { 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 ); + text += " pole " + Entry.pole_type + " " + point( Entry.pole ); + if( false == Entry.on.empty() ) { text += " on " + Entry.on; } + text += " 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( Entry.height > 0.0 ) { text += " height " + number( Entry.height, 3 ); } + if( Entry.rope_height > 0.0 ) { text += " rope_height " + number( Entry.rope_height, 3 ); } + if( false == Entry.insulators.empty() ) { text += " insulators " + Entry.insulators; } if( false == Entry.skin.empty() ) { text += " skin " + Entry.skin; } return text + " endanchor"; } diff --git a/world/catenary_suspension.h b/world/catenary_suspension.h index 55686480..88707dcd 100644 --- a/world/catenary_suspension.h +++ b/world/catenary_suspension.h @@ -38,6 +38,12 @@ struct suspension { // 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; + // wysięgnik rozstawiony (konsola 4890; para wpisów z tą samą nazwą "umbrella" po tej samej stronie słupa albo jeden): punkt na osi + // płaszczyzny wysięgnika przy słupie (oś słupa przesunięta wzdłuż toru do punktu mocowania na konsoli), "spread x y z" + // słup stawia inny wpis w tym samym miejscu (trzeci wysięgnik przy rozstawionych, w osi słupa): ten tylko z niego korzysta + bool shared_pole { false }; + bool has_spread { false }; + double spread[ 3 ] { 0.0, 0.0, 0.0 }; }; // łańcuch sieci: kolejne przęsła między podwieszeniami z listy (od kotwy do kotwy), wpis @@ -58,12 +64,28 @@ struct chain { 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 + // przewody jezdne zakotwione osobno (inne kotwienie, zwykle na innym słupie; 0404 albo przewody rozdzielonego): wtedy + // anchor_start/anchor_end trzyma samą linę nośną (0405, lina rozdzielonego). Puste = całość na anchor_start/anchor_end + std::string anchor_start_contact, anchor_end_contact; + // kotwienie środkowe (karty 63-0406 / 0408): podwieszenie, przy którym lina kotwienia środkowego jest zaciśnięta na linie nośnej, + // i karta (0406 z izolacją, 0408 bez); lina idzie do słupów sąsiednich podwieszeń. Puste = brak + std::string middle; + std::string middle_device { "0406" }; + double middle_heights[ 2 ] { 0.0, 0.0 }; // uchwyty liny kotwienia na słupach przed i za (nad główką); 0 = lina nośna sąsiada + // lina nośna z innego podwieszenia niż przewody jezdne (wysięgniki rozstawione na konsoli 4890, wysięgnik samej liny przy + // odciągu przewodów): w punkcie łańcucha przewody idą przez jego "at", lina przez "rope" podwieszenia + // . Wpis "messenger_from ", przed "suspensions" + std::vector> messenger_from; std::vector suspensions; // nazwy podwieszeń z tego samego pliku, po kolei }; +// podwieszenie, które trzyma linę nośną łańcucha w punkcie Listed (samo Listed, gdy nie ma wpisu messenger_from) +std::string const &messenger_holder( chain const &Chain, std::string const &Listed ); + // 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". +// "node anchor device network pole x y z [on ] toward x y z distance +// [color ] [top_color ] [elongation ] [missing ] [ground ] [height ] [rope_height ] [insulators ] [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 @@ -75,12 +97,19 @@ struct anchor { 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 }; + std::string on; // kotwienie na istniejącym słupie: nazwa podwieszenia (z tego pliku), którego słup to + // jest; słupa nie stawia, typ i punkt bierze z tamtego wpisu (pole tylko zapasowo) 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 }; + double height { 0.0 }; // oś rolek nad główką szyny; 0 = z tablicy montażowej (6,70). Kotwienie stałe: przewody + double rope_height { 0.0 }; // kotwienie stałe: uchwyt liny nośnej nad główką szyny; 0 = jak przy ostatnim słupie + // izolatory w łańcuchach do drutów: puste = auto (rolkowe ESN-2025 na obu przy drutach zaśniedziałych, inaczej żywiczne 7150), + // "new" żywiczne, "old" rolkowe na obu, "messenger" rolkowy tylko na linie nośnej, "contact" rolkowe tylko na przewodach + std::string insulators; std::string skin; }; diff --git a/world/catenary_tensioner.cpp b/world/catenary_tensioner.cpp index a695cf0d..06cbb693 100644 --- a/world/catenary_tensioner.cpp +++ b/world/catenary_tensioner.cpp @@ -89,11 +89,11 @@ tensioner_placement placed( std::string const &Role, std::string const &Part, ve return placement; } -// część liniowa: +X części w kierunku Direction +// część liniowa: biegnie w -X części (modele Jana: 4440 z hole_1 na -X), więc -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 ) }; + auto const x { unit( Direction ) * -1.0 }; + auto const y { side_of( x ) * -1.0 }; set_rotation( placement, x, y, cross( x, y ) ); return placement; } @@ -103,8 +103,23 @@ vec place_chain( std::vector const &Links, std::string const &Ro 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; + // "~część": część odwrócona - jej zero (pin_a) na końcu tego odcinka, biegnie z powrotem (uchwyt 4440 przy rolce ruchomej) + auto const reversed { false == link.name.empty() && link.name[ 0 ] == '~' }; + auto const name { reversed ? link.name.substr( 1 ) : link.name }; + auto const end { From + step * link.length }; + auto placement { placed_along( Role + "." + std::to_string( ++index ), name, reversed ? end : From, reversed ? step * -1.0 : step ) }; + if( link.roll != 0.0 ) { + // obrót wokół osi części (kolumna X): Y i Z obracają się w swojej płaszczyźnie + auto const angle { link.roll * kPi / 180.0 }; + auto const c { std::cos( angle ) }, s { std::sin( angle ) }; + for( int r = 0; r < 3; ++r ) { + auto const y { placement.rotation[ r * 3 + 1 ] }, z { placement.rotation[ r * 3 + 2 ] }; + placement.rotation[ r * 3 + 1 ] = c * y + s * z; + placement.rotation[ r * 3 + 2 ] = -s * y + c * z; + } + } + Result.parts.push_back( placement ); + From = end; } return From; } @@ -178,6 +193,10 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin for( auto k { From }; k + 1 < Words.size(); k += 2 ) { tensioner_link link; link.name = Words[ k ]; + if( auto const at { link.name.find( '@' ) }; at != std::string::npos ) { + if( false == number_of( link.name.substr( at + 1 ), link.roll ) ) { report( "bad roll in \"" + link.name + "\"" ); } + link.name.erase( at ); + } if( false == number_of( Words[ k + 1 ], link.length ) ) { report( "bad length \"" + Words[ k + 1 ] + "\"" ); return; } Links.push_back( link ); } @@ -236,13 +255,118 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin 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 }; + if( keyword == "cascade" ) { links_of( words, 1, device->cascade ); continue; } + if( keyword == "series" && words.size() >= 4 ) { + // series [holder ] + device->small_pulley = words[ 1 ]; + if( false == number_of( words[ 2 ], device->small_spacing ) || false == number_of( words[ 3 ], device->dead_end ) ) { report( "series: bad numbers" ); } + if( words.size() >= 6 && words[ 4 ] == "holder" ) { device->fixed_holder = words[ 5 ]; } + continue; + } + if( keyword == "messenger_each" ) { links_of( words, 1, device->messenger_each ); continue; } + if( keyword == "contact_each" ) { links_of( words, 1, device->contact_each ); continue; } + if( keyword == "messenger_equalizer" && words.size() >= 3 ) { + // messenger_equalizer [spread ] [z] + device->messenger_equalizer.name = words[ 1 ]; + if( false == number_of( words[ 2 ], device->messenger_equalizer.length ) ) { report( "bad length \"" + words[ 2 ] + "\"" ); } + for( std::size_t k = 3; k < words.size(); ++k ) { + if( words[ k ] == "spread" && k + 1 < words.size() ) { number_of( words[ ++k ], device->messenger_spread ); } + else if( words[ k ] == "z" ) { device->messenger_equalizer_z = true; } + } + continue; + } + if( keyword == "only" && words.size() >= 2 ) { + if( words[ 1 ] == "messenger" || words[ 1 ] == "contact" ) { device->only = words[ 1 ]; } + else { report( "only: messenger or contact" ); } + continue; + } + if( keyword == "split" && words.size() >= 3 ) { device->split_messenger = words[ 1 ]; device->split_contact = words[ 2 ]; continue; } + if( keyword == "frame" && words.size() >= 5 ) { + device->frame = words[ 1 ]; + double side { -1.0 }; + if( false == number_of( words[ 2 ], device->frame_length ) || false == number_of( words[ 3 ], device->frame_y ) || false == number_of( words[ 4 ], side ) ) { report( "frame: bad numbers" ); } + device->frame_side = side < 0.0 ? -1 : 1; + device->frame_owner = words.size() >= 6 && words[ 5 ] == "owner"; + continue; + } + if( keyword == "messenger2" ) { links_of( words, 1, device->messenger2 ); continue; } + if( keyword == "guide" && words.size() >= 3 ) { + device->guide = words[ 1 ]; + if( false == number_of( words[ 2 ], device->guide_length ) ) { report( "bad guide length \"" + words[ 2 ] + "\"" ); } + for( std::size_t k = 3; k + 1 < words.size(); k += 2 ) { + if( words[ k ] == "mount" ) { number_of( words[ k + 1 ], device->guide_mount ); } + else if( words[ k ] == "bottom" ) { number_of( words[ k + 1 ], device->guide_bottom ); } + else { report( "unknown guide keyword \"" + words[ k ] + "\"" ); } + } + continue; + } + if( keyword == "brackets" && words.size() >= 2 ) { + tensioner_device::bracket_set set; + std::size_t k { 1 }; + for( ; k < words.size() && words[ k ] != "top" && words[ k ] != "bottom" && words[ k ] != "hole" && words[ k ] != "bolt"; ++k ) { + set.poles.push_back( words[ k ] ); + } + for( ; k < words.size(); ) { + auto const &key { words[ k ] }; + if( ( key == "top" || key == "bottom" ) && k + 2 < words.size() ) { + ( key == "top" ? set.top_front : set.bottom_front ) = words[ k + 1 ]; + ( key == "top" ? set.top_back : set.bottom_back ) = words[ k + 2 ]; + k += 3; + } + else if( key == "hole" && k + 2 < words.size() ) { number_of( words[ k + 1 ], set.hole_out ); number_of( words[ k + 2 ], set.hole_drop ); k += 3; } + else if( key == "bolt" && k + 1 < words.size() ) { number_of( words[ k + 1 ], set.bolt ); k += 2; } + else { report( "unknown brackets keyword \"" + key + "\"" ); break; } + } + device->brackets.push_back( set ); + continue; + } + if( keyword == "pivot" && words.size() >= 3 ) { + number_of( words[ 1 ], device->pivot_top ); + number_of( words[ 2 ], device->pivot_bottom ); + if( words.size() >= 5 && words[ 3 ] == "rise" ) { number_of( words[ 4 ], device->mount_rise ); } + continue; + } + if( keyword == "ring" && words.size() >= 3 ) { + device->ring = words[ 1 ]; + if( false == number_of( words[ 2 ], device->ring_arm ) ) { report( "bad ring arm \"" + words[ 2 ] + "\"" ); } + for( std::size_t k = 3; k + 1 < words.size(); k += 2 ) { + if( words[ k ] == "turn" ) { number_of( words[ k + 1 ], device->ring_turn ); } + else if( words[ k ] == "at" ) { number_of( words[ k + 1 ], device->ring_at ); } + else { report( "unknown ring keyword \"" + words[ k ] + "\"" ); } + } + continue; + } + if( keyword == "messenger_old" ) { links_of( words, 1, device->messenger_old ); continue; } + if( keyword == "contact_old" ) { links_of( words, 1, device->contact_old ); continue; } + if( ( keyword == "messenger_clamp" || keyword == "contact_clamp" || keyword == "contact_clamp_old" || keyword == "equalizer" ) && words.size() >= 3 ) { + auto &link { keyword == "messenger_clamp" ? device->messenger_clamp : keyword == "contact_clamp" ? device->contact_clamp : keyword == "contact_clamp_old" ? device->contact_clamp_old : 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 == "safety" && words.size() >= 5 ) { + // safety + device->safety_fixed = words[ 1 ]; + device->safety_moving = words[ 2 ]; + number_of( words[ 3 ], device->safety_arm ); + number_of( words[ 4 ], device->safety_offset ); + continue; + } + if( keyword == "safety_series" && words.size() >= 7 ) { + device->series_arm_fixed = words[ 1 ]; + device->series_arm_moving = words[ 2 ]; + device->series_bar = words[ 3 ]; + number_of( words[ 4 ], device->series_arm ); + number_of( words[ 5 ], device->series_bar_length ); + number_of( words[ 6 ], device->safety_offset ); + continue; + } + if( keyword == "title" && words.size() >= 2 ) { + device->title.clear(); + for( std::size_t k = 1; k < words.size(); ++k ) { device->title += ( k > 1 ? " " : "" ) + words[ k ]; } + continue; + } if( keyword == "type" && words.size() >= 2 ) { tensioner_type type; double code; @@ -263,6 +387,11 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin type.contact_hole = words[ k++ ]; type.axis_hole = words[ k++ ]; } + else if( key == "second" && k + 2 < words.size() ) { + type.second_messenger_hole = words[ k++ ]; + type.second_contact_hole = words[ k++ ]; + type.second_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; } @@ -273,6 +402,12 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin report( "unknown keyword \"" + keyword + "\" in a device" ); continue; } + if( keyword == "network" && words.size() >= 4 && words[ 2 ] == "as" ) { + double code; + if( false == number_of( words[ 3 ], code ) ) { report( "bad network code \"" + words[ 3 ] + "\"" ); continue; } + m_networks[ words[ 1 ] ] = static_cast( code ); + 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) @@ -341,6 +476,7 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin 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 if( words[ k ] == "sheave" ) { pulley.sheave = words[ k + 1 ]; } else { report( "pulley " + pulley.name + ": unknown keyword \"" + words[ k ] + "\"" ); } } m_pulleys.push_back( pulley ); @@ -377,9 +513,17 @@ void tensioner_catalog::parse( std::vector const &Lines, std::strin if( key == "table" ) { device->table = value; } else if( key == "pulley" ) { device->pulley = value; } else if( key == "lever" ) { device->lever = value; } + else if( key == "lever2" ) { device->lever2 = value; } else if( key == "rod" ) { device->rod = value; } + else if( key == "kind" ) { + if( value == "fixed" ) { device->fixed = true; } + else if( value == "split" ) { device->split = true; } + else if( value != "weights" ) { report( "device " + device->name + ": unknown kind \"" + value + "\"" ); } + } else if( key == "rope" ) { device->rope = value; } else if( key == "rope_diameter" ) { number_of( value, device->rope_diameter ); } + else if( key == "rope_eye" && k + 2 < words.size() ) { device->rope_eye = value; number_of( words[ k + 2 ], device->rope_eye_length ); ++k; } + else if( key == "rope_spacer" && k + 2 < words.size() ) { device->rope_spacer = value; number_of( words[ k + 2 ], device->rope_spacer_length ); ++k; } else { report( "device " + device->name + ": unknown keyword \"" + key + "\"" ); } } continue; @@ -402,27 +546,210 @@ tensioner_pulley const *tensioner_catalog::pulley( std::string const &Name ) con for( auto const &entry : m_pulleys ) { if( entry.name == Name ) { return &entry; } } return nullptr; } +tensioner_device::bracket_set const *tensioner_device::brackets_for( std::string const &Pole ) const { + bracket_set const *best { nullptr }; + std::size_t length { 0 }; + for( auto const &set : brackets ) { + for( auto const &pole : set.poles ) { + if( Pole.rfind( pole, 0 ) == 0 && ( best == nullptr || pole.size() > length ) ) { best = &set; length = pole.size(); } + } + } + return best; +} + 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; } +int tensioner_catalog::code_for( std::string const &Symbol, int const Code ) const { + auto const found { m_networks.find( Symbol ) }; + return ( found != m_networks.end() ? found->second : Code ); +} + 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; } +namespace { + +// kotwienie stałe: przegubowy uchwyt (4430) w parze uchwytów konstrukcji na wysokości liny i drugi na wysokości przewodów, +// łańcuchy do drutów wycelowane w ostatnie podwieszenie. Uchwyty w układzie konstrukcji (structure_angle) jak przy ciężarowym +bool solve_fixed( tensioner_device const &Device, tensioner_type const &Type, tensioner_input const &Input, tensioner_result &Result ) { + Result.fixed = true; + vec const out { 1.0, 0.0, 0.0 }; + auto const ca { std::cos( Input.structure_angle ) }, sa { std::sin( Input.structure_angle ) }; + auto const onstructure = [ & ]( vec const &P ) { return vec { ca * P.x - sa * P.y, sa * P.x + ca * P.y, P.z }; }; + auto const fix = [ & ]( std::size_t const From ) { + if( Input.structure_angle == 0.0 ) { return; } + for( auto k { From }; k < Result.parts.size(); ++k ) { + auto &part { Result.parts[ k ] }; + auto const p { onstructure( vec { part.position[ 0 ], part.position[ 1 ], part.position[ 2 ] } ) }; + part.position[ 0 ] = p.x; part.position[ 1 ] = p.y; part.position[ 2 ] = p.z; + for( int c = 0; c < 3; ++c ) { + auto const q { onstructure( vec { part.rotation[ c ], part.rotation[ 3 + c ], part.rotation[ 6 + c ] } ) }; + part.rotation[ c ] = q.x; part.rotation[ 3 + c ] = q.y; part.rotation[ 6 + c ] = q.z; + } + } + }; + auto const *brackets { Input.pole.empty() ? nullptr : Device.brackets_for( Input.pole ) }; + auto const pivotx { brackets != nullptr ? brackets->hole_out : 0.0 }; + // wysokości: przewody - podana albo ostatniego podwieszenia; lina - ostatniego podwieszenia albo 1,70 wyżej + auto const contactheight { Input.height > 0.0 ? Input.height : Input.aimed ? Input.contact_target[ 2 ] : 5.60 }; + auto const messengerheight { Input.rope_height > 0.0 ? Input.rope_height : Input.aimed ? Input.messenger_target[ 2 ] + ( Input.height > 0.0 ? Input.height - Input.contact_target[ 2 ] : 0.0 ) : contactheight + 1.70 }; + Result.rope_height = messengerheight; + Result.height = contactheight; + bool warned { false }; + auto const aimed = [ & ]( vec const &From, double const *Target ) { + if( false == Input.aimed ) { return out; } + vec const to { Target[ 0 ] - From.x, Target[ 1 ] - From.y, Target[ 2 ] - From.z }; + if( to.x < 1.0 ) { + if( false == warned ) { Result.warnings.push_back( "the last suspension is behind or too close to the anchor, the wires are pulled level" ); warned = true; } + return out; + } + return unit( to ); + }; + // para uchwytów wokół przegubu na wysokości Height (górny wprost, dolny odwrócony), z tyłem po drugiej stronie konstrukcji + auto const pair = [ & ]( std::string const &Role, double const Height ) { + if( brackets == nullptr ) { return; } + auto const from { Result.parts.size() }; + for( auto const upside : { false, true } ) { + vec const at { 0.0, 0.0, upside ? Height - Device.pivot_bottom - brackets->hole_drop : Height + Device.pivot_top + brackets->hole_drop }; + auto const name { Role + ( upside ? "_bottom" : "_top" ) }; + if( false == brackets->top_front.empty() ) { + auto placement { placed( name, brackets->top_front, at ) }; + if( upside ) { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, 1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + else { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, 1.0 } ); } + Result.parts.push_back( placement ); + } + auto const depth { Input.depth ? Input.depth( at.z ) : 0.0 }; + if( false == brackets->top_back.empty() && depth > 0.0 ) { + auto placement { placed( name + ".back", brackets->top_back, vec { -depth, 0.0, at.z } ) }; + if( upside ) { set_rotation( placement, vec { 1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + Result.parts.push_back( placement ); + std::string const text { "the bracket bolts are shorter than the structure is deep" }; + if( depth > brackets->bolt + 1e-6 && std::find( Result.warnings.begin(), Result.warnings.end(), text ) == Result.warnings.end() ) { Result.warnings.push_back( text ); } + } + } + fix( from ); + }; + // łańcuch od przegubu do drutów: przegub obraca się za drutami, za wyrównywaczem każdy drut osobno + auto const wires = [ & ]( std::string const &Role, double const Height, double const *Target, std::vector const &Links, + tensioner_link const &Equalizer, int const Count, double const Spread, std::vector const &Each, + tensioner_link const &Clamp, bool const Vertical, std::vector &Ends ) { + pair( "bracket_" + Role, Height ); + auto const start { onstructure( vec { pivotx, 0.0, Height } ) }; + auto const way { aimed( start, Target ) }; + std::vector chain { Device.mount.empty() ? std::vector {} : std::vector { Device.mount.front() } }; + chain.insert( chain.end(), Links.begin(), Links.end() ); + auto end { place_chain( chain, Role, start, way, Result ) }; + std::vector starts; + if( Count >= 2 && false == Equalizer.name.empty() ) { + // przewody obok siebie, liny jedna nad drugą (wyrównywacz obrócony o 90 stopni wokół łańcucha) + vec const up { -way.x * way.z, -way.y * way.z, way.x * way.x + way.y * way.y }; + auto const across { Vertical ? unit( up ) : unit( vec { -way.y, way.x, 0.0 } ) }; + auto placement { placed_along( Role + "_equalizer", Equalizer.name, end, way ) }; + if( Vertical ) { set_rotation( placement, way * -1.0, across, cross( way * -1.0, across ) ); } + Result.parts.push_back( placement ); + end = end + way * Equalizer.length; + for( int k = 0; k < Count; ++k ) { starts.push_back( end + across * ( ( k - 0.5 * ( Count - 1 ) ) * Spread ) ); } + } + else { starts.push_back( end ); } + for( std::size_t k = 0; k < starts.size(); ++k ) { + auto const tag { Role + "_each." + std::to_string( k + 1 ) }; + // dwie liny nośne: górna celuje w linę przęsła, dolna o odstęp lin niżej + auto own { way }; + if( Vertical && starts.size() == 2 && Input.aimed ) { + double const lower[ 3 ] { Target[ 0 ], Target[ 1 ], Target[ 2 ] - ( k == 0 ? Input.messenger_stack : 0.0 ) }; + own = aimed( starts[ k ], lower ); + } + auto at { place_chain( Each, tag, starts[ k ], own, Result ) }; + if( false == Clamp.name.empty() ) { + Result.parts.push_back( placed_along( Role + "_clamp." + std::to_string( k + 1 ), Clamp.name, at, own ) ); + at = at + own * Clamp.length; + } + Ends.push_back( at ); + } + }; + std::vector ropes, contacts; + auto messengerclamp { Device.messenger_clamp }; + if( false == Type.messenger_clamp.empty() ) { messengerclamp.name = Type.messenger_clamp; } + // kotwienie samych przewodów (only contact, 0404): bez łańcucha liny + if( Device.only != "contact" ) { + wires( "messenger", messengerheight, Input.messenger_target, Device.messenger, Device.messenger_equalizer, Input.messengers, Device.equalizer_spread, + Device.messenger_each, messengerclamp, true, ropes ); + } + auto const &clamp { Input.old_fittings && false == Device.contact_clamp_old.name.empty() ? Device.contact_clamp_old : Device.contact_clamp }; + // kotwienie środkowe: tylko lina kotwienia (only messenger) + if( Device.only != "messenger" ) { + wires( "contact", contactheight, Input.contact_target, Device.contact, Device.equalizer, Input.contacts, Device.equalizer_spread, Device.contact_each, clamp, false, contacts ); + } + auto const copy = []( vec const &P, double *Out ) { Out[ 0 ] = P.x; Out[ 1 ] = P.y; Out[ 2 ] = P.z; }; + // lina nośna przęsła to górna (dolna o odstęp lin niżej, jak w typie sieci) + if( false == ropes.empty() ) { copy( ropes.back(), Result.messenger_end ); } + if( ropes.size() >= 2 ) { copy( ropes.front(), Result.messenger2_end ); } + for( auto const &end : contacts ) { Result.contact_ends.push_back( { end.x, end.y, end.z } ); } + // obrys dla edytora: od czoła do końca przegubu, na wysokości przewodów + Result.b = pivotx + ( Device.mount.empty() ? 0.0 : Device.mount.front().length ); + Result.s = contactheight; + return true; +} + +} // namespace + 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 ) ); } + if( Device.fixed ) { return solve_fixed( Device, *type, Input, Result ); } + if( Device.split ) { + // kotwienie rozdzielone: urządzenie liny i urządzenie przewodów (albo ich odmiana dla typu sieci: -) + auto const pick = [ & ]( std::string const &Name ) -> tensioner_device const * { + if( auto const *exact { Catalog.device( Name ) }; exact != nullptr && exact->type( Input.type ) != nullptr ) { return exact; } + for( auto const &other : Catalog.devices() ) { + if( other.name.rfind( Name + "-", 0 ) == 0 && other.type( Input.type ) != nullptr ) { return &other; } + } + return nullptr; + }; + auto const *messengerdevice { pick( Device.split_messenger ) }, *contactdevice { pick( Device.split_contact ) }; + if( messengerdevice == nullptr || contactdevice == nullptr ) { return fail( "device " + Device.name + " has no messenger / contact device for network type " + std::to_string( Input.type ) ); } + tensioner_result rope, wires; + auto contactinput { Input }; + contactinput.missing = 0; // brakujące ciężary - ze stosu liny + if( Input.height > 0.0 ) { + // wysokość podana dla rolek liny; przewody o tyle niżej, co w tablicach + auto const *upper { Catalog.table( messengerdevice->table ) }, *lower { Catalog.table( contactdevice->table ) }; + if( upper != nullptr && lower != nullptr ) { contactinput.height = Input.height - ( upper->height - lower->height ); } + } + if( false == solve_tensioner( Catalog, *messengerdevice, Input, rope ) ) { Result = rope; return false; } + if( false == solve_tensioner( Catalog, *contactdevice, contactinput, wires ) ) { Result = wires; return false; } + Result = rope; + for( auto part : wires.parts ) { part.role = "contact_device." + part.role; Result.parts.push_back( part ); } + Result.rope2 = wires.rope; + Result.contact_ends = wires.contact_ends; + Result.contact_tension = wires.contact_tension; + Result.tension = rope.tension + wires.tension; + Result.nominal_tension = rope.nominal_tension + wires.nominal_tension; + Result.weights = rope.weights + wires.weights; + Result.grounded = rope.grounded || wires.grounded; + for( auto const &warning : wires.warnings ) { + if( std::find( Result.warnings.begin(), Result.warnings.end(), warning ) == Result.warnings.end() ) { Result.warnings.push_back( warning ); } + } + return true; + } 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 ); } + // urządzenie tylko liny albo tylko przewodów (kotwienie rozdzielone): bez dźwigni, wszystkie "otwory" w osi na końcu uchwytu + auto const only { false == Device.only.empty() }; + auto const onlymessenger { Device.only == "messenger" }; + static std::pair const nowhere { 0.0, 0.0 }; + auto const *lever { only ? nullptr : Catalog.lever( Device.lever ) }; + if( false == only && lever == nullptr ) { return fail( "no lever " + Device.lever ); } auto const hole = [ & ]( std::string const &Name ) -> std::pair const * { + if( only ) { return &nowhere; } auto const found { lever->holes.find( Name ) }; return found == lever->holes.end() ? nullptr : &found->second; }; @@ -432,6 +759,23 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & if( messengerhole == nullptr || contacthole == nullptr || axishole == nullptr ) { return fail( "lever " + lever->name + " has no hole " + type->messenger_hole + "/" + type->contact_hole + "/" + type->axis_hole ); } + // druga dźwignia (dźwignie szeregowo): lever2 urządzenia albo ta sama część co pierwsza + auto const cascade { false == only && false == type->second_axis_hole.empty() }; + auto const *secondlever { cascade ? Catalog.lever( Device.lever2.empty() ? Device.lever : Device.lever2 ) : nullptr }; + if( cascade && secondlever == nullptr ) { return fail( "no lever " + Device.lever2 ); } + auto const secondholeof = [ & ]( std::string const &Name ) -> std::pair const * { + auto const found { secondlever->holes.find( Name ) }; + return found == secondlever->holes.end() ? nullptr : &found->second; + }; + auto const *secondmessenger { cascade ? secondholeof( type->second_messenger_hole ) : nullptr }; + auto const *secondcontact { cascade ? secondholeof( type->second_contact_hole ) : nullptr }; + auto const *secondaxis { cascade ? secondholeof( type->second_axis_hole ) : nullptr }; + if( cascade && ( secondmessenger == nullptr || secondcontact == nullptr || secondaxis == nullptr ) ) { + return fail( "lever " + secondlever->name + " has no hole " + type->second_messenger_hole + "/" + type->second_contact_hole + "/" + type->second_axis_hole ); + } + if( cascade && ( Device.cascade.empty() || Device.messenger2.empty() ) ) { return fail( "device " + Device.name + " has no cascade/messenger2 chain for the second lever" ); } + Result.cascade = cascade; + auto const series { false == Device.small_pulley.empty() }; 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; }() }; @@ -444,6 +788,9 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & 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; + // urządzenie wyżej albo niżej na konstrukcji niż w tablicy: lina ta sama, ciężary przesunięte o tyle samo + auto const height { Input.height > 0.0 ? Input.height : table->height }; + s += height - table->height; // stos ciężarów od dołu, brakujące z góry std::vector stack; @@ -452,12 +799,63 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & if( weight == nullptr ) { return fail( "no weight " + name ); } for( int k = 0; k < count; ++k ) { stack.push_back( weight ); } } + double fullmass { 0.0 }; + for( auto const *weight : stack ) { fullmass += weight->mass; } 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; + Result.nominal_tension = 4.0 * fullmass * Input.gravity; + + // podział naciągu dźwignią: momenty względem osi; część naciągu na otworze liny + auto const share = [ & ]( std::pair const &Messenger, std::pair const &Contact, std::pair const &Axis ) { + auto const arm_m { std::abs( Messenger.first - Axis.first ) }, arm_c { std::abs( Contact.first - Axis.first ) }; + return arm_m + arm_c > 1e-9 ? arm_c / ( arm_m + arm_c ) : 0.5; + }; + auto const messengershare { only ? ( onlymessenger ? 1.0 : 0.0 ) : share( *messengerhole, *contacthole, *axishole ) }; + auto const secondshare { cascade ? share( *secondmessenger, *secondcontact, *secondaxis ) : 0.0 }; + // części naciągu całej sieci: pierwsza lina, druga lina (szeregowo), przewody. Przesunięcia końców drutów przenoszą się na oś + // pierwszej dźwigni z tymi samymi wagami (dźwignia sztywna: oś = część·przesunięcie liny + reszta·przesunięcie drugiej strony) + auto const first { messengershare }; + auto const second { cascade ? ( 1.0 - messengershare ) * secondshare : 0.0 }; + auto const wires { 1.0 - first - second }; + auto const ropeshare { first + second }; + + // mniejszy naciąg niż w tablicy: sieć krótsza o wydłużenie sprężyste od różnicy naciągów i o nadmiar długości zwisu. + // Wieszaki trzymają przewody pod liną, więc dodatkowy zwis jest wspólny i niesie go naciąg liny i przewodów razem: + // lina zwisa f0·(Hm0 + Hc)/(Hm + Hc), przewody (prosto przy pełnym naciągu) f0·(Hm0 − Hm)/(Hm + Hc), gdzie + // f0 = w·a²/(8·Hm0); nadmiar długości zwisu w przęśle 8·f²/(3·a), przęseł distance/a. Dwie liny szeregowo: każda ma połowę + // sztywności i swoją część naciągu + if( mass > 0.0 && Result.tension < Result.nominal_tension && ( Input.messenger_stiffness > 0.0 || Input.contact_stiffness > 0.0 ) ) { + auto const length { std::max( 0.0, Input.distance ) }; + auto const drop { Result.nominal_tension - Result.tension }; + auto const messenger0 { Result.nominal_tension * ropeshare }, messenger { Result.tension * ropeshare }; + auto const contact0 { Result.nominal_tension * wires }, contact { Result.tension * wires }; + double sagmessengershift { 0.0 }, sagcontactshift { 0.0 }; + if( Input.load > 0.0 && Input.span > 0.0 && messenger0 > 0.0 && messenger + contact > 0.0 ) { + auto const sag0 { Input.load * Input.span * Input.span / ( 8.0 * messenger0 ) }; + auto const sagmessenger { sag0 * ( messenger0 + contact ) / ( messenger + contact ) }; + auto const sagcontact { sag0 * ( messenger0 - messenger ) / ( messenger + contact ) }; + auto const spans { length / Input.span }; + sagmessengershift = spans * 8.0 / ( 3.0 * Input.span ) * ( sagmessenger * sagmessenger - sag0 * sag0 ); + sagcontactshift = spans * 8.0 / ( 3.0 * Input.span ) * sagcontact * sagcontact; + } + // przesunięcie końca jednej grupy drutów o części Part naciągu i sztywności Stiffness + auto const shift = [ & ]( double const Part, double const Stiffness, double const Sag ) { + return Part * ( ( Stiffness > 0.0 ? Part * drop * length / Stiffness : 0.0 ) + Sag ); + }; + if( cascade ) { + Result.shortening = shift( first, 0.5 * Input.messenger_stiffness, sagmessengershift ) + shift( second, 0.5 * Input.messenger_stiffness, sagmessengershift ); + } + else { + Result.shortening = shift( first, Input.messenger_stiffness, sagmessengershift ); + } + Result.shortening += shift( wires, Input.contact_stiffness, sagcontactshift ); + b += Result.shortening; + s += 4.0 * Result.shortening; + } // ciężary nie zejdą niżej niż grunt: lina nie puści rolki bliżej konstrukcji, sieć traci naciąg if( s < Input.ground ) { @@ -470,61 +868,324 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & Result.b = b; Result.s = s; - auto const height { table->height }; + Result.height = 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 ) ); + // uchwyty do konstrukcji: 4450 zaczyna się w otworach uchwytów, hole_out przed czołem + auto const *brackets { Input.pole.empty() ? nullptr : Device.brackets_for( Input.pole ) }; + auto const pivotx { brackets != nullptr ? brackets->hole_out : 0.0 }; + // układ konstrukcji: obrócony o structure_angle wokół pionu przez zero (czoło); części konstrukcji liczone w nim, potem tu + auto const ca { std::cos( Input.structure_angle ) }, sa { std::sin( Input.structure_angle ) }; + auto const onstructure = [ & ]( vec const &P ) { return vec { ca * P.x - sa * P.y, sa * P.x + ca * P.y, P.z }; }; + auto const fix = [ & ]( std::size_t const From ) { + if( Input.structure_angle == 0.0 ) { return; } + for( auto k { From }; k < Result.parts.size(); ++k ) { + auto &part { Result.parts[ k ] }; + auto const at { onstructure( vec { part.position[ 0 ], part.position[ 1 ], part.position[ 2 ] } ) }; + part.position[ 0 ] = at.x; part.position[ 1 ] = at.y; part.position[ 2 ] = at.z; + for( int column = 0; column < 3; ++column ) { + auto const axis { onstructure( vec { part.rotation[ column ], part.rotation[ 3 + column ], part.rotation[ 6 + column ] } ) }; + part.rotation[ column ] = axis.x; part.rotation[ 3 + column ] = axis.y; part.rotation[ 6 + column ] = axis.z; + } + } + }; + // przód: -X modelu przed czoło (obrót o 180 stopni wokół pionu); Upside: do góry nogami (otwór nad zerem) + auto const front = [ & ]( std::string const &Role, std::string const &Part, vec const &At, bool const Upside ) { + if( Part.empty() ) { return; } + auto placement { placed( Role, Part, At ) }; + if( Upside ) { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, 1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + else { set_rotation( placement, vec { -1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, 1.0 } ); } + Result.parts.push_back( placement ); + }; + // tył: na tylnej ścianie konstrukcji, -X modelu na zewnątrz (w -X urządzenia) + auto const back = [ & ]( std::string const &Role, std::string const &Part, vec const &At, bool const Upside ) { + if( Part.empty() || brackets == nullptr ) { return; } + auto const depth { Input.depth ? Input.depth( At.z ) : 0.0 }; + if( depth <= 0.0 ) { return; } + auto placement { placed( Role, Part, vec { -depth, At.y, At.z } ) }; + if( Upside ) { set_rotation( placement, vec { 1.0, 0.0, 0.0 }, vec { 0.0, -1.0, 0.0 }, vec { 0.0, 0.0, -1.0 } ); } + Result.parts.push_back( placement ); + if( depth > brackets->bolt + 1e-6 ) { + std::string const text { "the bracket bolts are shorter than the structure is deep" }; + if( std::find( Result.warnings.begin(), Result.warnings.end(), text ) == Result.warnings.end() ) { Result.warnings.push_back( text ); } + } + }; + auto const bracketsfrom { Result.parts.size() }; + if( brackets != nullptr ) { + // dwa uchwyty wokół 4450: górny za mount_top, dolny odwrócony za mount_bottom + vec const top { 0.0, 0.0, height - Device.mount_rise + Device.pivot_top + brackets->hole_drop }; + vec const bottom { 0.0, 0.0, height - Device.mount_rise - Device.pivot_bottom - brackets->hole_drop }; + front( "bracket_top", brackets->top_front, top, false ); + back( "bracket_top.back", brackets->top_back, top, false ); + front( "bracket_bottom", brackets->top_front, bottom, true ); + back( "bracket_bottom.back", brackets->top_back, bottom, true ); } - 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 + fix( bracketsfrom ); + // przegub 4450 w otworach uchwytów (przy konstrukcji), dalej urządzenie w stronę drutów + // uchwyt z otworem rolki nad zerem (5040) zaczyna się niżej, rolka zostaje na wysokości z tablicy + auto const fixedaxle { place_chain( Device.mount, "mount", onstructure( vec { pivotx, 0.0, height - Device.mount_rise } ), out, Result ) + vec { 0.0, 0.0, Device.mount_rise } }; + // rolki szeregowe: b to oś dużej ruchomej (skrajnej), uchwyt dźwigni zaczyna się na małej ruchomej + auto const *smallpulley { series ? Catalog.pulley( Device.small_pulley ) : nullptr }; + if( series && smallpulley == nullptr ) { return fail( "no pulley " + Device.small_pulley ); } + vec const movingaxle { series ? b - Device.small_spacing : b, 0.0, height }; + vec const movingbig { b, 0.0, height }; + auto const fixedsmall { fixedaxle + out * Device.small_spacing }; + auto const radius { 0.5 * pulley->diameter + 0.5 * Device.rope_diameter }; + auto const smallradius { series ? 0.5 * smallpulley->diameter + 0.5 * Device.rope_diameter : radius }; + if( series ? movingaxle.x - fixedsmall.x < 2.0 * smallradius : movingaxle.x - fixedaxle.x < 2.0 * radius ) { + Result.warnings.push_back( "the pulleys overlap: the network pulled the moving one to the structure" ); + } + if( series ) { + double b0 { b }, s0 { 0.0 }; + table->lookup( 10.0, Input.distance, b0, s0 ); + auto const travel { b - b0 }; + // lina przechodzi: mała ruchoma d, mała stała 2d, duża ruchoma 3d, duża stała 4d + Result.turn_moving_a = travel / smallradius; + Result.turn_fixed_a = 2.0 * travel / smallradius; + Result.turn_moving_b = 3.0 * travel / radius; + Result.turn_fixed_b = 4.0 * travel / radius; + auto const spun = [ & ]( std::string const &Role, std::string const &Part, vec const &Axle, double const Angle ) { + auto placement { placed( Role, Part, Axle ) }; + auto const c { std::cos( Angle ) }, s { std::sin( Angle ) }; + set_rotation( placement, vec { c, 0.0, -s }, vec { 0.0, 1.0, 0.0 }, vec { s, 0.0, c } ); + Result.parts.push_back( placement ); + }; + spun( "pulley_fixed", pulley->name, fixedaxle, Result.turn_fixed_b ); + spun( "pulley_fixed_small", smallpulley->name, fixedsmall, Result.turn_fixed_a ); + spun( "pulley_moving", smallpulley->name, movingaxle, Result.turn_moving_a ); + spun( "pulley_moving_big", pulley->name, movingbig, Result.turn_moving_b ); + if( false == Device.fixed_holder.empty() ) { Result.parts.push_back( placed_along( "holder_fixed", Device.fixed_holder, fixedaxle, out ) ); } + } + else { + // krążki obracają się o linę przewiniętą od położenia przy +10 °C (bez wydłużenia). Przy przesunięciu rolki ruchomej + // o d lina przechodzi kolejno: ruchoma A d, stała A 2d, ruchoma B 3d, stała B 4d (tyle jadą ciężary); wszystkie + // krążki obracają się w tę samą stronę: przy rolce oddalającej się od konstrukcji lina cofa się w stronę zaczepu, + // co w płaszczyźnie X-Z jest obrotem wokół +Y (Z w stronę X) + double b0 { b }, s0 { 0.0 }; + table->lookup( 10.0, Input.distance, b0, s0 ); + auto const travel { b - b0 }; + auto const turn = [ & ]( double const Times ) { return Times * travel / radius; }; + auto const twin { pulley->grooves >= 2 && false == pulley->sheave.empty() }; + Result.turn_moving_a = turn( 1.0 ); + Result.turn_fixed_a = turn( 2.0 ); + Result.turn_moving_b = turn( pulley->grooves >= 2 ? 3.0 : 1.0 ); + Result.turn_fixed_b = turn( pulley->grooves >= 2 ? 4.0 : 2.0 ); + auto const spun = [ & ]( std::string const &Role, std::string const &Part, vec const &Axle, double const Angle ) { + auto placement { placed( Role, Part, Axle ) }; + auto const c { std::cos( Angle ) }, s { std::sin( Angle ) }; + set_rotation( placement, vec { c, 0.0, -s }, vec { 0.0, 1.0, 0.0 }, vec { s, 0.0, c } ); + Result.parts.push_back( placement ); + }; + spun( "pulley_fixed", pulley->name, fixedaxle, Result.turn_fixed_a ); + spun( "pulley_moving", pulley->name, movingaxle, Result.turn_moving_a ); + if( twin ) { + spun( "sheave_fixed", pulley->sheave, fixedaxle, Result.turn_fixed_b ); + spun( "sheave_moving", pulley->sheave, movingaxle, Result.turn_moving_b ); + } + } + // dźwignia: otwór osi na końcu uchwytu od rolki ruchomej; przy otworze liny niżej niż otwór przewodów odwrócona. + // Dźwignia na sworzniu staje prostopadle do wypadkowej sił z obu łańcuchów, a uchwyt 4440 idzie wzdłuż tej + // wypadkowej (pochylenie tylko w płaszczyźnie X-Z urządzenia; łańcuchy celują dalej w 3D). Otwory zależą od + // pochylenia, a pochylenie od kierunków z otworów, więc kilka przybliżeń (cel daleko - zbiega się od razu) 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 ) }; + Result.messenger_tensions[ 0 ] = Result.tension * first; + Result.messenger_tensions[ 1 ] = Result.tension * second; + Result.messenger_tension = Result.tension * ropeshare; + Result.contact_tension = Result.tension - Result.messenger_tension; + double holderlength { 0.0 }; + for( auto const &link : Device.holder ) { holderlength += link.length; } + // kierunek łańcucha do ostatniego podwieszenia albo poziomo; Short: cel za blisko albo za konstrukcją + auto const aim = [ & ]( vec const &From, double const *Target, bool &Short ) { + Short = false; + if( false == Input.aimed ) { return out; } + vec const to { Target[ 0 ] - From.x, Target[ 1 ] - From.y, Target[ 2 ] - From.z }; + if( to.x < 1.0 ) { Short = true; return out; } + return unit( to ); }; - { - 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 ); + vec pull { out }; // kierunek uchwytu i wypadkowej, w płaszczyźnie X-Z + vec axispoint, across, upward; // oś dźwigni i jej osie: across = wzdłuż pull, upward = wzdłuż dźwigni + auto const lever_frame = [ & ]() { + axispoint = movingaxle + pull * holderlength; + across = pull; + upward = vec { -pull.z, 0.0, pull.x }; + }; + auto const holepoint = [ & ]( std::pair const &Hole ) { + return axispoint + across * ( sign * ( Hole.second - axishole->second ) ) + upward * ( sign * ( Hole.first - axishole->first ) ); + }; + lever_frame(); + // druga dźwignia (szeregowo): oś na końcu łańcucha "cascade" z otworu przewodów pierwszej, wzdłuż wypadkowej drugiej + auto const secondsign { cascade && secondmessenger->first < secondcontact->first ? -1.0 : 1.0 }; + double cascadelength { 0.0 }; + for( auto const &link : Device.cascade ) { cascadelength += link.length; } + vec secondpull { out }, secondaxispoint; + auto const secondhole = [ & ]( std::pair const &Hole ) { + vec const up { -secondpull.z, 0.0, secondpull.x }; + return secondaxispoint + secondpull * ( secondsign * ( Hole.second - secondaxis->second ) ) + up * ( secondsign * ( Hole.first - secondaxis->first ) ); + }; + auto const second_frame = [ & ]() { if( cascade ) { secondaxispoint = holepoint( *contacthole ) + secondpull * cascadelength; } }; + second_frame(); + // wypadkowa w płaszczyźnie X-Z; dźwignia nie przechyla się dalej niż do 45 stopni (dziwny cel - zostaje bliżej pionu) + auto const flat_pull = []( vec const &Resultant, vec &Pull ) { + vec const flat { Resultant.x, 0.0, Resultant.z }; + if( length( flat ) < 1e-9 || flat.x < std::abs( flat.z ) ) { return false; } + Pull = unit( flat ); + return true; + }; + if( Input.aimed ) { + for( int pass = 0; pass < 6; ++pass ) { + bool shortm, shortc; + auto const previous { pull }; + auto const wm { aim( holepoint( *messengerhole ), Input.messenger_target, shortm ) }; + vec wc; + if( cascade ) { + // druga dźwignia ciągnie pierwszą wzdłuż swojej wypadkowej + auto const wm2 { aim( secondhole( *secondmessenger ), Input.messenger_target, shortm ) }; + auto const wc2 { aim( secondhole( *secondcontact ), Input.contact_target, shortc ) }; + flat_pull( wm2 * secondshare + wc2 * ( 1.0 - secondshare ), secondpull ); + wc = secondpull; + } + else { wc = aim( holepoint( *contacthole ), Input.contact_target, shortc ); } + if( false == flat_pull( wm * messengershare + wc * ( 1.0 - messengershare ), pull ) ) { break; } + lever_frame(); + second_frame(); + if( length( pull - previous ) < 1e-6 && pass > 0 ) { break; } + } } - // 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; + Result.lever_tilt = std::atan2( -pull.z, pull.x ); + { + auto const from { Result.parts.size() }; + place_chain( Device.holder, "holder", movingaxle, pull, Result ); + // rolki szeregowe: uchwyt 4440-5 odwrócony tyłem na przód względem uchwytu rolki stałej (Jan 2026-10-25) - obrót o 180 + // stopni wokół jego długiej osi + if( series ) { + for( auto k { from }; k < Result.parts.size(); ++k ) { + auto &r { Result.parts[ k ].rotation }; + for( auto const column : { 1, 2 } ) { for( int row = 0; row < 3; ++row ) { r[ row * 3 + column ] = -r[ row * 3 + column ]; } } + } + } } - // łańcuchy do drutów - { - auto end { place_chain( Device.messenger, "messenger", holepoint( *messengerhole ), out, Result ) }; + // zabezpieczenie: ramiona z osi obu rolek do wspólnej dolnej śruby, para po obu stronach rolek + // zabezpieczenie rolek szeregowych: ramiona z osi małych rolek pochylone tak, żeby ich końce dzielił drążek + if( series && false == Device.series_arm_fixed.empty() ) { + auto const offset { 0.5 * ( ( movingaxle.x - fixedsmall.x ) - Device.series_bar_length ) }; + if( std::abs( offset ) < Device.series_arm ) { + auto const drop { std::sqrt( Device.series_arm * Device.series_arm - offset * offset ) }; + for( auto const side : { -1.0, 1.0 } ) { + vec const aside { 0.0, side * Device.safety_offset, 0.0 }; + auto const a { fixedsmall + aside }, b { movingaxle + aside }; + vec const lowa { a.x + offset, a.y, height - drop }, lowb { b.x - offset, b.y, height - drop }; + auto const role { std::string( side < 0.0 ? "safety_a" : "safety_b" ) }; + Result.parts.push_back( placed_along( role + ".fixed", Device.series_arm_fixed, a, lowa - a ) ); + Result.parts.push_back( placed_along( role + ".moving", Device.series_arm_moving, b, lowb - b ) ); + if( false == Device.series_bar.empty() ) { Result.parts.push_back( placed_along( role + ".bar", Device.series_bar, lowa, lowb - lowa ) ); } + } + } + else { Result.warnings.push_back( "the pulleys are further apart than the safety arms reach" ); } + } + if( false == series && false == Device.safety_fixed.empty() ) { + auto const half { 0.5 * ( movingaxle.x - fixedaxle.x ) }; + if( half < Device.safety_arm ) { + auto const drop { std::sqrt( Device.safety_arm * Device.safety_arm - half * half ) }; + for( auto const side : { -1.0, 1.0 } ) { + vec const offset { 0.0, side * Device.safety_offset, 0.0 }; + vec const bolt { fixedaxle.x + half, offset.y, height - drop }; + auto const role { std::string( side < 0.0 ? "safety_a" : "safety_b" ) }; + Result.parts.push_back( placed_along( role + ".fixed", Device.safety_fixed, fixedaxle + offset, bolt - ( fixedaxle + offset ) ) ); + Result.parts.push_back( placed_along( role + ".moving", Device.safety_moving, movingaxle + offset, bolt - ( movingaxle + offset ) ) ); + } + } + else { Result.warnings.push_back( "the pulleys are further apart than the safety arms reach" ); } + } + + // model dźwigni (Jan): zero w otworze osi II, otwory na drutach odsunięte w -X modelu, czyli w stronę drutów; + // -X modelu idzie więc w stronę drutów (obrót o 180 stopni wokół pionu), odwrócona dodatkowo o 180 wokół Y; + // przechylona razem z układem across/upward + if( false == only ) { + auto placement { placed( "lever", lever->name, holepoint( { 0.0, 0.0 } ) ) }; + set_rotation( placement, across * -sign, vec { 0.0, -1.0, 0.0 }, upward * sign ); + Result.parts.push_back( placement ); + } + if( cascade ) { + place_chain( Device.cascade, "cascade", holepoint( *contacthole ), secondpull, Result ); + auto placement { placed( "lever2", secondlever->name, secondhole( { 0.0, 0.0 } ) ) }; + set_rotation( placement, secondpull * -secondsign, vec { 0.0, -1.0, 0.0 }, vec { -secondpull.z, 0.0, secondpull.x } * secondsign ); + Result.parts.push_back( placement ); + } + + // łańcuchy do drutów: w stronę ostatniego podwieszenia (gdy podane), inaczej poziomo + bool warned { false }; + auto const aimed = [ & ]( vec const &From, double const *Target ) { + bool tooshort; + auto const way { aim( From, Target, tooshort ) }; + if( tooshort && false == warned ) { + Result.warnings.push_back( "the last suspension is behind or too close to the anchor, the wires are pulled level" ); + warned = true; + } + return way; + }; + // lina nośna z otworu dźwigni do uchwytu krańcowego; Role: messenger albo messenger2 + // druga (dolna) lina nośna celuje o odstęp lin niżej niż pierwsza + double const lowertarget[ 3 ] { Input.messenger_target[ 0 ], Input.messenger_target[ 1 ], Input.messenger_target[ 2 ] - Input.messenger_stack }; + auto const rope_chain = [ & ]( vec const &Start, std::vector const &Links, std::string const &Role, double *End, double const *Target ) { + auto const way { aimed( Start, Target ) }; + auto end { place_chain( Links, Role, Start, way, 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.parts.push_back( placed_along( Role + "_clamp", clamp, end, way ) ); + end = end + way * Device.messenger_clamp.length; + } + End[ 0 ] = end.x; + End[ 1 ] = end.y; + End[ 2 ] = end.z; + }; + // izolatory rolkowe tylko w zwykłym złożeniu (szeregowo izolator jest w łańcuchu do drugiej dźwigni) + if( only && onlymessenger && Input.messengers >= 2 && false == Device.messenger_equalizer.name.empty() ) { + // dwie liny z jednego urządzenia (sieć 32: dźwignia równoramienna 6403, 35: wyrównywacz 4410): jedna nad drugą + auto const start { holepoint( *messengerhole ) }; + auto const way { aimed( start, Input.messenger_target ) }; + auto end { place_chain( Device.messenger, "messenger", start, way, Result ) }; + vec const up { -way.x * way.z, -way.y * way.z, way.x * way.x + way.y * way.y }; + auto const across { unit( up ) }; + auto placement { placed_along( "messenger_equalizer", Device.messenger_equalizer.name, end, way ) }; + if( false == Device.messenger_equalizer_z ) { set_rotation( placement, way * -1.0, across, cross( way * -1.0, across ) ); } + Result.parts.push_back( placement ); + end = end + way * Device.messenger_equalizer.length; + auto const clamp { type->messenger_clamp.empty() ? Device.messenger_clamp.name : type->messenger_clamp }; + for( int k = 0; k < 2; ++k ) { + auto const tag { "messenger_each." + std::to_string( k + 1 ) }; + // każda lina celuje w ostatnie podwieszenie osobno (rozstaw dźwigni 6403 zbiega się w przęśle) + auto const from { end + across * ( ( k - 0.5 ) * Device.messenger_spread ) }; + auto const own { aimed( from, k == 1 ? Input.messenger_target : lowertarget ) }; + auto at { place_chain( Device.messenger_each, tag, from, own, Result ) }; + if( false == clamp.empty() ) { + Result.parts.push_back( placed_along( "messenger_clamp." + std::to_string( k + 1 ), clamp, at, own ) ); + at = at + own * Device.messenger_clamp.length; + } + auto *out { k == 1 ? Result.messenger_end : Result.messenger2_end }; + out[ 0 ] = at.x; out[ 1 ] = at.y; out[ 2 ] = at.z; } - 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 ) }; + else if( false == only || onlymessenger ) { + rope_chain( holepoint( *messengerhole ), Input.old_messenger && false == cascade && false == Device.messenger_old.empty() ? Device.messenger_old : Device.messenger, "messenger", Result.messenger_end, Input.messenger_target ); + } + if( cascade ) { rope_chain( secondhole( *secondmessenger ), Device.messenger2, "messenger2", Result.messenger2_end, lowertarget ); } + if( false == only || false == onlymessenger ) { + auto const start { cascade ? secondhole( *secondcontact ) : holepoint( *contacthole ) }; + auto const way { aimed( start, Input.contact_target ) }; + auto const &links { Input.old_contact && false == cascade && false == Device.contact_old.empty() ? Device.contact_old : Device.contact }; + auto end { place_chain( links, "contact", start, way, 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; + Result.parts.push_back( placed_along( "equalizer", Device.equalizer.name, end, way ) ); + end = end + way * Device.equalizer.length; + // rozstaw przewodów poziomo, w poprzek kierunku łańcucha + auto const across { unit( vec { -way.y, way.x, 0.0 } ) }; 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 } ); + ends.push_back( end + across * offset ); } } else { @@ -532,9 +1193,10 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & } 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; + auto const &clamp { Input.old_fittings && false == Device.contact_clamp_old.name.empty() ? Device.contact_clamp_old : Device.contact_clamp }; + if( false == clamp.name.empty() ) { + Result.parts.push_back( placed_along( "contact_clamp." + std::to_string( k + 1 ), clamp.name, at, way ) ); + at = at + way * clamp.length; } Result.contact_ends.push_back( { at.x, at.y, at.z } ); } @@ -546,33 +1208,73 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & 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 ) ) ) ) }; + // kąt w płaszczyźnie X-Z od +X w stronę +Z; punkty co najwyżej co 5 stopni (gładka rura liny), bez pierwszego + auto const steps { std::max( 1, static_cast( std::ceil( std::abs( To - From ) / ( kPi / 36.0 ) - 1e-9 ) ) ) }; 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 ); + if( series ) { + // zaczep na uchwycie stałym, ukośnie pod małą ruchomą, wokół niej w górę, górą do małej stałej, wokół niej w dół, + // dołem do dużej ruchomej, wokół niej w górę, górą do dużej stałej i z jej lewej strony w dół + auto const around = [ & ]( vec const &Center, double const Radius, double const From, double const To ) { + auto const steps { std::max( 1, static_cast( std::ceil( std::abs( To - From ) / ( kPi / 36.0 ) - 1e-9 ) ) ) }; + for( int k = 1; k <= steps; ++k ) { + auto const angle { From + ( To - From ) * k / steps }; + point( vec { Center.x + Radius * std::cos( angle ), 0.0, Center.z + Radius * std::sin( angle ) } ); + } + }; + point( vec { fixedaxle.x + Device.dead_end, 0.0, height } ); + point( vec { movingaxle.x, 0.0, height - smallradius } ); + around( movingaxle, smallradius, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedsmall.x, 0.0, height + smallradius } ); + around( fixedsmall, smallradius, 0.5 * kPi, 1.5 * kPi ); + point( vec { movingbig.x, 0.0, height - radius } ); + around( movingbig, radius, -0.5 * kPi, 0.5 * kPi ); + point( vec { fixedaxle.x, 0.0, height + radius } ); + around( fixedaxle, radius, 0.5 * kPi, kPi ); + } + else { + 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 }; + // oś stosu: pod rolką stałą; z prowadnicą na uchwytach ustawia ją pierścień jeżdżący po osi prowadnicy (mount 5070 w hole_1 + // uchwytu, Jan 2026-10-24) - lina schodzi z rolki lekko ukośnie + auto const turn { Device.ring_turn * kPi / 180.0 }; + vec toward { -std::cos( turn ), std::sin( turn ), 0.0 }; // od osi stosu do prowadnicy + auto const onbrackets { brackets != nullptr && false == Device.guide.empty() && false == Device.ring.empty() }; + auto const onframe { onbrackets && false == Device.frame.empty() }; + // na ramie 5090: prowadnica w otworze ramy, stos pod liną (Y 0) o ramię pierścienia od prowadnicy, przy konstrukcji albo dalej; + // pierścień obrócony w stronę prowadnicy (Jan 2026-10-24) + if( onframe ) { + auto const offset { std::max( 0.0, Device.ring_arm * Device.ring_arm - Device.frame_y * Device.frame_y ) }; + vec const guide { brackets->hole_out + Device.frame_length, Device.frame_y, 0.0 }; + vec const stack { guide.x + Device.frame_side * std::sqrt( offset ), 0.0, 0.0 }; + toward = unit( vec { guide.x - stack.x, guide.y - stack.y, 0.0 } ); + } + vec const guideaxis { onframe ? vec { brackets->hole_out + Device.frame_length, Device.frame_y, 0.0 } : onbrackets ? vec { brackets->hole_out, 0.0, 0.0 } - toward * Device.guide_mount : vec {} }; + // z prowadnicą stos liczony w układzie konstrukcji (fix na końcu), bez niej - pod rolką, w układzie urządzenia + auto const columnx { onbrackets ? guideaxis.x - toward.x * Device.ring_arm : fixedaxle.x - radius }; + auto const columny { onbrackets ? guideaxis.y - toward.y * Device.ring_arm : yb }; + vec const ropeend { columnx, columny, 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 ); + point( onbrackets ? onstructure( ropeend ) : ropeend ); + auto const stackfrom { Result.parts.size() }; place_chain( Device.hanger, "hanger", ropeend, down, Result ); { - auto placement { placed( "rod", rodname, vec { columnx, yb, s } ) }; + auto placement { placed( "rod", rodname, vec { columnx, columny, s } ) }; Result.parts.push_back( placement ); } auto z { s }; @@ -589,30 +1291,140 @@ bool solve_tensioner( tensioner_catalog const &Catalog, tensioner_device const & 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 placement { placed( "weight." + std::to_string( index ), model, vec { columnx, columny, 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" ); } + // pierścień prowadzący na stosie, prowadnica w osi jego przesuwu, w stronę konstrukcji + // obrót wokół osi stosu: -X modelu (w stronę prowadnicy) idzie w kierunku toward + auto const turned = [ & ]( std::string const &Role, std::string const &Part, vec const &At ) { + auto placement { placed( Role, Part, At ) }; + set_rotation( placement, toward * -1.0, vec { -toward.y, toward.x, 0.0 } * -1.0, vec { 0.0, 0.0, 1.0 } ); + return placement; + }; + auto const ringz { Device.ring_at < 0.0 ? z : s + std::min( Device.ring_at, z - s ) }; + // prowadnica tylko na uchwytach konstrukcji (bez nich nie ma jej gdzie przykręcić) - wtedy też bez pierścienia + if( onbrackets ) { + Result.parts.push_back( turned( "ring", Device.ring, vec { columnx, columny, ringz } ) ); + } + if( onbrackets ) { + auto const guide { vec { columnx, columny, Device.guide_bottom } + toward * ( Device.ring.empty() ? 0.0 : Device.ring_arm ) }; + Result.parts.push_back( turned( "guide", Device.guide, guide ) ); + Result.has_guide = true; + auto const at { guide + toward * Device.guide_mount }; + double const mount[ 3 ] { at.x, at.y, at.z }; + std::copy( mount, mount + 3, Result.guide_mount ); + std::copy( mount, mount + 3, Result.guide_end ); + Result.guide_end[ 2 ] += Device.guide_length; + if( mount[ 0 ] < 0.0 ) { Result.warnings.push_back( "the guide mounts are behind the face of the structure" ); } + // uchwyty prowadnicy (karta 33-0455): dolny poz. 2 przy spodzie, górny poz. 1 przy końcu; otwory w mount / end + // na ramach 5090: ramy w otworach uchwytów (środek słupa), stawia je i ich uchwyty jedno z dwóch urządzeń (owner) + if( onframe && Device.frame_owner ) { + for( auto const z : { Result.guide_mount[ 2 ], Result.guide_end[ 2 ] } ) { + auto placement { placed_along( z == Result.guide_mount[ 2 ] ? "frame_low" : "frame_high", Device.frame, vec { brackets->hole_out, 0.0, z }, out ) }; + // dolna rama odwrócona o 180 stopni wokół swojej długiej osi (Jan 2026-10-25), jak dolny uchwyt + if( z == Result.guide_mount[ 2 ] ) { + auto &r { placement.rotation }; + for( auto const column : { 1, 2 } ) { for( int row = 0; row < 3; ++row ) { r[ row * 3 + column ] = -r[ row * 3 + column ]; } } + } + Result.parts.push_back( placement ); + } + } + if( onbrackets && ( false == onframe || Device.frame_owner ) ) { + // dolny odwrócony o 180 stopni (karta 33-0455): otwór nad zerem + auto const y { onframe ? 0.0 : at.y }; + vec const low { 0.0, y, Result.guide_mount[ 2 ] - brackets->hole_drop }; + vec const high { 0.0, y, Result.guide_end[ 2 ] + brackets->hole_drop }; + front( "bracket_guide_low", brackets->bottom_front, low, true ); + back( "bracket_guide_low.back", brackets->bottom_back, low, true ); + front( "bracket_guide_high", brackets->top_front, high, false ); + back( "bracket_guide_high.back", brackets->top_back, high, false ); + } + if( false == Device.ring.empty() && ( ringz < Device.guide_bottom || ringz > Device.guide_bottom + Device.guide_length ) ) { + Result.warnings.push_back( "the guide ring is off the guide" ); + } + // mocowania prowadnicy w układzie urządzenia + for( auto *point : { Result.guide_mount, Result.guide_end } ) { + auto const p { onstructure( vec { point[ 0 ], point[ 1 ], point[ 2 ] } ) }; + point[ 0 ] = p.x; point[ 1 ] = p.y; point[ 2 ] = p.z; + } + } + if( onbrackets ) { fix( stackfrom ); } 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 ); +void tensioner_rope_solid( tensioner_result &Result, double const Radius, int const Sides, std::string const &Eye, double const EyeLength, std::vector &Triangles, + std::string const &Spacer, double const SpacerLength ) { + std::vector points; + for( auto const &point : Result.rope ) { + vec const p { point[ 0 ], point[ 1 ], point[ 2 ] }; + if( points.empty() || length( p - points.back() ) > 1e-6 ) { points.push_back( p ); } + } + if( points.size() < 2 || Radius <= 0.0 ) { return; } + // ucha: zaczep ("eye") w skrajnym punkcie liny, zero ucha w stronę liny; rura zaczyna się i kończy w zerze ucha + if( false == Eye.empty() && EyeLength > 1e-6 ) { + auto const eye = [ & ]( std::string const &Role, vec const &Joint, vec const &Toward ) { + auto const z { unit( Toward ) }; + auto const y { side_of( z ) }; + auto placement { placed( Role, Eye, Joint ) }; + // ucho obrócone o 90 stopni wokół liny względem pierwotnego (Jan 2026-10-25): płaszczyzna ucha pionowo + set_rotation( placement, y, cross( y, z ) * -1.0, z ); + Result.parts.push_back( placement ); + }; + auto const n { points.size() }; + auto const startdir { unit( points[ 1 ] - points[ 0 ] ) }; + auto const enddir { unit( points[ n - 1 ] - points[ n - 2 ] ) }; + auto const start { points[ 0 ] + startdir * std::min( EyeLength, length( points[ 1 ] - points[ 0 ] ) ) }; + auto const end { points[ n - 1 ] - enddir * std::min( EyeLength, length( points[ n - 1 ] - points[ n - 2 ] ) ) }; + eye( "rope_eye.1", start, startdir * -1.0 ); + eye( "rope_eye.2", end, enddir ); + // przekładka ślizgowa na sworzniu ucha przy pręcie: sworzeń wzdłuż X ucha (ucho płaskie w Y-Z modelu), w zaczepie + if( false == Spacer.empty() ) { + auto const z { unit( enddir ) }; + auto const pin { side_of( z ) }; // X ucha (obróconego o 90 stopni) + auto placement { placed( "rope_spacer", Spacer, points[ n - 1 ] - pin * ( 0.5 * SpacerLength ) ) }; + set_rotation( placement, z, cross( pin, z ), pin ); + Result.parts.push_back( placement ); + } + points.front() = start; + points.back() = end; + } + // pierścienie przekroju w każdym punkcie ścieżki, w płaszczyźnie dwusiecznej sąsiednich odcinków; oś odniesienia + // przekroju przenoszona z punktu do punktu bez skręcania (rura się nie skręca na łukach) + auto const count { points.size() }; + auto const sides { std::max( 3, Sides ) }; + std::vector along( count ), across( count ); + for( std::size_t k = 0; k < count; ++k ) { + auto const before { k > 0 ? unit( points[ k ] - points[ k - 1 ] ) : vec {} }; + auto const after { k + 1 < count ? unit( points[ k + 1 ] - points[ k ] ) : vec {} }; + along[ k ] = k == 0 ? after : k + 1 == count ? before : unit( before + after ); + auto const reference { k == 0 ? side_of( along[ 0 ] ) : across[ k - 1 ] }; + across[ k ] = unit( reference - along[ k ] * dot( reference, along[ k ] ) ); + } + auto const ring = [ & ]( std::size_t const Point, int const Side, vec &Position, vec &Normal ) { + auto const angle { 2.0 * kPi * ( Side % sides ) / sides }; + auto const u { across[ Point ] }; + auto const v { cross( along[ Point ], u ) }; + Normal = u * std::cos( angle ) + v * std::sin( angle ); + Position = points[ Point ] + Normal * Radius; + }; + auto const emit = [ & ]( vec const &Position, vec const &Normal ) { + Triangles.push_back( { { Position.x, Position.y, Position.z }, { Normal.x, Normal.y, Normal.z } } ); + }; + Triangles.reserve( Triangles.size() + ( count - 1 ) * sides * 6 ); + for( std::size_t k = 0; k + 1 < count; ++k ) { + for( int side = 0; side < sides; ++side ) { + vec p00, n00, p01, n01, p10, n10, p11, n11; + ring( k, side, p00, n00 ); + ring( k, side + 1, p01, n01 ); + ring( k + 1, side, p10, n10 ); + ring( k + 1, side + 1, p11, n11 ); + emit( p00, n00 ); emit( p11, n11 ); emit( p10, n10 ); + emit( p00, n00 ); emit( p01, n01 ); emit( p11, n11 ); + } } } diff --git a/world/catenary_tensioner.h b/world/catenary_tensioner.h index 0cd5fcef..36f8a0f4 100644 --- a/world/catenary_tensioner.h +++ b/world/catenary_tensioner.h @@ -26,13 +26,18 @@ http://mozilla.org/MPL/2.0/. // 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) +// zero w sworzniu od strony konstrukcji/dźwigni, biegną w -X do drugiego sworznia (pusty węzeł, +// np. hole_1 uchwytu 4440; długość z danych); "~część" w łańcuchu - postawiona odwrotnie (4440: zero +// w wąskim końcu, widełki z osią rolki na -X; przy rolce ruchomej widełki są od strony słupa) +// dźwignia 6406 zero w otworze osi II (mount_2 = III), otwory w +Z (I na dole), otwory na druty odsunięte w -X +// modelu (w stronę drutów); przy otworze liny poniżej otworu przewodów dźwignia odwrócona (180 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 +// prowadnica 5070 zero w osi przesuwu pierścienia na dole, w +Z; mocowania do wsporników w -X (w stronę konstrukcji) +// pierścień 5080 zero w osi stosu, poziomo, punkt przesuwu po prowadnicy w -X (w stronę konstrukcji); ring_at nad spodem stosu // lina linka rozciągana (część typu cord, zero w zaczepie, w +Z, skalowana wzdłuż Z) #pragma once +#include #include #include #include @@ -77,6 +82,9 @@ struct tensioner_pulley { double diameter { 0.220 }; // po dnie rowka double width { 0.080 }; int grooves { 2 }; + // krążek drugiego rowka jako osobny model (4460-11), stawiany w osi rolki; model rolki niesie wtedy oś i krążek + // pierwszego rowka. Oba obracają się osobno o przewiniętą linę; pusty = cała rolka jednym modelem, obracana z rowkiem A + std::string sheave; }; struct tensioner_lever { @@ -88,6 +96,7 @@ struct tensioner_lever { struct tensioner_link { std::string name; double length { 0.0 }; + double roll { 0.0 }; // stopnie wokół osi łańcucha (w danych "@"), np. 90 - sworzeń obrócony do następnej części }; // stos ciężarów i otwory dźwigni dla typu sieci (numer typu z karty 0002 = traction_code) @@ -95,6 +104,9 @@ struct tensioner_type { int code { -1 }; std::vector> stack; // od dołu: ciężar, liczba std::string messenger_hole, contact_hole, axis_hole; + // dźwignie szeregowo (2 liny + 2 przewody na jednym stosie): otwór przewodów pierwszej dźwigni ciągnie łańcuchem "cascade" + // drugą dźwignię za jej otwór osi; druga dzieli dalej na drugą linę nośną i przewody. Puste = jedna dźwignia + std::string second_messenger_hole, second_contact_hole, second_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 }; @@ -102,15 +114,88 @@ struct tensioner_type { struct tensioner_device { std::string name; // karta, np. 0461 std::string table, pulley, lever, rod, rope; + std::string lever2; // druga dźwignia (szeregowo), gdy inna niż pierwsza double rope_diameter { 0.010 }; + std::string rope_eye; // ucho (kausza) na obu końcach liny, nieskalowane; pusty = lina bez uch + double rope_eye_length { 0.0 }; // od zera ucha (styk z liną) do punktu "eye" (zaczep), wzdłuż +Z modelu + std::string rope_spacer; // przekładka na sworzniu ucha przy pręcie (6411, oś w +Z modelu); pusta = bez + double rope_spacer_length { 0.0 }; 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 + // dźwignie szeregowo: od otworu przewodów pierwszej do osi drugiej (z izolatorem) i od otworu liny drugiej do uchwytu drugiej + // liny nośnej; przewody ("contact") wtedy z drugiej dźwigni + std::vector cascade, messenger2; + // kotwienie stałe (kind fixed): bez rolek i ciężarów; na konstrukcji dwa przegubowe uchwyty kotwienia (pierwsza część "mount", + // 4430) - liny na wysokości liny nośnej, przewodów na wysokości przewodów, każdy w parze uchwytów poz. 1 (karta 43-0453). + // Za wyrównywaczem łańcuch każdego drutu osobno (…_each), dwie liny nośne przez messenger_equalizer (sieć 32, 35) + bool fixed { false }; + // rolki szeregowe (karty 0421, 0471, 0480): przy konstrukcji duża rolka (pulley) i za nią mała (small_pulley) o small_spacing + // dalej na uchwycie fixed_holder (4440-6), na wózku mała i duża (uchwyt 4440-5 = holder); lina od zaczepu na uchwycie stałym + // (dead_end od osi dużej) przez małą ruchomą, małą stałą, dużą ruchomą i dużą stałą w dół. b z tablicy = oś dużej ruchomej. + // Puste small_pulley = rolki równoległe (podwójne) + std::string small_pulley, fixed_holder; + // kotwienie rozdzielone: urządzenie tylko liny albo tylko przewodów (only), bez dźwigni - łańcuch wprost z uchwytu rolki + // ruchomej; dwa takie składa urządzenie "kind split" (split ). Prowadnica na ramach 5090 (frame + // [owner]): stos pod liną (Y 0), + // pierścień obrócony tak, żeby szekla trafiała w prowadnicę; owner stawia ramy i ich uchwyty + std::string only; + bool split { false }; + std::string split_messenger, split_contact; + std::string frame; + double frame_length { 0.0 }, frame_y { 0.0 }; + int frame_side { -1 }; + bool frame_owner { false }; + double messenger_spread { 0.08 }; // rozstaw dwóch lin za messenger_equalizer + bool messenger_equalizer_z { false }; // otwory messenger_equalizer w Z modelu (dźwignia 6403), a nie w Y (wyrównywacz 4410) + double small_spacing { 0.336 }, dead_end { 0.703 }; + std::vector messenger_each, contact_each; + tensioner_link messenger_equalizer; + // stare złożenia: izolator rolkowy ESN-2025 zamiast 7150 (Jan 2026-10-24: ESN-2025 - 4420 - [wyrównywacz] - uchwyt); + // puste = jak nowe + std::vector messenger_old, contact_old; tensioner_link messenger_clamp, contact_clamp; + tensioner_link contact_clamp_old; // uchwyt krańcowy przewodów w starych złożeniach (5110, stara karta 33-0401); pusty = jak nowe tensioner_link equalizer; // wyrównywacz przewodów jezdnych (tylko przy dwóch przewodach) double equalizer_spread { 0.08 }; // rozstaw przewodów za wyrównywaczem + // zabezpieczenie 4600 wyk. 1: dwa ramiona z osi rolek (stałej i ruchomej) zbiegające się na dolnej śrubie, para po obu stronach rolek + std::string safety_fixed, safety_moving; + double safety_arm { 1.0 }; // rozstaw otworów ramienia + double safety_offset { 0.06 }; // odsunięcie każdej pary od środka rolki w bok + // zabezpieczenie 4600 wyk. 2 (rolki szeregowe, karta 13-0421 poz. 10): ramiona z osi małych rolek (stałej i ruchomej) w dół, + // na końcach drążek; puste = bez niego. safety_series + std::string series_arm_fixed, series_arm_moving, series_bar; + double series_arm { 0.46268 }, series_bar_length { 1.30 }; + // tytuł do wyboru w edytorze (karta z odmianą widoczna na liście mimo "-" w nazwie); puste = bez + std::string title; + // prowadnica ciężarów (5070, karta 33-0455) i pierścień prowadzący na wierzchu stosu (5080). Model prowadnicy: zero w osi + // przesuwu pierścienia na dole, w +Z (długość), punkty "mount"/"end" mocowań do wsporników o guide_mount w -X (w stronę + // konstrukcji). Pierścień: zero w osi stosu, poziomo, "mount" o ring_arm w -X (w osi przesuwu po prowadnicy); pusty = bez nich + std::string guide, ring; + double guide_length { 4.50 }; + double guide_mount { 0.07 }; + double guide_bottom { 0.80 }; // spód prowadnicy nad główką szyny (karta 23-0018-2: 0,80, prowadnica 4,50 do 5,30) + double ring_arm { 0.200 }; // od osi stosu do osi szekli, która chodzi po prowadnicy (karta 23-5080 wyk. 1: 200) + double ring_turn { 0.0 }; // stopnie wokół osi stosu: 0 = prowadnica w stronę konstrukcji, 90 = w bok (+Y) + double ring_at { 1.00 }; // wysokość pierścienia nad spodem stosu (karta 23-0018-2: 1,00 m); ujemna = na wierzchu stosu + // uchwyty do konstrukcji (karta 33-0455): para przód + tył ściśnięta śrubami przez słup, na ścianach wzdłuż drutów (obrócone + // o 90 stopni względem wysięgnika; rozstaw śrub obejmuje słup w poprzek). Przód: zero na czole konstrukcji, śruby w +X modelu + // (w głąb), hole_1 o hole_out przed czołem i hole_drop pod zerem. Tył: ten sam układ, po drugiej stronie. + // Uchwyt przegubowy 4450 (pierwsza część "mount") trzymają dwa: górny wprost za mount_top, dolny odwrócony o 180 stopni za + // mount_bottom (pivot_top / pivot_bottom nad / pod zerem 4450) + struct bracket_set { + std::vector poles; // przedrostki nazw słupów, np. 1611-11 (najdłuższy pasujący wygrywa) + double hole_out { 0.04 }, hole_drop { 0.04 }, bolt { 0.217 }; + std::string top_front, top_back, bottom_front, bottom_back; + }; + std::vector brackets; + double pivot_top { 0.0 }, pivot_bottom { 0.0 }; + // otwór na końcu uchwytu (oś stałej rolki) nad jego zerem: uchwyt schodzi o tyle, żeby rolka została na swojej wysokości + // (5040: hole_1 0,03 nad mount_bottom; wpis "pivot [rise ]") + double mount_rise { 0.0 }; + bracket_set const *brackets_for( std::string const &Pole ) const; std::vector types; tensioner_type const *type( int Code ) const; }; @@ -126,6 +211,8 @@ public: 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) + // numer typu sieci dla urządzeń: sieć bez własnych danych bierze dane innej (wpis "network as "), inaczej jej kod + int code_for( std::string const &Symbol, int Code ) const; private: std::vector m_tables; @@ -134,19 +221,45 @@ private: std::vector m_levers; std::vector m_devices; std::map m_lengths; + std::map m_networks; }; struct tensioner_input { int type { 3 }; // numer typu sieci (traction_code) int contacts { 2 }; // liczba przewodów jezdnych + int messengers { 1 }; // liczba lin nośnych (kotwienie stałe sieci 32, 35: dwie) + double messenger_stack { 0.04 }; // odstęp pionowy dwóch lin nośnych w przęśle (druga celuje o tyle niżej) 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) + // sieć do skrócenia przy naciągu mniejszym niż od pełnego stosu (brakujące ciężary): sztywności EA wszystkich lin + // nośnych i wszystkich przewodów jezdnych (daN), ciężar sieci na metr wiszący na linie (daN/m) i średnia rozpiętość + // przęseł (m). Lina i przewody skracają się sprężyście, a lina dodatkowo zwisa bardziej, więc urządzenie ściąga je ku + // przęsłom: rolka dalej od konstrukcji, ciężary wyżej. Sztywność 0 = bez tego (tablica jak przy pełnym stosie) + double messenger_stiffness { 0.0 }, contact_stiffness { 0.0 }; + double load { 0.0 }; + double span { 0.0 }; std::string color; // kolor ciężarów (tensioner_color); pusty = szary std::string top_color; // kolor najwyższego ciężaru; pusty = jak reszta + bool old_fittings { false }; // uchwyty krańcowe przewodów 5110 (druty zaśniedziałe) + bool old_messenger { false }, old_contact { false }; // izolatory rolkowe (łańcuchy messenger_old / contact_old) na linie / przewodach + // punkty, w które celują łańcuchy do liny nośnej i do przewodów jezdnych (ostatnie podwieszenie, układ urządzenia); + // bez nich łańcuchy poziomo wzdłuż +X + bool aimed { false }; + double messenger_target[ 3 ] { 0.0, 0.0, 0.0 }; + double contact_target[ 3 ] { 0.0, 0.0, 0.0 }; // stare złożenie (druty zaśniedziałe): uchwyty krańcowe przewodów 5110 zamiast 5120 + double height { 0.0 }; // oś rolek nad główką szyny; 0 = z tablicy. Wyżej/niżej: ciężary o tyle samo (lina ta sama) + double rope_height { 0.0 }; // kotwienie stałe: uchwyt liny nośnej nad główką; 0 = jak drut przy ostatnim podwieszeniu 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 + // konstrukcja: nazwa słupa (dobór uchwytów) i jej grubość od czoła do tyłu (wzdłuż drutów; słup 161x - szerokość wzdłuż toru, + // przez nią idą śruby uchwytów) na wysokości nad główką; puste = bez uchwytów + std::string pole; + std::function depth; + // kąt osi konstrukcji (od czoła w stronę kotwionego przęsła) w układzie urządzenia, radiany wokół +Z: urządzenie od przegubu + // 4450 ciągnie do drutów, a uchwyty, prowadnica, pierścień i stos zostają przy konstrukcji (0 = konstrukcja w linii drutów) + double structure_angle { 0.0 }; }; struct tensioner_placement { @@ -162,19 +275,46 @@ struct tensioner_result { 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 }; + double height { 0.0 }; // oś rolek nad główką szyny + double lever_tilt { 0.0 }; // radiany: odchylenie uchwytu dźwigni od poziomu w dół (dźwignia od pionu w stronę konstrukcji u góry) + // obrót krążków (radiany wokół +Y urządzenia) od położenia przy +10 °C: lina przewinięta przez rowek / promień + double turn_fixed_a { 0.0 }, turn_fixed_b { 0.0 }, turn_moving_a { 0.0 }, turn_moving_b { 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 nominal_tension { 0.0 }; // daN, naciąg od pełnego stosu (jak w tablicy) + double shortening { 0.0 }; // m, o ile rolka odjechała od tablicy przez mniejszy naciąg (sprężystość i zwis) 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> rope2; // kotwienie rozdzielone: lina urządzenia przewodów (rope - urządzenia liny) + // dźwignie szeregowo: druga lina nośna (z drugiej dźwigni) i naciągi obu lin osobno (messenger_tension to ich suma) + bool cascade { false }; + bool fixed { false }; // kotwienie stałe (bez ciężarów; b, s, naciąg zero) + double rope_height { 0.0 }; // kotwienie stałe: wysokość uchwytu liny nośnej (height - przewodów) + double messenger2_end[ 3 ] { 0.0, 0.0, 0.0 }; // druga lina nośna: z drugiej dźwigni albo za wyrównywaczem kotwienia stałego + double messenger_tensions[ 2 ] { 0.0, 0.0 }; 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 + // mocowania prowadnicy do wsporników na konstrukcji (dolne i górne, układ urządzenia); has_guide = false bez prowadnicy + bool has_guide { false }; + double guide_mount[ 3 ] { 0.0, 0.0, 0.0 }, guide_end[ 3 ] { 0.0, 0.0, 0.0 }; 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 ); +// wierzchołek bryły proceduralnej w układzie urządzenia; kolejne trójki to trójkąty, obchodzone przeciwnie do ruchu +// wskazówek zegara patrząc z zewnątrz (w prawoskrętnym układzie urządzenia) +struct tensioner_vertex { + double position[ 3 ]; + double normal[ 3 ]; +}; + +// lina jako jedna bryła proceduralna: rura o promieniu Radius i Sides ścianach po całej ścieżce Result.rope (łuki na +// rolkach co 5°), bez łączeń. Eye (gdy niepusty): ucho o długości EyeLength (zero w styku z liną, punkt "eye" w +Z w +// zaczepie) na obu końcach, dopisywane do Result.parts; rura skrócona o ucha +// Spacer: przekładka ślizgowa (6411) na sworzniu ucha przy pręcie ciężarów (koniec liny), oś wzdłuż sworznia; pusta = bez +void tensioner_rope_solid( tensioner_result &Result, double Radius, int Sides, std::string const &Eye, double EyeLength, std::vector &Triangles, + std::string const &Spacer = {}, double SpacerLength = 0.0 ); } // namespace catenary