diff --git a/CMakeLists.txt b/CMakeLists.txt index 37f5c4ae..e2fa3abd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ set(SOURCES "utilities/Timer.cpp" "world/Track.cpp" "world/Road.cpp" +"world/Sweep.cpp" "world/RoadPoint.cpp" "world/Traction.cpp" "world/TractionPower.cpp" diff --git a/application/drivermode.cpp b/application/drivermode.cpp index dbe3e176..a79c31b0 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -255,6 +255,19 @@ bool driver_mode::update() // variable step simulation time routines + if (false == Global.editor_enter_vehicle.empty()) + { + auto const name{std::move(Global.editor_enter_vehicle)}; + Global.editor_enter_vehicle.clear(); + if (auto *dynamic{simulation::Vehicles.find(name)}) + { + if (false == FreeFlyModeFlag) + InOutKey(); + m_relay.post(user_command::entervehicle, 0.0, simulation::Train ? simulation::Train->id() : 0, GLFW_PRESS, 0, dynamic->GetPosition(), &dynamic->name()); + change_train = dynamic->name(); + } + } + if (!change_train.empty()) { TTrain *train = simulation::Trains.find(change_train); diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index 77f69a35..a24d3cf9 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -119,6 +119,10 @@ void ui::scenerylist_panel::draw_launch_box() ImGui::TextWrapped(selected_trainset->description.c_str()); + ImGui::Checkbox(STR_C("Trainset turned around"), &selected_trainset->reversed); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The first vehicle stays where the trainset starts and leads the other way,\nthe rest of the trainset stands behind it")); + if (ImGui::Button(STR_C("Launch"), ImVec2(-1, 0))) { if (!launch_simulation()) ImGui::OpenPopup("missing_driver"); @@ -163,7 +167,7 @@ void ui::scenerylist_panel::add_replace_entry(const trainset_desc &trainset) set += trainset.name + " "; set += trainset.track + " "; set += std::to_string(trainset.offset) + " "; - set += std::to_string(trainset.velocity) + "\n"; + set += std::to_string(trainset.velocity) + (trainset.reversed ? " reversed\n" : "\n"); for (const auto &veh : trainset.vehicles) { if (!veh.skin) { ErrorLog("trainset contains invalid vehicle " + veh.name); diff --git a/launcher/scenery_scanner.cpp b/launcher/scenery_scanner.cpp index 7bfa8a07..bcd18ac9 100644 --- a/launcher/scenery_scanner.cpp +++ b/launcher/scenery_scanner.cpp @@ -95,6 +95,10 @@ void scenery_scanner::parse_trainset(cParser &parser) parser >> trainset.name >> trainset.track >> trainset.offset >> trainset.velocity; parser.getTokens(); + if (parser.peek() == "reversed") { + trainset.reversed = true; + parser.getTokens(); + } while (parser.peek() == "node") { trainset.vehicles.emplace_back(); dynamic_desc &dyn = trainset.vehicles.back(); diff --git a/launcher/scenery_scanner.h b/launcher/scenery_scanner.h index 44854438..fcda27da 100644 --- a/launcher/scenery_scanner.h +++ b/launcher/scenery_scanner.h @@ -30,6 +30,7 @@ struct trainset_desc { float offset { 0.f }; float velocity { 0.f }; + bool reversed { false }; std::vector vehicles; }; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b146d5d1..ff0e9951 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -5154,6 +5154,7 @@ glm::dvec3 opengl33_renderer::Update_Mouse_Position() if (pointdepth != std::numeric_limits::max()) { + m_mousehit = (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) ? pointdepth > 0.0f : pointdepth < 1.0f; if (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) { if (pointdepth > 0.0f) m_worldmousecoordinates = glm::unProjectZO(glm::vec3(bufferpos, pointdepth), glm::mat4(glm::mat3(m_colorpass.pass_camera.modelview())), m_colorpass.pass_camera.projection(), diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 506e9211..36c8417c 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -105,6 +105,8 @@ class opengl33_renderer : public gfx_renderer { Pick_Node() const override { return m_picksceneryitem; } glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } + bool + Mouse_Hit() const override { return m_mousehit; } glm::mat4 Camera_View_Matrix() const override { return glm::mat4( glm::mat3( m_colorpass.pass_camera.modelview() ) ); } glm::mat4 @@ -387,6 +389,7 @@ class opengl33_renderer : public gfx_renderer { std::vector m_picksceneryitems; scene::basic_node *m_picksceneryitem{nullptr}; glm::vec3 m_worldmousecoordinates { 0.f }; + bool m_mousehit { false }; #ifdef EU07_USE_DEBUG_CAMERA renderpass_config m_worldcamera; // debug item #endif diff --git a/rendering/renderer.h b/rendering/renderer.h index f7b6d762..225e26c0 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -73,6 +73,7 @@ public: virtual auto Pick_Node() const -> scene::basic_node const * = 0; virtual auto Mouse_Position() const -> glm::dvec3 = 0; + virtual auto Mouse_Hit() const -> bool { return true; } // editor helpers: matrices/position of the most recent color pass camera. // the view matrix is camera-relative (rotation only, camera at origin), matching the // camera-relative rendering used by the engine; build object matrices relative to Camera_Position(). diff --git a/scene/scene.h b/scene/scene.h index 74b3ba4c..6b1ba175 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -69,7 +69,9 @@ struct scratch_data { std::vector vehicles; std::vector couplings; TDynamicObject * driver { nullptr }; + TTrack * path { nullptr }; // given directly, for a trainset placed in the editor on a path which may have no name bool is_open { false }; + bool reversed { false }; std::unordered_map assignment; } trainset; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 98da1f12..7751a860 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "world/Track.h" #include "world/Road.h" +#include "world/Sweep.h" #include "world/RoadPoint.h" #include "simulation/simulation.h" @@ -231,7 +232,7 @@ bool patch_model(std::string &Text, model_placement const &Placement, std::strin } // definition layout: node memcell endmemcell -bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &Error) +bool patch_memcell(std::string &Text, std::optional const &Location, std::string const &Track, std::string &Error) { auto const tokens{tokenize(Text)}; if (tokens.size() < 13 || tokens[0].text != "node" || tokens[4].text != "memcell" || tokens.back().text != "endmemcell") @@ -239,7 +240,13 @@ bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &E Error = "unexpected layout of memory cell definition"; return false; } - apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}}); + std::vector changes; + if (Location) + changes.insert(changes.end(), {{tokens[5].begin, tokens[5].end, number(Location->x)}, {tokens[6].begin, tokens[6].end, number(Location->y)}, {tokens[7].begin, tokens[7].end, number(Location->z)}}); + auto const &track{tokens[tokens.size() - 2]}; + if (false == Track.empty() && track.text != Track) + changes.push_back({track.begin, track.end, Track}); + apply(Text, changes); return true; } @@ -699,7 +706,9 @@ save_result node_layers::save(std::vector const &Rootstatements) auto const rotated{false == glm::all(glm::epsilonEqual(angles, source.angles, 1e-3f))}; auto const scaled{false == glm::all(glm::epsilonEqual(scale, source.scale, 1e-4f))}; auto const renamed{Model != nullptr && m_renamed.find(Node) != m_renamed.end()}; - if (false == (moved || rotated || scaled || renamed)) + auto const *cell{Model == nullptr ? static_cast(Node) : nullptr}; + auto const track{cell != nullptr && cell->Track != nullptr ? cell->Track->name() : std::string{}}; + if (false == (moved || rotated || scaled || renamed || (false == track.empty() && Node->dirty()))) { return true; } @@ -739,13 +748,17 @@ save_result node_layers::save(std::vector const &Rootstatements) } else { - patched = patch_memcell(text, source.context.to_local(location), error); + patched = patch_memcell(text, moved ? std::optional{source.context.to_local(location)} : std::nullopt, track, error); } if (false == patched) { state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; return false; } + if (text.size() == static_cast(source.span.end - source.span.begin) && content.compare(static_cast(source.span.begin), text.size(), text) == 0) + { + return true; + } file_patch::edit edit; edit.begin = source.span.begin; edit.end = source.span.end; @@ -988,6 +1001,72 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[source.layer].edits.emplace_back(std::move(edit)); rewritten.emplace(road); } + // models laid along curves, likewise + std::vector savedsweeps; + std::vector droppedsweeps; + auto const sweep_text = [](sweep_node &Sweep, glm::dvec3 const &Offset) { + auto const definition{Sweep.definition()}; + auto local{definition}; + for (auto &piece : local.pieces) + { + piece.points[segment_data::point::start] -= Offset; + piece.points[segment_data::point::end] -= Offset; + } + Sweep.define(local); + std::string text; + Sweep.export_as_text(text); + Sweep.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->from_template()) + { + continue; + } + auto const nodelayer{resolve(sweep->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(sweep)}; + if (lookup == m_sources.end()) + { + if (sweep->m_editorremoved || false == sweep->dirty()) + { + continue; + } + created[nodelayer].emplace_back(sweep, sweep_text(*sweep, layer(nodelayer).context_insert().offset)); + savedsweeps.push_back(sweep); + continue; + } + auto const &source{lookup->second}; + if ((false == sweep->m_editorremoved && false == sweep->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (sweep->m_editorremoved) + { + droppedsweeps.push_back(sweep); + } + else + { + edit.text = sweep_text(*sweep, source.context.offset); + edit.length = edit.text.size(); + edit.node = sweep; + savedsweeps.push_back(sweep); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(sweep); + } // road junctions, likewise std::vector savedjunctions; std::vector droppedjunctions; @@ -1647,6 +1726,14 @@ save_result node_layers::save(std::vector const &Rootstatements) { m_sources.erase(road); } + for (auto *sweep : savedsweeps) + { + sweep->m_dirty = false; + } + for (auto const *sweep : droppedsweeps) + { + m_sources.erase(sweep); + } for (auto *junction : savedjunctions) { junction->m_dirty = false; diff --git a/scene/scenenode.h b/scene/scenenode.h index dd844a65..b7fa9fda 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -149,6 +149,9 @@ public: // set lighting (used by generated geometry painted with plain colour) void lighting( lighting_data const &Lighting ); + // sets whether the shape is drawn with the translucent geometry (used by generated geometry) + void + translucent( bool const Translucent ); // data access shapenode_data const & data() const; @@ -186,6 +189,11 @@ void shape_node::lighting( lighting_data const &Lighting ) { m_data.lighting = Lighting; } +inline +void +shape_node::translucent( bool const Translucent ) { + m_data.translucent = Translucent; +} // data access inline shape_node::shapenode_data const & diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 035a4f37..3cbe6c69 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "world/MemCell.h" #include "world/Track.h" #include "world/Road.h" +#include "world/Sweep.h" #include "world/RoadPoint.h" #include "world/Traction.h" #include "world/TractionPower.h" @@ -39,6 +40,7 @@ event_manager Events; memory_table Memory; path_table Paths; road_table Roads; +sweep_table Sweeps; junction_table Junctions; roadpoint_table Roadpoints; traction_table Traction; @@ -497,6 +499,10 @@ TAnimModel * state_manager::create_model(const std::string &src, const std::stri return m_serializer.create_model(src, name, position); } +std::vector state_manager::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) { + return m_serializer.insert_trainset(Name, Path, Offset, Vehicles, Reversed); +} + TEventLauncher * state_manager::create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position) { return m_serializer.create_eventlauncher(src, name, position); } diff --git a/simulation/simulation.h b/simulation/simulation.h index 89337583..0e96da68 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -40,6 +40,9 @@ public: // create model from node string TAnimModel * create_model(const std::string &src, const std::string &name, const glm::dvec3 &position); + // places the vehicles, given as their node statements, as a trainset on the path. returns: the vehicles made + std::vector + insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false); // create eventlauncher from node string TEventLauncher * create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position); @@ -86,6 +89,7 @@ extern event_manager Events; extern memory_table Memory; extern path_table Paths; extern road_table Roads; +extern sweep_table Sweeps; extern junction_table Junctions; extern roadpoint_table Roadpoints; extern traction_table Traction; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 8736e56d..fe5486e8 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -28,6 +28,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/lightarray.h" #include "world/TractionPower.h" #include "world/Road.h" +#include "world/Sweep.h" #include "world/RoadPoint.h" #include "application/application.h" #include "rendering/renderer.h" @@ -35,8 +36,19 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include "editor/editorTerrainStreamer.hpp" + namespace simulation { +namespace { + +double vehicle_length( std::string const &Folder, std::string const &Type ) { + + TMoverParameters probe( 0.0, Type, Type, 0 ); + return probe.LoadFIZ( paths::dynamic + Folder + "/" ) ? probe.Dim.L : 0.0; +} + +} // namespace + std::shared_ptr state_serializer::deserialize_begin( std::string const &Scenariofile ) { @@ -123,7 +135,8 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { { "trainset", &state_serializer::deserialize_trainset }, { "terrain", &state_serializer::deserialize_terrain }, { "editorterrain", &state_serializer::deserialize_editorterrain }, - { "endtrainset", &state_serializer::deserialize_endtrainset } }; + { "endtrainset", &state_serializer::deserialize_endtrainset }, + { "reversed", &state_serializer::deserialize_reversed } }; for( auto &function : functionlist ) { state->functionmap.emplace( function.first, std::bind( function.second, this, std::ref( state->input ), std::ref( state->scratchpad ) ) ); @@ -191,6 +204,7 @@ state_serializer::deserialize_continue(std::shared_ptr state simulation::Roads.create_geometry( Scratchpad ); simulation::Junctions.create_geometry( Scratchpad ); simulation::Roadpoints.create_geometry(); + simulation::Sweeps.create_geometry( Scratchpad ); return false; } @@ -554,6 +568,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch road->create_lanes(); scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } ); } + else if( nodedata.type == "sweep" ) { + + auto *sweep { new sweep_node( nodedata ) }; + sweep->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Sweeps.insert( sweep ) ) { + ErrorLog( "Bad scenario: duplicate sweep name \"" + sweep->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), sweep ); + scene::Layers.track( sweep, { sourcebegin, Input.TokenEnd() } ); + } else if( nodedata.type == "junction" ) { auto *junction { new junction_node( nodedata ) }; @@ -1224,6 +1252,17 @@ include_terrain_file( std::string File, std::string &Includes, scene::scratch_da } // namespace +void +state_serializer::deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad ) { + + if( false == Scratchpad.trainset.is_open + || false == Scratchpad.trainset.vehicles.empty() ) { + ErrorLog( "Bad trainset: \"reversed\" has to follow the trainset header, ahead of its vehicles, in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); + return; + } + Scratchpad.trainset.reversed = true; +} + void state_serializer::deserialize_terrain(cParser &Input, scene::scratch_data &Scratchpad) { @@ -1312,9 +1351,16 @@ state_serializer::deserialize_endtrainset( cParser &Input, scene::scratch_data & } if( vehicleindex > 0 ) { // from second vehicle on couple it with the previous one - Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext( - vehicle, - Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + if( Scratchpad.trainset.reversed ) { + vehicle->AttachNext( + Scratchpad.trainset.vehicles[ vehicleindex - 1 ], + Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + } + else { + Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext( + vehicle, + Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + } } ++vehicleindex; } @@ -1494,7 +1540,7 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra loadtype = ""; } - auto *path = simulation::Paths.find( pathname ); + auto *path = Scratchpad.trainset.path != nullptr ? Scratchpad.trainset.path : simulation::Paths.find( pathname ); if( path == nullptr ) { ErrorLog( "Bad scenario: vehicle \"" + Nodedata.name + "\" placed on nonexistent path \"" + pathname + "\" in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); @@ -1502,7 +1548,9 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra return nullptr; } - if( true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd, + auto const reversedset { Scratchpad.trainset.is_open && Scratchpad.trainset.reversed }; + if( false == reversedset + && true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd, && false == path->m_events0.empty() // tor ma Event0 && std::abs(velocity) <= 1.f // a skład stoi && Scratchpad.trainset.offset >= 0.0 // ale może nie sięgać na owy tor @@ -1512,23 +1560,34 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra } auto *vehicle = new TDynamicObject(); - + + auto const gap { offset == -1.0 ? 0.0 : offset }; + auto const turned { ( offset == -1.0 ) != reversedset }; + auto const expected { reversedset ? vehicle_length( datafolder, mmdfile ) : 0.0 }; auto const length = vehicle->Init( Nodedata.name, datafolder, skinfile, mmdfile, path, - offset == -1.0 ? Scratchpad.trainset.offset : Scratchpad.trainset.offset - offset, + reversedset ? Scratchpad.trainset.offset + gap + expected : Scratchpad.trainset.offset - gap, drivertype, velocity, Scratchpad.trainset.name, loadcount, loadtype, - offset == -1.0, + turned, params ); + if( length != 0.0 && reversedset ) { + if( std::abs( length - expected ) > 0.01 ) { + vehicle->place_on_track( path, Scratchpad.trainset.offset + gap + length, turned ); + } + Scratchpad.trainset.offset += length; + } if( length != 0.0 ) { // zero oznacza błąd // przesunięcie dla kolejnego, minus bo idziemy w stronę punktu 1 - Scratchpad.trainset.offset -= length; + if( false == reversedset ) { + Scratchpad.trainset.offset -= length; + } // automatically establish permanent connections for couplers which specify them in their definitions if( coupling != 0 && vehicle->MoverParameters->Couplers[(offset == -1.0 ? end::front : end::rear)].AllowedFlag & coupling::permanent ) { @@ -1774,6 +1833,11 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons point->export_as_text( scmfile ); } } + for( auto const *sweep : Sweeps.sequence() ) { + if( sweep != nullptr && false == sweep->m_editorremoved && sweep->dirty() == Dirty && sweep->group() == null_handle ) { + sweep->export_as_text( scmfile ); + } + } // traction scmfile << "// traction\n"; for( auto const *traction : Traction.sequence() ) { @@ -1826,6 +1890,25 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st return cloned; } +std::vector state_serializer::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) { + scene::scratch_data scratch; + scratch.trainset.is_open = true; + scratch.trainset.name = Name; + scratch.trainset.track = Path != nullptr ? Path->name() : std::string{}; + scratch.trainset.path = Path; + scratch.trainset.offset = static_cast(Offset); + scratch.trainset.reversed = Reversed; + cParser parser(Vehicles, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction); + auto token { parser.getToken() }; + while (false == token.empty()) { + if (token == "node") { deserialize_node(parser, scratch); } + token = parser.getToken(); + } + auto const vehicles { scratch.trainset.vehicles }; + if (false == vehicles.empty()) { deserialize_endtrainset(parser, scratch); } + return vehicles; +} + std::pair state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { // statements which take more than a single token, with the tokens ending them static std::unordered_map const nodeends { diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index 001f6ad3..a5cbc32f 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -44,6 +44,8 @@ public: export_as_text( std::string const &Scenariofile ) const; // create new model from node stirng TAnimModel * create_model(std::string const &src, std::string const &name, const glm::dvec3 &position); + // places the vehicles, given as their node statements, as a trainset on the path, while the simulation runs. returns: the vehicles made + std::vector insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false); // create new eventlauncher from node stirng TEventLauncher * create_eventlauncher(std::string const &src, std::string const &name, const glm::dvec3 &position); // creates model instances defined by an include directive placed or changed in the scenery editor, to show the outcome. @@ -80,6 +82,7 @@ private: void deserialize_terrain( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_editorterrain( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_endtrainset( cParser &Input, scene::scratch_data &Scratchpad ); + void deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad ); TTrack * deserialize_path( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TTraction * deserialize_traction( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TTractionPowerSource * deserialize_tractionpowersource( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); diff --git a/utilities/Classes.h b/utilities/Classes.h index ec60965c..6a5ca167 100644 --- a/utilities/Classes.h +++ b/utilities/Classes.h @@ -45,6 +45,8 @@ struct light_array; class particle_manager; class road_node; // kawałek drogi, z którego powstają pasy ruchu class road_table; +class sweep_node; // model ułożony wzdłuż krzywej: wygięty albo powtarzany +class sweep_table; class junction_node; class junction_table; class roadpoint_node; // przejazd kolejowy albo miejsce, w którym pojazdy drogowe się pojawiają lub znikają diff --git a/utilities/Globals.h b/utilities/Globals.h index acf0c00f..36574ab1 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -84,6 +84,7 @@ struct global_settings { std::string szDefaultExt{ szTexturesDDS }; std::string SceneryFile; std::string local_start_vehicle{ "EU07-424" }; + std::string editor_enter_vehicle; // vehicle placed in the editor, to be entered when the driving goes on bool editor_session{ false }; // scenery was opened for editing (-edit): loader keeps track of scenery layers and hands over to the editor int iConvertModels{ 0 }; // tworzenie plików binarnych int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan diff --git a/world/Event.cpp b/world/Event.cpp index 4e042b29..fa752078 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -1243,6 +1243,26 @@ multi_event::init() { } } +std::vector multi_event::children() const { + std::vector result; + for (auto const &childevent : m_children) { + if (std::get(childevent) != nullptr) { + result.push_back(std::get(childevent)); + } + } + return result; +} + +std::vector basic_event::target_nodes() const { + std::vector result; + for (auto const &target : m_targets) { + if (std::get(target) != nullptr) { + result.push_back(std::get(target)); + } + } + return result; +} + std::vector multi_event::dump_children_names() const { std::vector result; for (auto const &childevent : m_children) { diff --git a/world/Event.h b/world/Event.h index 9b8f32d8..69d66e3c 100644 --- a/world/Event.h +++ b/world/Event.h @@ -47,6 +47,7 @@ public: // destructor virtual ~basic_event(); // methods + std::vector target_nodes() const; // restores event data from provided stream virtual void @@ -334,6 +335,7 @@ public: void init() override; std::vector dump_children_names() const; + std::vector children() const; private: // types @@ -699,6 +701,9 @@ public: // returns all eventlaunchers in radius ignoring height std::vector find_eventlaunchers(glm::vec2 center, float radius) const; + std::deque const & + sequence() const { + return m_events; } // all event launchers, global ones included std::vector launchers() const { diff --git a/world/Sweep.cpp b/world/Sweep.cpp new file mode 100644 index 00000000..ef3af81b --- /dev/null +++ b/world/Sweep.cpp @@ -0,0 +1,907 @@ +/* +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/Sweep.h" +#include "editor/editorGauge.hpp" + +#include "model/MdlMngr.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "vehicle/DynObj.h" + +#include +#include + +#include +#include +#include +#include + +namespace +{ + +double const sample_spacing{0.5}; // m along the curve between the samples +std::size_t const vertex_limit{4000000}; // copies beyond this many vertices are left out +double const slice_width{1.0}; // m of the model, the faces of a bent copy are cut into +double const edge_band{0.3}; // m from the side facing the curve, where the top of the edge is looked for + +struct cubic +{ + glm::dvec3 p0, p1, p2, p3; + + explicit cubic(segment_data const &Piece) + { + p0 = Piece.points[segment_data::point::start]; + p3 = Piece.points[segment_data::point::end]; + auto const &control1{Piece.points[segment_data::point::control1]}; + auto const &control2{Piece.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + p1 = glm::mix(p0, p3, 1.0 / 3.0); + p2 = glm::mix(p0, p3, 2.0 / 3.0); + } + else + { + p1 = p0 + control1; + p2 = p3 + control2; + } + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + } + glm::dvec3 first(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + } +}; + +double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return glm::length(glm::dvec2{B.x - A.x, B.z - A.z}); +} + +// triangles of the model which share the look, in the space of the model +struct model_part +{ + material_handle material{null_handle}; + lighting_data lighting; + bool translucent{false}; + std::vector vertices; +}; + +void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_data const &Skins, std::vector &Parts) +{ + for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->Next) + { + auto transform{Transform}; + if ((submodel->iFlags & 0xC000) != 0 && submodel->GetMatrix() != nullptr) + { + transform = Transform * glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray())); + } + // the nearest level of detail only, the others would be drawn over it + if (submodel->eType == GL_TRIANGLES && submodel->fSquareMinDist <= 0.f && (submodel->m_geometry.handle.bank != 0 || submodel->m_geometry.handle.chunk != 0)) + { + auto material{submodel->m_material}; + if (material < 0) + { + material = Skins.replacable_skins[std::clamp(-material, 1, 4)]; + } + model_part part; + part.material = material; + part.lighting.ambient = submodel->f4Ambient; + part.lighting.diffuse = submodel->f4Diffuse; + part.lighting.specular = submodel->f4Specular; + part.translucent = material != null_handle && GfxRenderer->Material(material)->get_or_guess_opacity() == 0.0f; + auto const normals{glm::dmat3(transform)}; + auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)}; + auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)}; + auto const take = [&](gfx::basic_vertex const &Vertex) { + auto vertex{Vertex.to_world()}; + vertex.position = glm::dvec3(transform * glm::dvec4(vertex.position, 1.0)); + auto const normal{normals * glm::dvec3(vertex.normal)}; + vertex.normal = glm::length(normal) > 1e-9 ? glm::vec3(glm::normalize(normal)) : vertex.normal; + part.vertices.push_back(vertex); + }; + if (false == indices.empty()) + { + for (std::size_t i = 0; i + 2 < indices.size(); i += 3) + { + take(vertices[indices[i]]); + take(vertices[indices[i + 1]]); + take(vertices[indices[i + 2]]); + } + } + else + { + for (std::size_t i = 0; i + 2 < vertices.size(); i += 3) + { + take(vertices[i]); + take(vertices[i + 1]); + take(vertices[i + 2]); + } + } + if (false == part.vertices.empty()) + { + Parts.push_back(std::move(part)); + } + } + if (submodel->Child != nullptr) + { + gather_parts(submodel->Child, transform, Skins, Parts); + } + } +} + +world_vertex blend(world_vertex const &A, world_vertex const &B, double const T) +{ + world_vertex result; + result.position = glm::mix(A.position, B.position, T); + auto const normal{glm::mix(A.normal, B.normal, static_cast(T))}; + result.normal = glm::length(normal) > 1e-6f ? glm::normalize(normal) : A.normal; + result.texture = glm::mix(A.texture, B.texture, static_cast(T)); + return result; +} + +// the part of the polygon on one side of the plane Along = Cut: Side 1 keeps what's beyond it, -1 what's before it +std::vector clip(std::vector const &Polygon, std::function const &Along, double const Cut, double const Side) +{ + std::vector result; + for (std::size_t i = 0; i < Polygon.size(); ++i) + { + auto const ¤t{Polygon[i]}; + auto const &next{Polygon[(i + 1) % Polygon.size()]}; + auto const a{Side * (Along(current.position) - Cut)}; + auto const b{Side * (Along(next.position) - Cut)}; + if (a >= 0.0) + result.push_back(current); + if ((a >= 0.0) != (b >= 0.0)) + result.push_back(blend(current, next, a / (a - b))); + } + return result; +} + +// triangles cut across the axis of the model into slices, so a long face bends with the curve instead of spanning it straight +std::vector slices(std::vector const &Vertices, std::function const &Along, double const Low, double const Width) +{ + std::vector result; + result.reserve(Vertices.size()); + for (std::size_t i = 0; i + 2 < Vertices.size(); i += 3) + { + auto const a0{Along(Vertices[i].position)}; + auto const a1{Along(Vertices[i + 1].position)}; + auto const a2{Along(Vertices[i + 2].position)}; + auto const first{static_cast(std::floor((std::min({a0, a1, a2}) - Low) / Width))}; + auto const last{static_cast(std::ceil((std::max({a0, a1, a2}) - Low) / Width)) - 1}; + if (last <= first) + { + result.insert(result.end(), Vertices.begin() + i, Vertices.begin() + i + 3); + continue; + } + std::vector const triangle{Vertices[i], Vertices[i + 1], Vertices[i + 2]}; + for (int slice = first; slice <= last; ++slice) + { + auto const polygon{clip(clip(triangle, Along, Low + slice * Width, 1.0), Along, Low + (slice + 1) * Width, -1.0)}; + for (std::size_t k = 1; k + 1 < polygon.size(); ++k) + { + result.push_back(polygon[0]); + result.push_back(polygon[k]); + result.push_back(polygon[k + 1]); + } + } + } + return result; +} + +} // namespace + +// height of the head of the rail on the side of the offset over the points of the track, less the height of the rail; 0 for no side +double sweep_node::rail_head(double const Roll, double const Lateral) +{ + auto const roll{glm::radians(Roll)}; + auto const raise{Global.bRollFix ? 0.75 * std::abs(std::sin(roll)) : 0.0}; + if (Lateral == 0.0) + return raise; + return raise - (Lateral > 0.0 ? 1.0 : -1.0) * 0.75 * std::sin(roll); +} + +bool sweep_node::state::operator==(state const &Other) const +{ + if (model != Other.model || skin != Other.skin || bend != Other.bend || step != Other.step || from != Other.from || to != Other.to || lateral != Other.lateral || height != Other.height || along_x != Other.along_x || + flip != Other.flip || mirror != Other.mirror || tilt != Other.tilt || face != Other.face || widen != Other.widen || parameters != Other.parameters || side_anchor != Other.side_anchor || height_anchor != Other.height_anchor || pieces.size() != Other.pieces.size()) + { + return false; + } + for (std::size_t i = 0; i < pieces.size(); ++i) + { + if (pieces[i].points != Other.pieces[i].points || pieces[i].rolls != Other.pieces[i].rolls) + { + return false; + } + } + return true; +} + +sweep_node::sweep_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} + +void sweep_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + m_state = state{}; + m_state.model = Input.getToken(); + m_state.skin = Input.getToken(); + auto token{Input.getToken()}; + while (false == token.empty() && token != "endsweep") + { + if (token == "mode") + { + m_state.bend = Input.getToken() != "repeat"; + } + else if (token == "step") + { + Input.getTokens(); + Input >> m_state.step; + } + else if (token == "range") + { + Input.getTokens(2); + Input >> m_state.from >> m_state.to; + } + else if (token == "offset") + { + Input.getTokens(2); + Input >> m_state.lateral >> m_state.height; + } + else if (token == "axis") + { + m_state.along_x = Input.getToken() == "x"; + } + else if (token == "flip") + { + m_state.flip = true; + } + else if (token == "mirror") + { + m_state.mirror = true; + } + else if (token == "tilt") + { + m_state.tilt = true; + } + else if (token == "widen") + { + m_state.widen = true; + } + else if (token == "noface") + { + m_state.face = false; + } + else if (token == "parameters") + { + int count{0}; + Input.getTokens(); + Input >> count; + m_state.parameters.clear(); + for (int i = 0; i < count; ++i) + m_state.parameters.push_back(Input.getToken(false)); + } + else if (token == "anchor") + { + auto const side{Input.getToken()}; + auto const height{Input.getToken()}; + m_state.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0; + m_state.height_anchor = height == "bottom" ? 1 : height == "top" ? 2 : height == "edge" ? 3 : 0; + } + else if (token == "piece") + { + segment_data piece; + piece.deserialize(Input, Offset); + m_state.pieces.push_back(piece); + } + else + { + ErrorLog("Bad sweep: unknown property \"" + token + "\" of \"" + m_name + "\" in file \"" + Input.Name() + "\""); + } + token = Input.getToken(); + } + rebuild_samples(); +} + +void sweep_node::define(state const &State) +{ + auto const shown{m_shapes.shown()}; + if (shown) + { + hide(); + } + m_state = State; + rebuild_samples(); + if (shown) + { + show(); + } +} + +void sweep_node::rebuild_samples() +{ + m_samples.clear(); + double station{0.0}; + for (auto const &piece : m_state.pieces) + { + cubic const curve{piece}; + auto const count{std::clamp(static_cast(plan_distance(curve.p0, curve.p3) / sample_spacing), 4, 4000)}; + for (int k = m_samples.empty() ? 0 : 1; k <= count; ++k) + { + auto const t{static_cast(k) / count}; + auto const position{curve.point(t)}; + if (false == m_samples.empty()) + { + station += plan_distance(m_samples.back().position, position); + } + auto tangent{curve.first(t)}; + tangent = glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{0.0, 0.0, 1.0}; + m_samples.push_back({station, position, tangent, piece.rolls[0] + (piece.rolls[1] - piece.rolls[0]) * t, {}, {}}); + } + } + if (m_samples.size() > 2) + { + // the structure gauge widens ahead of the curves and of the cant, the same way the gauge check of the editor has it + std::vector chainage, curvature, cant; + for (std::size_t i = 0; i < m_samples.size(); ++i) + { + auto const &before{m_samples[i == 0 ? 0 : i - 1]}; + auto const &after{m_samples[std::min(i + 1, m_samples.size() - 1)]}; + glm::dvec2 const a{before.tangent.x, before.tangent.z}; + glm::dvec2 const b{after.tangent.x, after.tangent.z}; + auto const span{after.station - before.station}; + auto const turn{glm::length(a) > 1e-9 && glm::length(b) > 1e-9 ? std::atan2(a.x * b.y - a.y * b.x, glm::dot(a, b)) : 0.0}; + chainage.push_back(m_samples[i].station); + curvature.push_back(span > 1e-9 ? turn / span : 0.0); + cant.push_back(1.5 * std::abs(std::sin(glm::radians(m_samples[i].roll)))); + } + auto const sections{gauge::sections(gauge::kind::unified, chainage, curvature, cant)}; + for (std::size_t i = 0; i < m_samples.size() && i < sections.size(); ++i) + { + m_samples[i].widening = sections[i].lower; + m_samples[i].cant = sections[i].cant; + } + } + if (false == m_samples.empty()) + { + location(frame_at(0.5 * length()).position); + } + m_area.radius = -1.f; +} + +double sweep_node::length() const +{ + return m_samples.empty() ? 0.0 : m_samples.back().station; +} + +double sweep_node::start() const +{ + return std::clamp(m_state.from, 0.0, length()); +} + +double sweep_node::end() const +{ + return m_state.to < 0.0 ? length() : std::clamp(m_state.to, start(), length()); +} + +sweep_node::frame sweep_node::frame_at(double const Station) const +{ + frame result; + if (m_samples.empty()) + { + return result; + } + auto const found{std::upper_bound(m_samples.begin(), m_samples.end(), Station, [](double const Value, sample const &Sample) { return Value < Sample.station; })}; + auto const after{found == m_samples.end() ? std::prev(found) : found}; + auto const before{after == m_samples.begin() ? after : std::prev(after)}; + auto const span{after->station - before->station}; + auto const f{span > 1e-9 ? std::clamp((Station - before->station) / span, 0.0, 1.0) : 0.0}; + result.position = glm::mix(before->position, after->position, f); + // beyond the ends the curve goes on straight + if (Station < m_samples.front().station) + { + result.position = m_samples.front().position + m_samples.front().tangent * (Station - m_samples.front().station); + } + else if (Station > m_samples.back().station) + { + result.position = m_samples.back().position + m_samples.back().tangent * (Station - m_samples.back().station); + } + auto forward{glm::mix(before->tangent, after->tangent, f)}; + forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0}; + glm::dvec3 left{-forward.z, 0.0, forward.x}; + left = glm::length(left) > 1e-9 ? glm::normalize(left) : glm::dvec3{-1.0, 0.0, 0.0}; + glm::dvec3 up{glm::normalize(glm::cross(left, forward))}; + result.roll = before->roll + (after->roll - before->roll) * f; + for (int side = 0; side < 2; ++side) + { + result.widening[side] = before->widening[side] + (after->widening[side] - before->widening[side]) * f; + result.cant[side] = before->cant[side] + (after->cant[side] - before->cant[side]) * f; + } + if (m_state.tilt) + { + auto const roll{glm::radians(result.roll)}; + auto const turnedleft{left * std::cos(roll) - up * std::sin(roll)}; + up = up * std::cos(roll) + left * std::sin(roll); + left = turnedleft; + } + result.forward = forward; + result.left = left; + result.up = up; + return result; +} + +// models a template is made of, with their placement in the space of the template +std::vector sweep_node::items() const +{ + std::vector result; + auto const &definition{m_state}; + auto const ends = [](std::string const &Name, std::string const &Extension) { return Name.size() >= Extension.size() && Name.compare(Name.size() - Extension.size(), Extension.size(), Extension) == 0; }; + auto name{definition.model}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + if (false == ends(name, ".inc")) + { + item single; + single.model = TModelsManager::GetModel(definition.model, false, false); + if (false == definition.skin.empty() && definition.skin != "none") + single.skins.assign(definition.skin); + if (single.model != nullptr) + result.push_back(single); + return result; + } + cParser parser(definition.model, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction, definition.parameters); + std::vector offsets{glm::dvec3{0.0}}; + glm::dvec3 rotation{0.0}; + auto token{parser.getToken()}; + while (false == token.empty()) + { + if (token == "origin") + { + glm::dvec3 offset; + parser.getTokens(3); + parser >> offset.x >> offset.y >> offset.z; + offsets.push_back(offsets.back() + offset); + } + else if (token == "endorigin") + { + if (offsets.size() > 1) + offsets.pop_back(); + } + else if (token == "rotate") + { + parser.getTokens(3); + parser >> rotation.x >> rotation.y >> rotation.z; + } + else if (token == "node") + { + double rangemax, rangemin; + std::string nodename, type; + parser.getTokens(4); + parser >> rangemax >> rangemin >> nodename >> type; + if (type == "model") + { + glm::dvec3 location; + double yaw; + parser.getTokens(4); + parser >> location.x >> location.y >> location.z >> yaw; + auto const model{parser.getToken()}; + auto const skin{parser.getToken()}; + auto rest{parser.getToken()}; + while (false == rest.empty() && rest != "endmodel") + rest = parser.getToken(); + item entry; + entry.model = TModelsManager::GetModel(model, false, false); + if (skin != "none" && false == skin.empty() && skin.front() != '*') + entry.skins.assign(skin); + // placed the way the scenery places a model in an origin turned by a rotate statement + auto const place{glm::rotateY(location, glm::radians(rotation.y)) + offsets.back()}; + glm::dmat4 transform{glm::translate(glm::dmat4(1.0), place)}; + transform = glm::rotate(transform, glm::radians(rotation.y + yaw), glm::dvec3{0.0, 1.0, 0.0}); + transform = glm::rotate(transform, glm::radians(rotation.x), glm::dvec3{1.0, 0.0, 0.0}); + transform = glm::rotate(transform, glm::radians(rotation.z), glm::dvec3{0.0, 0.0, 1.0}); + entry.transform = transform; + if (entry.model != nullptr && rangemin >= 0.0) + result.push_back(entry); + } + else + { + // whatever else the template defines isn't laid along the curve + auto rest{parser.getToken()}; + while (false == rest.empty() && rest != "end" + type && rest != "end" && rest != "endtri" && rest != "endline") + rest = parser.getToken(); + } + } + token = parser.getToken(); + } + return result; +} + +double sweep_node::project(glm::dvec3 const &Point, double &Lateral) const +{ + double best{std::numeric_limits::max()}; + double station{0.0}; + Lateral = 0.0; + glm::dvec2 const point{Point.x, Point.z}; + for (std::size_t i = 1; i < m_samples.size(); ++i) + { + glm::dvec2 const a{m_samples[i - 1].position.x, m_samples[i - 1].position.z}; + glm::dvec2 const b{m_samples[i].position.x, m_samples[i].position.z}; + auto const ab{b - a}; + auto const length2{glm::dot(ab, ab)}; + if (length2 < 1e-12) + continue; + auto const t{std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0)}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < best) + { + best = distance; + station = m_samples[i - 1].station + (m_samples[i].station - m_samples[i - 1].station) * t; + auto const side{ab.x * (point - a).y - ab.y * (point - a).x}; + Lateral = side >= 0.0 ? distance : -distance; + } + } + return station; +} + +double sweep_node::model_length() const +{ + auto const pieces{items()}; + double low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto const &entry : pieces) + { + std::vector parts; + gather_parts(entry.model->Root, entry.transform, entry.skins, parts); + for (auto const &part : parts) + for (auto const &vertex : part.vertices) + { + auto const along{m_state.along_x ? vertex.position.x : vertex.position.z}; + low = std::min(low, along); + high = std::max(high, along); + } + } + return high > low ? high - low : 0.0; +} + +std::vector sweep_node::create_shapes() const +{ + std::vector shapes; + auto const &definition{m_state}; + if (m_samples.size() < 2) + return shapes; + // the parts of each model of the template, and where the model stands in the template + struct piece + { + std::vector parts; + glm::dvec3 origin{0.0}; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + bool bent{false}; + }; + std::vector pieces; + for (auto const &entry : items()) + { + piece model; + gather_parts(entry.model->Root, entry.transform, entry.skins, model.parts); + model.origin = glm::dvec3(entry.transform[3]); + if (false == model.parts.empty()) + pieces.push_back(std::move(model)); + } + if (pieces.empty()) + return shapes; + // the side of the model facing the curve is the one its lateral axis points away from: -x, or -z for a model laid along x + auto const needsflip{definition.along_x ? definition.lateral < 0.0 : definition.lateral > 0.0}; + auto const flip{definition.flip != (definition.face && needsflip)}; + // the model's own axes: along the curve, sideways (+ to the right of it), and up + auto const axes = [&](glm::dvec3 const &Vector) { + glm::dvec3 result{definition.along_x ? Vector.x : Vector.z, definition.along_x ? Vector.z : -Vector.x, Vector.y}; + if (flip) + { + result.x = -result.x; + result.y = -result.y; + } + if (definition.mirror) + { + result.y = -result.y; + } + return result; + }; + glm::dvec3 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto &model : pieces) + { + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{axes(vertex.position)}; + model.low = glm::min(model.low, local); + model.high = glm::max(model.high, local); + } + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + // the long models of a template follow the curve, the others stand at their places along it + for (auto &model : pieces) + model.bent = definition.bend && (pieces.size() == 1 || model.high.x - model.low.x >= 0.5 * (high.x - low.x)); + // what's bent sets the length of a copy, its sides and its top; the rest only goes along with it + if (std::any_of(pieces.begin(), pieces.end(), [](piece const &Model) { return Model.bent; })) + { + low = glm::dvec3{std::numeric_limits::max()}; + high = glm::dvec3{-std::numeric_limits::max()}; + for (auto const &model : pieces) + if (model.bent) + { + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + } + auto const own{high.x - low.x}; + // the point of the model the offset puts at its place, sideways and up + auto const nearside{definition.lateral >= 0.0 ? low.y : high.y}; + auto const farside{definition.lateral >= 0.0 ? high.y : low.y}; + double edgetop{-std::numeric_limits::max()}; + for (auto const &model : pieces) + if (model.bent || false == definition.bend) + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{axes(vertex.position)}; + if (std::abs(local.y - nearside) <= edge_band) + edgetop = std::max(edgetop, local.z); + } + if (edgetop == -std::numeric_limits::max()) + edgetop = high.z; + glm::dvec3 const anchor_shift{0.0, + definition.side_anchor == 1 ? -nearside : definition.side_anchor == 2 ? -farside : definition.side_anchor == 3 ? -0.5 * (low.y + high.y) : 0.0, + definition.height_anchor == 1 ? -low.z : definition.height_anchor == 2 ? -high.z : definition.height_anchor == 3 ? -edgetop : 0.0}; + auto const from{start()}; + auto const to{end()}; + auto const stretch{to - from}; + if (own < 1e-3 || stretch < 1e-3) + return shapes; + if (definition.bend) + { + auto const along = [&](glm::dvec3 const &Position) { return axes(Position).x; }; + for (auto &model : pieces) + if (model.bent) + for (auto &part : model.parts) + part.vertices = slices(part.vertices, along, low.x, slice_width); + } + auto const copy_length{definition.step > 0.01 ? definition.step : own}; + // copies end to end fill the stretch exactly, each one squeezed or stretched a bit; repeated ones go at even steps + auto const copies{definition.bend ? std::max(1, static_cast(std::round(stretch / copy_length))) : static_cast(std::floor(stretch / copy_length + 1e-6)) + 1}; + auto const spacing{definition.bend ? stretch / copies : copy_length}; + auto const scale{definition.bend ? spacing / own : 1.0}; + std::size_t percopy{0}; + for (auto const &model : pieces) + for (auto const &part : model.parts) + percopy += part.vertices.size(); + auto const limit{std::max(1, vertex_limit / std::max(1, percopy))}; + if (static_cast(copies) > limit) + ErrorLog("Bad sweep: \"" + m_name + "\" would make " + std::to_string(copies) + " copies of \"" + definition.model + "\", only " + std::to_string(limit) + " are made"); + auto const count{std::min(copies, limit)}; + struct batch + { + material_handle material; + lighting_data lighting; + bool translucent; + std::vector vertices; + }; + std::vector batches; + auto const target = [&](model_part const &Part) -> std::vector & { + for (auto &entry : batches) + if (entry.material == Part.material && entry.lighting == Part.lighting && entry.translucent == Part.translucent) + return entry.vertices; + batches.push_back({Part.material, Part.lighting, Part.translucent, {}}); + return batches.back().vertices; + }; + // set back by the widening of the gauge: from the radius, and on the inner side of a canted curve by the lean of the cars + // at the height of the edge + auto const side{definition.lateral >= 0.0 ? 1 : 0}; + auto const edgeheight{std::max(0.0, definition.height - gauge::rail_height)}; + auto const setback = [&](frame const &At) { + if (false == definition.widen) + return 0.0; + auto const widening{At.widening[side] + std::max(0.0, At.cant[side]) * edgeheight / 1.5}; + return definition.lateral >= 0.0 ? widening : -widening; + }; + // heights are given from the head of the rail on the side of the copies. the cant turns the track about its inner rail when the + // simulation raises it for the cant, about the axis otherwise; a copy which leans with the cant goes up with the axis + auto const cone = [&](frame const &At) { return rail_head(At.roll, definition.tilt ? 0.0 : definition.lateral); }; + auto const upright = [](frame At) { + glm::dvec3 forward{At.forward.x, 0.0, At.forward.z}; + forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0}; + At.forward = forward; + At.left = glm::dvec3{-forward.z, 0.0, forward.x}; + At.up = glm::dvec3{0.0, 1.0, 0.0}; + return At; + }; + glm::dvec3 const offset{0.0, definition.height, 0.0}; + for (std::size_t copy = 0; copy < count; ++copy) + { + auto const base{from + spacing * static_cast(copy)}; + for (auto const &model : pieces) + { + // a model which doesn't bend stands upright at its point: the place of its origin along the curve + auto const anchor{axes(model.origin) + anchor_shift}; + auto const stand{upright(frame_at(definition.bend ? base + (anchor.x - low.x) * scale : base))}; + for (auto const &part : model.parts) + { + auto &vertices{target(part)}; + auto const first{vertices.size()}; + for (auto const &vertex : part.vertices) + { + auto const local{axes(vertex.position) + anchor_shift}; + auto const direction{axes(glm::dvec3(vertex.normal))}; + world_vertex placed{vertex}; + if (model.bent) + { + auto const at{frame_at(base + (local.x - low.x) * scale)}; + placed.position = at.position + at.left * (local.y + definition.lateral + setback(at)) + at.up * local.z + offset + glm::dvec3{0.0, cone(at), 0.0}; + placed.normal = glm::vec3(glm::normalize(at.forward * direction.x + at.left * direction.y + at.up * direction.z)); + } + else + { + auto const along{definition.bend ? local.x - anchor.x : local.x}; + placed.position = stand.position + stand.forward * along + stand.left * (local.y + definition.lateral + setback(stand)) + stand.up * local.z + offset + glm::dvec3{0.0, cone(stand), 0.0}; + placed.normal = glm::vec3(glm::normalize(stand.forward * direction.x + stand.left * direction.y + stand.up * direction.z)); + } + vertices.push_back(placed); + } + if (definition.mirror) + { + // a mirror image turns the triangles inside out + for (auto i = first; i + 2 < vertices.size(); i += 3) + std::swap(vertices[i + 1], vertices[i + 2]); + } + } + } + } + for (auto &entry : batches) + { + if (entry.vertices.empty()) + continue; + scene::shape_node shape; + shape.make_terrain(entry.material, std::move(entry.vertices), glm::dvec3{0.0}); + shape.lighting(entry.lighting); + shape.translucent(entry.translucent); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + +void sweep_node::show() +{ + if (m_merged || m_shapes.shown() || m_editorremoved) + { + return; + } + m_shapes.show(create_shapes(), location()); +} + +void sweep_node::hide() +{ + m_shapes.hide(); +} + +float sweep_node::radius_() +{ + return static_cast(0.5 * length() + std::abs(m_state.lateral) + 25.0); +} + +void sweep_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +void sweep_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +void sweep_node::export_as_text_(std::ostream &Output) const +{ + auto const &sweep{m_state}; + Output << "sweep " << sweep.model << ' ' << (sweep.skin.empty() ? std::string{"none"} : sweep.skin) << ' '; + Output << "mode " << (sweep.bend ? "bend" : "repeat") << ' '; + if (sweep.step > 0.0) + { + Output << "step " << sweep.step << ' '; + } + if (sweep.from > 0.0 || sweep.to >= 0.0) + { + Output << "range " << sweep.from << ' ' << sweep.to << ' '; + } + if (sweep.lateral != 0.0 || sweep.height != 0.0) + { + Output << "offset " << sweep.lateral << ' ' << sweep.height << ' '; + } + if (sweep.along_x) + { + Output << "axis x "; + } + if (sweep.flip) + { + Output << "flip "; + } + if (sweep.mirror) + { + Output << "mirror "; + } + if (sweep.tilt) + { + Output << "tilt "; + } + if (false == sweep.face) + { + Output << "noface "; + } + if (sweep.widen) + { + Output << "widen "; + } + if (false == sweep.parameters.empty()) + { + Output << "parameters " << sweep.parameters.size() << ' '; + for (auto const ¶meter : sweep.parameters) + Output << (parameter.empty() ? std::string{"none"} : parameter) << ' '; + } + if (sweep.side_anchor != 0 || sweep.height_anchor != 0) + { + char const *sides[] = {"origin", "near", "far", "centre"}; + char const *heights[] = {"origin", "bottom", "top", "edge"}; + Output << "anchor " << sides[std::clamp(sweep.side_anchor, 0, 3)] << ' ' << heights[std::clamp(sweep.height_anchor, 0, 3)] << ' '; + } + auto const precision{Output.precision(std::numeric_limits::digits10)}; + for (auto const &piece : sweep.pieces) + { + auto const &start{piece.points[segment_data::point::start]}; + auto const &control1{piece.points[segment_data::point::control1]}; + auto const &control2{piece.points[segment_data::point::control2]}; + auto const &end{piece.points[segment_data::point::end]}; + Output << "\npiece " << start.x << ' ' << start.y << ' ' << start.z << ' ' << piece.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' + << end.x << ' ' << end.y << ' ' << end.z << ' ' << piece.rolls[1] << ' ' << piece.radius; + } + Output.precision(precision); + Output << "\nendsweep\n"; +} + +void sweep_table::create_geometry(scene::scratch_data &Scratchpad) +{ + if (simulation::Region == nullptr) + { + return; + } + for (auto *sweep : m_items) + { + if (sweep == nullptr) + { + continue; + } + if (Global.editor_session && false == Global.NvRenderer) + { + // scenery opened for editing: the copies are kept apart, so they can be changed later + sweep->show(); + continue; + } + for (auto &shape : sweep->create_shapes()) + { + simulation::Region->insert(shape, Scratchpad, false); + } + sweep->merged(true); + } +} diff --git a/world/Sweep.h b/world/Sweep.h new file mode 100644 index 00000000..7da708d7 --- /dev/null +++ b/world/Sweep.h @@ -0,0 +1,158 @@ +/* +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 "utilities/Classes.h" +#include "utilities/Names.h" +#include "scene/scenenode.h" +#include "world/Road.h" +#include "world/Segment.h" +#include "vehicle/DynObj.h" + +namespace scene +{ +struct scratch_data; +} + +// a model laid along a curve: copies of it bent to follow the curve end to end, or set along it at even steps. +// the node isn't drawn itself, it makes ordinary shapes of the copies, so the renderers deal with it as with the rest +// of the scenery. animations, lights and other submodels which aren't plain triangles are left out of the copies. +// scenery entry: +// node sweep [ ]... piece ... endsweep +// properties: mode , step , +// range (m along the curve, to below 0: up to its end), offset , +// anchor (what of the model the offset puts there: sideways its origin, its side +// facing the curve, the other side or its middle; up its origin, its bottom or its top), +// axis (axis of the model laid along the curve), flip (the model goes the other way), mirror (its sides swapped), +// widen (the offset grows by the widening of the structure gauge in the curves, the way the platforms are set back), +// noface (the model isn't turned to have its -x side, or -z for one laid along x, facing the curve), +// parameters ... (of a template: the model can be an *.inc file, its long models are bent, the others stand +// upright at their places along the curve), +// anchor sides: origin, near, far, centre; heights: origin, bottom, top, edge (the top of the side facing the curve), +// tilt (bent copies lean with the cant of the curve; otherwise they stay upright, as the platforms and the walls by a track do, +// at the height the plane of the cant has at their sideways offset) +class sweep_node : public scene::basic_node +{ + + public: + struct state + { + std::string model; + std::string skin{"none"}; + bool bend{true}; + double step{0.0}; + double from{0.0}; + double to{-1.0}; + double lateral{0.0}; + double height{0.0}; + bool along_x{false}; + bool flip{false}; + bool mirror{false}; + bool tilt{false}; + bool face{true}; // the model is turned so its -x side (-z for a model laid along x) faces the curve + bool widen{false}; // moved away from the curve by the widening of the structure gauge in the curves + std::vector parameters; // of a template (*.inc) laid along the curve + int side_anchor{0}; // 0: origin, 1: the side facing the curve, 2: the far side, 3: the middle + int height_anchor{0}; // 0: origin, 1: bottom, 2: top, 3: top of the side facing the curve + std::vector pieces; + + bool operator==(state const &Other) const; + }; + // point of the curve, with the directions the copies are laid out by + struct frame + { + glm::dvec3 position{0.0}; + glm::dvec3 forward{0.0, 0.0, 1.0}; + glm::dvec3 left{-1.0, 0.0, 0.0}; + glm::dvec3 up{0.0, 1.0, 0.0}; + double roll{0.0}; // degrees, of the curve there + std::array widening{}; // of the structure gauge, below the platforms: right and left side, metres + std::array cant{}; // metres, positive on the inner side of the curve + }; + + explicit sweep_node(scene::node_data const &Nodedata); + + void import(cParser &Input, glm::dvec3 const &Offset); + void define(state const &State); + state const &definition() const + { + return m_state; + } + // length of the curve, in the plan + double length() const; + frame frame_at(double const Station) const; + // station of the point of the curve nearest to the point in the plan, and how far to the right of the curve it is + double project(glm::dvec3 const &Point, double &Lateral) const; + // stretch of the curve the copies take + double start() const; + double end() const; + // length of a copy along the curve, as the model has it + double model_length() const; + std::vector create_shapes() const; + // a model of what's laid along the curve, placed in its space + struct item + { + TModel3d *model{nullptr}; + material_data skins; + glm::dmat4 transform{1.0}; + }; + std::vector items() const; + // height of the head of the rail on the side of the offset over the points of the track, less the height of the rail + static double rail_head(double const Roll, double const Lateral); + // puts the copies in the scene as shapes of their own, which can be taken back + void show(); + void hide(); + void merged(bool const Merged) + { + m_merged = Merged; + } + bool merged() const + { + return m_merged; + } + + bool m_editorremoved{false}; + + private: + void rebuild_samples(); + float radius_() override; + void serialize_(std::ostream &Output) const override; + void deserialize_(std::istream &Input) override; + void export_as_text_(std::ostream &Output) const override; + + struct sample + { + double station; + glm::dvec3 position; + glm::dvec3 tangent; + double roll; // degrees + std::array widening; + std::array cant; + }; + state m_state; + std::vector m_samples; + bool m_merged{false}; + owned_shapes m_shapes; +}; + +// collection of the models laid along curves present in the scene +class sweep_table : public basic_table +{ + + public: + // generates geometry of the copies and puts it in the scene + void create_geometry(scene::scratch_data &Scratchpad); +};