diff --git a/AnimModel.cpp b/AnimModel.cpp index 91e9f45d..f18fdc7b 100644 --- a/AnimModel.cpp +++ b/AnimModel.cpp @@ -929,7 +929,12 @@ TAnimModel::export_as_text_( std::ostream &Output ) const { // don't include 'textures/' in the path texturefile.erase( 0, std::string{ szTexturePath }.size() ); } - Output << texturefile << ' '; + if( texturefile.find( ' ' ) == std::string::npos ) { + Output << texturefile << ' '; + } + else { + Output << "\"" << texturefile << "\"" << ' '; + } // light submodels activation configuration if( iNumLights > 0 ) { Output << "lights "; diff --git a/editormode.cpp b/editormode.cpp index 61414899..b23ccc39 100644 --- a/editormode.cpp +++ b/editormode.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "Timer.h" #include "Console.h" #include "renderer.h" +#include "AnimModel.h" bool editor_mode::editormode_input::init() { @@ -175,6 +176,22 @@ editor_mode::on_key( int const Key, int const Scancode, int const Action, int co break; } + // TODO: ensure delete method can play nice with history stack + case GLFW_KEY_DELETE: { + TAnimModel *model = dynamic_cast(m_node); + if (!model) + break; + + m_node = nullptr; + m_dragging = false; + Application.set_cursor( GLFW_CURSOR_NORMAL ); + static_cast( m_userinterface.get() )->set_node(nullptr); + + simulation::State.delete_model(model); + + break; + } + default: { break; } @@ -224,6 +241,8 @@ editor_mode::on_cursor_pos( double const Horizontal, double const Vertical ) { } +/* +TODO: re-enable in post-merge cleanup void editor_mode::on_mouse_button( int const Button, int const Action, int const Mods ) { @@ -255,6 +274,81 @@ editor_mode::on_mouse_button( int const Button, int const Action, int const Mods m_input.mouse.button( Button, Action ); } +*/ +void +editor_mode::on_mouse_button( int const Button, int const Action, int const Mods ) { + + // give the ui first shot at the input processing... + if( true == m_userinterface->on_mouse_button( Button, Action, Mods ) ) { return; } + + if( Button == GLFW_MOUSE_BUTTON_LEFT ) { + + if( Action == GLFW_PRESS ) { + // left button press + auto const mode = static_cast( m_userinterface.get() )->mode(); + + m_node = nullptr; + + GfxRenderer->Pick_Node_Callback([this, mode](scene::basic_node *node) + { + editor_ui *ui = static_cast( m_userinterface.get() ); + + if (mode == nodebank_panel::MODIFY) { + if (!m_dragging) + return; + + m_node = node; + if( m_node ) + Application.set_cursor( GLFW_CURSOR_DISABLED ); + else + m_dragging = false; + ui->set_node( m_node ); + } + else if (mode == nodebank_panel::COPY) { + if (node && typeid(*node) == typeid(TAnimModel)) { + std::string as_text; + node->export_as_text(as_text); + + ui->add_node_template(as_text); + } + + m_dragging = false; + } + else if (mode == nodebank_panel::ADD) { + const std::string *src = ui->get_active_node_template(); + std::string name = "editor_" + std::to_string(LocalRandom(0.0, 100000.0)); + + if (!src) + return; + + TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + GfxRenderer->Mouse_Position()); + + if (!cloned) + return; + + if (!m_dragging) + return; + + m_node = cloned; + Application.set_cursor( GLFW_CURSOR_DISABLED ); + ui->set_node( m_node ); + } + }); + + m_dragging = true; + } + else { + // left button release + if( m_node ) + Application.set_cursor( GLFW_CURSOR_NORMAL ); + m_dragging = false; + // prime history stack for another snapshot + m_takesnapshot = true; + } + } + + m_input.mouse.button( Button, Action ); +} void editor_mode::on_scroll( double const Xoffset, double const Yoffset ) { diff --git a/editormode.h b/editormode.h index ad04fd86..339f21dd 100644 --- a/editormode.h +++ b/editormode.h @@ -89,5 +89,5 @@ private: scene::basic_editor m_editor; scene::basic_node *m_node; // currently selected scene node bool m_takesnapshot { true }; // helper, hints whether snapshot of selected node(s) should be taken before modification - + bool m_dragging = false; }; diff --git a/editoruilayer.cpp b/editoruilayer.cpp index e250635c..b823cd54 100644 --- a/editoruilayer.cpp +++ b/editoruilayer.cpp @@ -19,6 +19,7 @@ editor_ui::editor_ui() { clear_panels(); // bind the panels with ui object. maybe not the best place for this but, eh push_back( &m_itempropertiespanel ); + push_back( &m_nodebankpanel ); } // updates state of UI elements @@ -46,3 +47,18 @@ editor_ui::set_node( scene::basic_node * Node ) { m_node = Node; } + +void +editor_ui::add_node_template(const std::string &desc) { + m_nodebankpanel.add_template(desc); +} + +std::string const * +editor_ui::get_active_node_template() { + return m_nodebankpanel.get_active_template(); +} + +nodebank_panel::edit_mode +editor_ui::mode() { + return m_nodebankpanel.mode; +} diff --git a/editoruilayer.h b/editoruilayer.h index daa050ef..893a03e9 100644 --- a/editoruilayer.h +++ b/editoruilayer.h @@ -29,9 +29,16 @@ public: update() override; void set_node( scene::basic_node * Node ); + void + add_node_template(const std::string &desc); + const std::string * + get_active_node_template(); + nodebank_panel::edit_mode + mode(); private: // members itemproperties_panel m_itempropertiespanel { "Node Properties", true }; + nodebank_panel m_nodebankpanel{ "Node Bank", true }; scene::basic_node * m_node { nullptr }; // currently bound scene node, if any }; diff --git a/editoruipanels.cpp b/editoruipanels.cpp index 1b69e580..03758c51 100644 --- a/editoruipanels.cpp +++ b/editoruipanels.cpp @@ -297,3 +297,79 @@ itemproperties_panel::render_group() { return true; } + + + +nodebank_panel::nodebank_panel( std::string const &Name, bool const Isopen ) : ui_panel( Name, Isopen ) { + size_min = { 100, 50 }; + size_max = { 1000, 1000 }; + + std::ifstream file; + file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary); + + std::string line; + while (std::getline(file, line)) + if (line.size() > 2) + m_nodebank.push_back(std::make_unique(line)); +} + +void +nodebank_panel::render() { + + if( false == is_open ) { return; } + + auto flags = + ImGuiWindowFlags_NoFocusOnAppearing + | ImGuiWindowFlags_NoCollapse + | ( size.x > 0 ? ImGuiWindowFlags_NoResize : 0 ); + + if( size.x > 0 ) { + ImGui::SetNextWindowSize( ImVec2( size.x, size.y ) ); + } + if( size_min.x > 0 ) { + ImGui::SetNextWindowSizeConstraints( ImVec2( size_min.x, size_min.y ), ImVec2( size_max.x, size_max.y ) ); + } + auto const panelname { ( + title.empty() ? + name : + title ) + + "###" + name }; + + if( true == ImGui::Begin( panelname.c_str(), nullptr, flags ) ) { + + ImGui::RadioButton("modify node", (int*)&mode, MODIFY); + ImGui::SameLine(); + ImGui::RadioButton("insert from bank", (int*)&mode, ADD); + ImGui::SameLine(); + ImGui::RadioButton( "copy to bank", (int*)&mode, COPY ); + + ImGui::PushItemWidth(-1); + if (ImGui::ListBoxHeader("##nodebank", ImVec2(-1, -1))) + { + int i = 0; + for (auto const entry : m_nodebank) { + std::string label = *entry + "##" + std::to_string(i); + if (ImGui::Selectable(label.c_str(), entry == m_selectedtemplate)) + m_selectedtemplate = entry; + i++; + } + + ImGui::ListBoxFooter(); + } + } + + ImGui::End(); +} + +void +nodebank_panel::add_template(const std::string &desc) { + std::ofstream file; + file.open("nodebank.txt", std::ios_base::out | std::ios_base::app | std::ios_base::binary); + file << desc; + + m_nodebank.push_back(std::make_unique(desc)); +} + +const std::string *nodebank_panel::get_active_template() { + return m_selectedtemplate.get(); +} diff --git a/editoruipanels.h b/editoruipanels.h index 4ed9a83a..f86a6994 100644 --- a/editoruipanels.h +++ b/editoruipanels.h @@ -47,3 +47,23 @@ private: std::string m_groupprefix; std::vector m_grouplines; }; + +class nodebank_panel : public ui_panel { + std::vector> m_nodebank; + std::shared_ptr m_selectedtemplate; + +public: + enum edit_mode { + MODIFY, + COPY, + ADD + }; + + edit_mode mode = MODIFY; + + nodebank_panel( std::string const &Name, bool const Isopen ); + + void render() override; + void add_template(const std::string &desc); + const std::string* get_active_template(); +}; diff --git a/scenenode.cpp b/scenenode.cpp index 9198af74..dd7c4db3 100644 --- a/scenenode.cpp +++ b/scenenode.cpp @@ -749,7 +749,7 @@ basic_node::export_as_text( std::ostream &Output ) const { << ' ' << ( m_rangesquaredmax < std::numeric_limits::max() ? std::sqrt( m_rangesquaredmax ) : -1 ) << ' ' << std::sqrt( m_rangesquaredmin ) // name - << ' ' << m_name << ' '; + << ' ' << ( m_name.empty() ? "none" : m_name ) << ' '; // template method implementation export_as_text_( Output ); } diff --git a/scenenodegroups.cpp b/scenenodegroups.cpp index cefa040c..c0197034 100644 --- a/scenenodegroups.cpp +++ b/scenenodegroups.cpp @@ -93,8 +93,8 @@ node_groups::insert( scene::group_handle const Group, basic_event *Event ) { // sends basic content of the class in legacy (text) format to provided stream void -node_groups::export_as_text( std::ostream &Output ) const { - +node_groups::export_as_text( std::ostream &Output, bool const Dirty ) const { +/* for( auto const &group : m_groupmap ) { Output << "group\n"; @@ -112,6 +112,38 @@ node_groups::export_as_text( std::ostream &Output ) const { } Output << "endgroup\n"; } +*/ + for( auto const &group : m_groupmap ) { + bool any = false; + for( auto *node : group.second.nodes ) { + if (node->dirty() != Dirty) + continue; + // HACK: auto-generated memory cells aren't exported, so we check for this + // TODO: is_exportable as basic_node method + if( ( typeid( *node ) == typeid( TMemCell ) ) + && ( false == static_cast( node )->is_exportable ) ) { + continue; + } + + if (!any) + Output << "group\n"; + any = true; + + node->export_as_text( Output ); + } + for( auto *event : group.second.events ) { + if (Dirty) + continue; + + if (!any) + Output << "group\n"; + any = true; + + event->export_as_text( Output ); + } + if (any) + Output << "endgroup\n"; + } } // removes specified group from the group list and group information from the group's nodes diff --git a/scenenodegroups.h b/scenenodegroups.h index d0815bae..b7e18d4c 100644 --- a/scenenodegroups.h +++ b/scenenodegroups.h @@ -48,7 +48,7 @@ public: return m_groupmap[ Group ]; } // sends basic content of the class in legacy (text) format to provided stream void - export_as_text( std::ostream &Output ) const; + export_as_text( std::ostream &Output, bool const Dirty ) const; private: // types diff --git a/simulationstateserializer.cpp b/simulationstateserializer.cpp index aa60194f..d94f2f65 100644 --- a/simulationstateserializer.cpp +++ b/simulationstateserializer.cpp @@ -1012,7 +1012,7 @@ state_serializer::transform( glm::dvec3 Location, scene::scratch_data const &Scr return Location; } - +/* // stores class data in specified file, in legacy (text) format void state_serializer::export_as_text( std::string const &Scenariofile ) const { @@ -1083,6 +1083,89 @@ state_serializer::export_as_text( std::string const &Scenariofile ) const { WriteLog( "Scenery data export done." ); } +*/ +void +state_serializer::export_as_text(std::string const &Scenariofile) const { + + if( Scenariofile == "$.scn" ) { + ErrorLog( "Bad file: scenery export not supported for file \"$.scn\"" ); + } + else { + WriteLog( "Scenery data export in progress..." ); + } + + auto filename { Scenariofile }; + while( filename[ 0 ] == '$' ) { + // trim leading $ char rainsted utility may add to the base name for modified .scn files + filename.erase( 0, 1 ); + } + erase_extension( filename ); + auto absfilename = Global.asCurrentSceneryPath + filename + "_export"; + + std::ofstream scmdirtyfile { absfilename + "_dirty.scm" }; + export_nodes_to_stream(scmdirtyfile, true); + + std::ofstream scmfile { absfilename + ".scm" }; + export_nodes_to_stream(scmfile, false); + + // sounds + // NOTE: sounds currently aren't included in groups + scmfile << "// sounds\n"; + Region->export_as_text( scmfile ); + + scmfile << "// modified objects\ninclude " << filename << "_export_dirty.scm\n"; + + std::ofstream ctrfile { absfilename + ".ctr" }; + // mem cells + ctrfile << "// memory cells\n"; + for( auto const *memorycell : Memory.sequence() ) { + if( ( true == memorycell->is_exportable ) + && ( memorycell->group() == null_handle ) ) { + memorycell->export_as_text( ctrfile ); + } + } + + // events + Events.export_as_text( ctrfile ); + + WriteLog( "Scenery data export done." ); +} + +void +state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) const { + // groups + scmfile << "// groups\n"; + scene::Groups.export_as_text( scmfile, Dirty ); + + // tracks + scmfile << "// paths\n"; + for( auto const *path : Paths.sequence() ) { + if( path->dirty() == Dirty && path->group() == null_handle ) { + path->export_as_text( scmfile ); + } + } + // traction + scmfile << "// traction\n"; + for( auto const *traction : Traction.sequence() ) { + if( traction->dirty() == Dirty && traction->group() == null_handle ) { + traction->export_as_text( scmfile ); + } + } + // power grid + scmfile << "// traction power sources\n"; + for( auto const *powersource : Powergrid.sequence() ) { + if( powersource->dirty() == Dirty && powersource->group() == null_handle ) { + powersource->export_as_text( scmfile ); + } + } + // models + scmfile << "// instanced models\n"; + for( auto const *instance : Instances.sequence() ) { + if( instance && instance->dirty() == Dirty && instance->group() == null_handle ) { + instance->export_as_text( scmfile ); + } + } +} TAnimModel *state_serializer::create_model(const std::string &src, const std::string &name, const glm::dvec3 &position) { cParser parser(src); diff --git a/simulationstateserializer.h b/simulationstateserializer.h index 2994d7a9..a9697633 100644 --- a/simulationstateserializer.h +++ b/simulationstateserializer.h @@ -81,6 +81,7 @@ private: void skip_until( cParser &Input, std::string const &Token ); // transforms provided location by specifed rotation and offset glm::dvec3 transform( glm::dvec3 Location, scene::scratch_data const &Scratchpad ); + void export_nodes_to_stream( std::ostream &, bool Dirty ) const; }; } // simulation diff --git a/version.h b/version.h index b210a3bb..3b53185c 100644 --- a/version.h +++ b/version.h @@ -1,5 +1,5 @@ #pragma once #define VERSION_MAJOR 20 -#define VERSION_MINOR 708 +#define VERSION_MINOR 709 #define VERSION_REVISION 0