diff --git a/CHANGELOG.md b/CHANGELOG.md index 608c0d28..8df70e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ If a copy of the MPL was not distributed with this file, You can obtain one at h +## [0.11.0] Lua Modules +* **File version number has changed. Files saved with RaCo 0.11.0 cannot be opened by previous versions.** +* **Export file format has changed. Scenes exported with RaCo 0.11.0 / ramses-logic 0.13.0 cannot be opened by previous ramses-logic versions.** + +### Added +* Added multi-selection for deleting, copying, cutting and pasting in Scene Graph, Resources, Prefabs and Project Browser. +* Added Lua module support. + * The new user type LuaScriptModule is a resource that loads modules from specified Lua files. + * LuaScripts have a new output entry "Modules" - for each module defined in the Lua script file, this entry will contain a reference drop-down where LuaScriptModules can be selected + * Nested modules are currently not supported. +* Added support for generating mipmaps for a textures. + * The texture object has a new option "Generate Mipmaps" which by default is off. If enabled, Ramses will auto-generate mipmaps for the texture. + +### Changes +* Update from ramses-logic 0.12.0 to ramses-logic 0.13.0 + * Major performance improvement for large scenes with lots of links alongside few bugfixes +* Update from ramses 27.0.113 to 27.0.114 + +### Fixes +* Undo / Redo is now properly working for collapsed vector editors in the property browser. +* Fixed visual issue with number inputs in property editor not leaving highlighted state after pressing enter. +* Fixed undo/redo to prevent creation of valid links starting on non-existing properties. +* Fixed drop down boxes with "" reference in property browser causing a crash when being deactivated. + +### Known Issues +* The INT64 type introduced in ramses-logic 0.13.0 is not supported yet. +* The property "timeRange" in Animations introduced in ramses-logic 0.13.0 is not supported yet. +* The new "TimerNode" introduced in ramses-logic 0.13.0 cannot be created in RaCo yet. + ## [0.10.0] Animations * **File version number has changed. Files saved with RaCo 0.10.0 cannot be opened by previous versions.** diff --git a/CMakeLists.txt b/CMakeLists.txt index b0f97308..f5c6afd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ cmake_minimum_required(VERSION 3.19) SET(CMAKE_CONFIGURATION_TYPES "Debug;RelWithDebInfo") -project(RaCoOS VERSION 0.10.0) +project(RaCoOS VERSION 0.11.0) SET(RACO_RELEASE_DIRECTORY ${CMAKE_BINARY_DIR}/release) diff --git a/EditorApp/DebugActions.cpp b/EditorApp/DebugActions.cpp index c3494aed..efa4eebe 100644 --- a/EditorApp/DebugActions.cpp +++ b/EditorApp/DebugActions.cpp @@ -57,7 +57,7 @@ void configureDebugActions(Ui::MainWindow* ui, QWidget* widget, raco::core::Comm auto node = commandInterface->createObject(raco::user_types::Node::typeDescription.typeName, Naming::format("DuckNode")); auto meshNode = commandInterface->createObject(raco::user_types::MeshNode::typeDescription.typeName, Naming::format("DuckMeshNode")); - commandInterface->moveScenegraphChild(meshNode, node); + commandInterface->moveScenegraphChildren({meshNode}, node); commandInterface->set(raco::core::ValueHandle{meshNode, &MeshNode::mesh_}, mesh); commandInterface->set(raco::core::ValueHandle{meshNode, {"materials", "material", "material"}}, material); diff --git a/EditorApp/EditMenu.cpp b/EditorApp/EditMenu.cpp index d08b13e9..be7e74a4 100644 --- a/EditorApp/EditMenu.cpp +++ b/EditorApp/EditMenu.cpp @@ -52,10 +52,10 @@ EditMenu::EditMenu(raco::application::RaCoApplication* racoApplication, raco::ob pasteAction->setEnabled(raco::RaCoClipboard::hasEditorObject()); } else { auto focusedTreeView = activeObjectTreeDockWithSelection->getCurrentlyActiveTreeView(); - auto selectedRows = focusedTreeView->selectionModel()->selectedRows(); - auto selectedIndex = focusedTreeView->proxyModel() ? focusedTreeView->proxyModel()->mapToSource(selectedRows.front()) : selectedRows.front(); - copyAction->setEnabled(focusedTreeView->canCopy(selectedIndex)); - pasteAction->setEnabled(focusedTreeView->canPasteInto(selectedIndex)); + auto selectedIndices = focusedTreeView->getSelectedIndices(); + auto pasteIndex = focusedTreeView->getSelectedInsertionTargetIndex(); + copyAction->setEnabled(focusedTreeView->canCopyAtIndices(selectedIndices)); + pasteAction->setEnabled(focusedTreeView->canPasteIntoIndex(pasteIndex, false) || focusedTreeView->canPasteIntoIndex({}, false)); } QObject::connect(copyAction, &QAction::triggered, [racoApplication, objectTreeDockManager]() { @@ -96,10 +96,8 @@ void EditMenu::globalCopyCallback(raco::application::RaCoApplication* racoApplic void EditMenu::globalPasteCallback(raco::application::RaCoApplication* racoApplication, raco::object_tree::view::ObjectTreeDockManager* objectTreeDockManager) { if (auto activeObjectTreeDockWithSelection = objectTreeDockManager->getActiveDockWithSelection()) { auto focusedTreeView = activeObjectTreeDockWithSelection->getCurrentlyActiveTreeView(); - auto selectedRows = focusedTreeView->selectionModel()->selectedRows(); - auto selectionIndex = selectedRows.empty() ? QModelIndex() : selectedRows.front(); - focusedTreeView->globalPasteCallback(selectionIndex); + focusedTreeView->globalPasteCallback(focusedTreeView->getSelectedInsertionTargetIndex()); } else { auto copiedObjs = raco::RaCoClipboard::get(); racoApplication->activeRaCoProject().commandInterface()->pasteObjects(copiedObjs); diff --git a/EditorApp/mainwindow.cpp b/EditorApp/mainwindow.cpp index 92377965..bb6239ac 100644 --- a/EditorApp/mainwindow.cpp +++ b/EditorApp/mainwindow.cpp @@ -53,6 +53,7 @@ #include "user_types/AnimationChannel.h" #include "user_types/CubeMap.h" #include "user_types/LuaScript.h" +#include "user_types/LuaScriptModule.h" #include "user_types/MeshNode.h" #include "user_types/Node.h" #include "user_types/OrthographicCamera.h" @@ -198,6 +199,7 @@ ads::CDockAreaWidget* createAndAddResourceTree(MainWindow* mainWindow, const cha static const std::vector allowedCreateableUserTypes{ AnimationChannel::typeDescription.typeName, CubeMap::typeDescription.typeName, + LuaScriptModule::typeDescription.typeName, Material::typeDescription.typeName, Mesh::typeDescription.typeName, Texture::typeDescription.typeName, diff --git a/components/libApplication/src/RaCoApplication.cpp b/components/libApplication/src/RaCoApplication.cpp index 9e4470ee..8d8f1b50 100644 --- a/components/libApplication/src/RaCoApplication.cpp +++ b/components/libApplication/src/RaCoApplication.cpp @@ -125,7 +125,7 @@ void RaCoApplication::doOneLoop() { scenesBackend_->setScene(activeRaCoProject().project(), activeRaCoProject().errors()); } - auto animNodes = engine_->logicEngine().animationNodes(); + auto animNodes = engine_->logicEngine().getCollection(); logicEngineNeedsUpdate_ |= (animNodes.size() > 0); auto elapsedTime = std::chrono::high_resolution_clock::now() - startTime_; @@ -133,7 +133,7 @@ void RaCoApplication::doOneLoop() { auto activeProjectRunsTimer = activeRaCoProject().project()->settings()->runTimer_.asBool(); if (activeProjectRunsTimer) { - auto loadedScripts = engine_->logicEngine().scripts(); + auto loadedScripts = engine_->logicEngine().getCollection(); for (auto* loadedScript : loadedScripts) { if (auto* timerInput = loadedScript->getInputs()->getChild("time_ms")) { timerInput->set(static_cast(elapsedMsec)); diff --git a/components/libApplication/src/RaCoProject.cpp b/components/libApplication/src/RaCoProject.cpp index ff693ffc..b523d00c 100644 --- a/components/libApplication/src/RaCoProject.cpp +++ b/components/libApplication/src/RaCoProject.cpp @@ -202,7 +202,7 @@ std::unique_ptr RaCoProject::createNew(RaCoApplication* app) { result->context_->set({sNode, &user_types::Node::tags_}, std::vector({"render_main"})); result->context_->set({sCamera, &user_types::Node::translation_, &data_storage::Vec3f::z}, 10.0); - result->context_->moveScenegraphChild(sMeshNode, sNode); + result->context_->moveScenegraphChildren({sMeshNode}, sNode); result->undoStack_.reset(); result->context_->changeMultiplexer().reset(); result->dirty_ = false; diff --git a/components/libApplication/tests/CMakeLists.txt b/components/libApplication/tests/CMakeLists.txt index 48e2e1ab..3d49d39b 100644 --- a/components/libApplication/tests/CMakeLists.txt +++ b/components/libApplication/tests/CMakeLists.txt @@ -42,8 +42,10 @@ raco_package_add_test_resouces( meshes/negativeScaleQuad.gltf meshes/ToyCar/ToyCar.gltf meshes/ToyCar/ToyCar.bin + scripts/compile-error.lua + scripts/moduleDefinition.lua + scripts/moduleDependency.lua scripts/SimpleScript.lua scripts/types-scalar.lua scripts/runtime-error.lua - scripts/compile-error.lua ) diff --git a/components/libApplication/tests/RaCoApplication_test.cpp b/components/libApplication/tests/RaCoApplication_test.cpp index 7543a0a0..ab7fd890 100644 --- a/components/libApplication/tests/RaCoApplication_test.cpp +++ b/components/libApplication/tests/RaCoApplication_test.cpp @@ -55,7 +55,7 @@ TEST_F(RaCoApplicationFixture, exportDuckProject) { auto node = commandInterface->createObject(raco::user_types::Node::typeDescription.typeName, Naming::format("NodeDuck")); auto meshNode = commandInterface->createObject(raco::user_types::MeshNode::typeDescription.typeName, Naming::format("MeshNodeDuck")); - commandInterface->moveScenegraphChild(meshNode, node); + commandInterface->moveScenegraphChildren({meshNode}, node); commandInterface->set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); commandInterface->set(raco::core::ValueHandle{meshNode, {"materials", "material", "material"}}, material); diff --git a/components/libApplication/tests/RaCoProject_test.cpp b/components/libApplication/tests/RaCoProject_test.cpp index 6982cba5..119dd7c2 100644 --- a/components/libApplication/tests/RaCoProject_test.cpp +++ b/components/libApplication/tests/RaCoProject_test.cpp @@ -16,6 +16,8 @@ #include "core/PathManager.h" #include "testing/TestEnvironmentCore.h" #include "testing/TestUtil.h" +#include "user_types/LuaScript.h" +#include "user_types/LuaScriptModule.h" #include "user_types/Material.h" #include "user_types/Mesh.h" #include "user_types/MeshNode.h" @@ -728,4 +730,58 @@ TEST_F(RaCoProjectFixture, copyPasteDeepAnimationReferencingAnimationChannel) { app.activeRaCoProject().commandInterface()->pasteObjects(clipboard); ASSERT_NO_FATAL_FAILURE(app.doOneLoop()); } +} + +TEST_F(RaCoProjectFixture, copyPasteShallowLuaScriptReferencingLuaScriptModule) { + std::string clipboard; + { + RaCoApplication app{ backend }; + app.activeRaCoProject().project()->setCurrentPath((cwd_path() / "project.file").string()); + auto module = app.activeRaCoProject().commandInterface()->createObject(raco::user_types::LuaScriptModule::typeDescription.typeName, "m", "m"); + auto script = app.activeRaCoProject().commandInterface()->createObject(raco::user_types::LuaScript::typeDescription.typeName, "s", "s"); + app.doOneLoop(); + + app.activeRaCoProject().commandInterface()->set({ module, &raco::user_types::LuaScriptModule::uri_ }, cwd_path().append("scripts/moduleDefinition.lua").string()); + app.doOneLoop(); + + app.activeRaCoProject().commandInterface()->set({ script, &raco::user_types::LuaScript::uri_ }, cwd_path().append("scripts/moduleDependency.lua").string()); + app.doOneLoop(); + + app.activeRaCoProject().commandInterface()->set({ script, {"luaModules", "coalas"} }, module); + app.doOneLoop(); + + clipboard = app.activeRaCoProject().commandInterface()->copyObjects({ script }); + } + { + RaCoApplication app{ backend }; + app.activeRaCoProject().commandInterface()->pasteObjects(clipboard); + ASSERT_NO_FATAL_FAILURE(app.doOneLoop()); + } +} + +TEST_F(RaCoProjectFixture, copyPasteDeepLuaScriptReferencingLuaScriptModule) { + std::string clipboard; + { + RaCoApplication app{ backend }; + app.activeRaCoProject().project()->setCurrentPath((cwd_path() / "project.file").string()); + auto module = app.activeRaCoProject().commandInterface()->createObject(raco::user_types::LuaScriptModule::typeDescription.typeName, "m", "m"); + auto script = app.activeRaCoProject().commandInterface()->createObject(raco::user_types::LuaScript::typeDescription.typeName, "s", "s"); + app.doOneLoop(); + + app.activeRaCoProject().commandInterface()->set({ module, &raco::user_types::LuaScriptModule::uri_ }, cwd_path().append("scripts/moduleDefinition.lua").string()); + app.doOneLoop(); + + app.activeRaCoProject().commandInterface()->set({ script, &raco::user_types::LuaScript::uri_ }, cwd_path().append("scripts/moduleDependency.lua").string()); + app.doOneLoop(); + + app.activeRaCoProject().commandInterface()->set({ script, {"luaModules", "coalas"} }, module); + app.doOneLoop(); + + clipboard = app.activeRaCoProject().commandInterface()->copyObjects({ script }, true); + } + { + RaCoApplication app{ backend }; + app.activeRaCoProject().commandInterface()->pasteObjects(clipboard); + ASSERT_NO_FATAL_FAILURE(app.doOneLoop()); + } } \ No newline at end of file diff --git a/components/libRamsesBase/CMakeLists.txt b/components/libRamsesBase/CMakeLists.txt index 4cf7d98e..afc2976b 100644 --- a/components/libRamsesBase/CMakeLists.txt +++ b/components/libRamsesBase/CMakeLists.txt @@ -37,6 +37,7 @@ add_library(libRamsesBase include/ramses_adaptor/MeshNodeAdaptor.h src/ramses_adaptor/MeshNodeAdaptor.cpp include/ramses_adaptor/NodeAdaptor.h src/ramses_adaptor/NodeAdaptor.cpp include/ramses_adaptor/LuaScriptAdaptor.h src/ramses_adaptor/LuaScriptAdaptor.cpp + include/ramses_adaptor/LuaScriptModuleAdaptor.h src/ramses_adaptor/LuaScriptModuleAdaptor.cpp include/ramses_adaptor/ObjectAdaptor.h src/ramses_adaptor/ObjectAdaptor.cpp include/ramses_adaptor/BaseCameraAdaptorHelpers.h src/ramses_adaptor/BaseCameraAdaptorHelpers.cpp include/ramses_adaptor/OrthographicCameraAdaptor.h src/ramses_adaptor/OrthographicCameraAdaptor.cpp diff --git a/components/libRamsesBase/include/ramses_adaptor/LuaScriptAdaptor.h b/components/libRamsesBase/include/ramses_adaptor/LuaScriptAdaptor.h index 3cd14f82..60a8b9b3 100644 --- a/components/libRamsesBase/include/ramses_adaptor/LuaScriptAdaptor.h +++ b/components/libRamsesBase/include/ramses_adaptor/LuaScriptAdaptor.h @@ -33,14 +33,15 @@ class LuaScriptAdaptor : public ObjectAdaptor, public ILogicPropertyProvider { bool sync(core::Errors* errors) override; void readDataFromEngine(core::DataChangeRecorder &recorder); - rlogic::LuaScript* rlogicLuaScript() const { - return luaScript_.get(); - } private: void setupParentSubscription(); void setupInputValuesSubscription(); std::string generateRamsesObjectName() const; + rlogic::LuaScript* rlogicLuaScript() const { + return luaScript_.get(); + } + std::shared_ptr editorObject_; std::unique_ptr> luaScript_{nullptr, [](auto) {}}; components::Subscription subscription_; @@ -48,6 +49,9 @@ class LuaScriptAdaptor : public ObjectAdaptor, public ILogicPropertyProvider { components::Subscription inputSubscription_; components::Subscription childrenSubscription_; components::Subscription parentNameSubscription_; + components::Subscription moduleSubscription_; + + std::vector modules; // Flag to keep track if a change needs to recreate the lua script in the logicengine // or if it is sufficient to just update the input properties. diff --git a/components/libRamsesBase/include/ramses_adaptor/LuaScriptModuleAdaptor.h b/components/libRamsesBase/include/ramses_adaptor/LuaScriptModuleAdaptor.h new file mode 100644 index 00000000..56ad222b --- /dev/null +++ b/components/libRamsesBase/include/ramses_adaptor/LuaScriptModuleAdaptor.h @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of Ramses Composer + * (see https://github.com/GENIVI/ramses-composer). + * + * 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 "core/Handles.h" +#include "ramses_adaptor/ObjectAdaptor.h" +#include "components/DataChangeDispatcher.h" +#include "user_types/LuaScriptModule.h" + +#include + +#include +#include + +namespace raco::ramses_adaptor { + +class LuaScriptModuleAdaptor : public ObjectAdaptor { +public: + explicit LuaScriptModuleAdaptor(SceneAdaptor* sceneAdaptor, raco::user_types::SLuaScriptModule editorObject); + SEditorObject baseEditorObject() noexcept override; + const SEditorObject baseEditorObject() const noexcept override; + + bool sync(core::Errors* errors) override; + + ramses_base::RamsesLuaModule module_; +private: + + raco::user_types::SLuaScriptModule editorObject_; + components::Subscription subscription_; + components::Subscription nameSubscription_; +}; + +}; // namespace raco::ramses_adaptor diff --git a/components/libRamsesBase/include/ramses_base/CoreInterfaceImpl.h b/components/libRamsesBase/include/ramses_base/CoreInterfaceImpl.h index d04b4ec1..0988662d 100644 --- a/components/libRamsesBase/include/ramses_base/CoreInterfaceImpl.h +++ b/components/libRamsesBase/include/ramses_base/CoreInterfaceImpl.h @@ -19,7 +19,9 @@ class CoreInterfaceImpl final : public raco::core::EngineInterface { public: explicit CoreInterfaceImpl(BaseEngineBackend* backend); bool parseShader(const std::string& vertexShader, const std::string& geometryShader, const std::string& fragmentShader, const std::string& shaderDefines, raco::core::PropertyInterfaceList& outUniforms, raco::core::PropertyInterfaceList& outAttributes, std::string& error) override; - bool parseLuaScript(const std::string& luaScript, raco::core::PropertyInterfaceList& outInputs, raco::core::PropertyInterfaceList& outOutputs, std::string& error) override; + bool parseLuaScript(const std::string& luaScript, const raco::data_storage::Table& modules, raco::core::PropertyInterfaceList& outInputs, raco::core::PropertyInterfaceList& outOutputs, std::string& error) override; + bool parseLuaScriptModule(const std::string& luaScriptModule, std::string& outError) override; + bool extractLuaDependencies(const std::string& luaScript, std::vector& moduleList, std::string& outError) override; const std::map& enumerationDescription(raco::core::EngineEnumeration type) const override; private: diff --git a/components/libRamsesBase/include/ramses_base/RamsesHandles.h b/components/libRamsesBase/include/ramses_base/RamsesHandles.h index b7009dd1..3fb30d43 100644 --- a/components/libRamsesBase/include/ramses_base/RamsesHandles.h +++ b/components/libRamsesBase/include/ramses_base/RamsesHandles.h @@ -11,6 +11,7 @@ #pragma once #include "log_system/log.h" +#include "ramses_base/Utils.h" #include #include @@ -40,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -543,6 +545,7 @@ using UniqueRamsesAppearanceBinding = std::unique_ptr>; using UniqueRamsesNodeBinding = std::unique_ptr>; using UniqueRamsesCameraBinding = std::unique_ptr>; +using RamsesLuaModule = RamsesHandle; using RamsesAnimationNode = RamsesHandle; @@ -579,6 +582,17 @@ inline UniqueRamsesCameraBinding ramsesCameraBinding(ramses::Camera& camera, rlo }}; } +inline RamsesLuaModule ramsesLuaModule(const std::string& luaContent, rlogic::LogicEngine* logicEngine, const std::string& name) { + auto moduleConfig = defaultLuaConfig(); + return { + logicEngine->createLuaModule(luaContent, moduleConfig, name), [logicEngine](rlogic::LuaModule* module) { + if (module) { + logicEngine->destroy(*module); + } + }}; + +} + struct RamsesAnimationChannelData { std::string name; rlogic::EInterpolationType interpolationType; diff --git a/components/libRamsesBase/include/ramses_base/Utils.h b/components/libRamsesBase/include/ramses_base/Utils.h index 85ca022d..107a9138 100644 --- a/components/libRamsesBase/include/ramses_base/Utils.h +++ b/components/libRamsesBase/include/ramses_base/Utils.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -31,7 +32,13 @@ bool parseShaderText(ramses::Scene& scene, const std::string& vertexShader, cons // Parse luascripts using ramses logic and return set of in and out parameters with name and type. // Returns true if script can be successfully parsed. -bool parseLuaScript(LogicEngine& engine, const std::string& luaScript, PropertyInterfaceList& outInputs, PropertyInterfaceList& outOutputs, std::string& outError); +bool parseLuaScript(LogicEngine& engine, const std::string& luaScript, const raco::data_storage::Table& modules, PropertyInterfaceList& outInputs, PropertyInterfaceList& outOutputs, std::string& outError); + +// Parse luascript module using ramses logic. +// Returns true if module can be successfully parsed. +bool parseLuaScriptModule(LogicEngine& engine, const std::string& luaScriptModule, std::string& outError); + +rlogic::LuaConfig defaultLuaConfig(); ramses::RamsesVersion getRamsesVersion(); rlogic::RamsesLogicVersion getLogicEngineVersion(); diff --git a/components/libRamsesBase/src/ramses_adaptor/Factories.cpp b/components/libRamsesBase/src/ramses_adaptor/Factories.cpp index e0303fec..56ed77a9 100644 --- a/components/libRamsesBase/src/ramses_adaptor/Factories.cpp +++ b/components/libRamsesBase/src/ramses_adaptor/Factories.cpp @@ -14,6 +14,7 @@ #include "ramses_adaptor/AnimationChannelAdaptor.h" #include "ramses_adaptor/CubeMapAdaptor.h" #include "ramses_adaptor/LuaScriptAdaptor.h" +#include "ramses_adaptor/LuaScriptModuleAdaptor.h" #include "ramses_adaptor/MaterialAdaptor.h" #include "ramses_adaptor/MeshAdaptor.h" #include "ramses_adaptor/MeshNodeAdaptor.h" @@ -56,6 +57,7 @@ UniqueObjectAdaptor Factories::createAdaptor(SceneAdaptor* sceneAdaptor, core::S {user_types::Mesh::typeDescription.typeName, [](SceneAdaptor* sceneAdaptor, core::SEditorObject obj) { return std::make_unique(sceneAdaptor, std::dynamic_pointer_cast(obj)); }}, {user_types::Texture::typeDescription.typeName, [](SceneAdaptor* sceneAdaptor, core::SEditorObject obj) { return std::make_unique(sceneAdaptor, std::dynamic_pointer_cast(obj)); }}, {user_types::CubeMap::typeDescription.typeName, [](SceneAdaptor* sceneAdaptor, core::SEditorObject obj) { return std::make_unique(sceneAdaptor, std::dynamic_pointer_cast(obj)); }}, + {user_types::LuaScriptModule::typeDescription.typeName, [](SceneAdaptor* sceneAdaptor, core::SEditorObject obj) { return std::make_unique(sceneAdaptor, std::dynamic_pointer_cast(obj)); }}, {user_types::RenderBuffer::typeDescription.typeName, [](SceneAdaptor* sceneAdaptor, core::SEditorObject obj) { return std::make_unique(sceneAdaptor, std::dynamic_pointer_cast(obj)); }}, {user_types::RenderTarget::typeDescription.typeName, [](SceneAdaptor* sceneAdaptor, core::SEditorObject obj) { return std::make_unique(sceneAdaptor, std::dynamic_pointer_cast(obj)); }}, diff --git a/components/libRamsesBase/src/ramses_adaptor/LuaScriptAdaptor.cpp b/components/libRamsesBase/src/ramses_adaptor/LuaScriptAdaptor.cpp index 7e13181f..7e31638b 100644 --- a/components/libRamsesBase/src/ramses_adaptor/LuaScriptAdaptor.cpp +++ b/components/libRamsesBase/src/ramses_adaptor/LuaScriptAdaptor.cpp @@ -9,9 +9,11 @@ */ #include "ramses_adaptor/LuaScriptAdaptor.h" +#include "ramses_adaptor/LuaScriptModuleAdaptor.h" #include "ramses_adaptor/SceneAdaptor.h" #include "ramses_adaptor/utilities.h" #include "ramses_base/LogicEngineFormatter.h" +#include "ramses_base/Utils.h" #include "user_types/PrefabInstance.h" #include "utils/FileUtils.h" @@ -35,7 +37,11 @@ LuaScriptAdaptor::LuaScriptAdaptor(SceneAdaptor* sceneAdaptor, std::shared_ptrdispatcher()->registerOnChildren({editorObject_, &user_types::LuaScript::luaModules_}, [this](auto) { + tagDirty(); + recreateStatus_ = true; + })} { setupParentSubscription(); setupInputValuesSubscription(); } @@ -89,9 +95,20 @@ bool LuaScriptAdaptor::sync(core::Errors* errors) { auto scriptContent = utils::file::read(raco::core::PathQueries::resolveUriPropertyToAbsolutePath(sceneAdaptor_->project(), {editorObject_, &user_types::LuaScript::uri_})); LOG_TRACE(log_system::RAMSES_ADAPTOR, "{}: {}", generateRamsesObjectName(), scriptContent); luaScript_.reset(); + modules.clear(); if (!scriptContent.empty()) { - rlogic::LuaConfig luaConfig; - luaConfig.addStandardModuleDependency(rlogic::EStandardModule::All); + auto luaConfig = raco::ramses_base::defaultLuaConfig(); + + const auto& moduleDeps = editorObject_->luaModules_.asTable(); + for (auto i = 0; i < moduleDeps.size(); ++i) { + if (auto moduleRef = moduleDeps.get(i)->asRef()) { + auto moduleAdaptor = sceneAdaptor_->lookup(moduleRef); + if (auto module = moduleAdaptor->module_) { + modules.emplace_back(module); + luaConfig.addDependency(moduleDeps.name(i), *module); + } + } + } auto ptr = sceneAdaptor_->logicEngine().createLuaScript(scriptContent, luaConfig, generateRamsesObjectName()); LOG_TRACE(log_system::RAMSES_ADAPTOR, "create: {}", fmt::ptr(ptr)); if (ptr) { diff --git a/components/libRamsesBase/src/ramses_adaptor/LuaScriptModuleAdaptor.cpp b/components/libRamsesBase/src/ramses_adaptor/LuaScriptModuleAdaptor.cpp new file mode 100644 index 00000000..83c3af96 --- /dev/null +++ b/components/libRamsesBase/src/ramses_adaptor/LuaScriptModuleAdaptor.cpp @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of Ramses Composer + * (see https://github.com/GENIVI/ramses-composer). + * + * 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 "ramses_adaptor/LuaScriptModuleAdaptor.h" + +#include "ramses_adaptor/SceneAdaptor.h" +#include "ramses_adaptor/utilities.h" +#include "ramses_base/LogicEngineFormatter.h" +#include "user_types/PrefabInstance.h" +#include "utils/FileUtils.h" + +namespace raco::ramses_adaptor { + +LuaScriptModuleAdaptor::LuaScriptModuleAdaptor(SceneAdaptor* sceneAdaptor, raco::user_types::SLuaScriptModule editorObject) + : ObjectAdaptor{sceneAdaptor}, + editorObject_{editorObject}, + nameSubscription_{sceneAdaptor_->dispatcher()->registerOn({editorObject_, &user_types::LuaScriptModule::objectName_}, [this]() { + tagDirty(); + })}, + subscription_{sceneAdaptor_->dispatcher()->registerOnPreviewDirty(editorObject_, [this]() { + tagDirty(); + })} { +} + +raco::user_types::SEditorObject LuaScriptModuleAdaptor::baseEditorObject() noexcept { + return editorObject_; +} +const raco::user_types::SEditorObject LuaScriptModuleAdaptor::baseEditorObject() const noexcept { + return editorObject_; +} + +bool LuaScriptModuleAdaptor::sync(core::Errors* errors) { + ObjectAdaptor::sync(errors); + + const auto& scriptContents = editorObject_->currentScriptContents_; + if (scriptContents.empty()) { + module_.reset(); + } else { + module_ = raco::ramses_base::ramsesLuaModule(scriptContents, &sceneAdaptor_->logicEngine(), editorObject_->objectName()); + } + + tagDirty(false); + return true; +} + +} // namespace raco::ramses_adaptor diff --git a/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp b/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp index 5b091fcf..d148f8f9 100644 --- a/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp +++ b/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp @@ -146,29 +146,33 @@ std::vector SceneBackend::getSceneItemDescriptions( } } - for (const auto* script : logicEngine_->scripts()) { + for (const auto* module : logicEngine_->getCollection()) { + sceneItems.emplace_back("LuaScriptModule", module->getName().data(), -1); + } + + for (const auto* script : logicEngine_->getCollection()) { sceneItems.emplace_back("LuaScript", script->getName().data(), -1); } - for (const auto* dataArray : logicEngine_->dataArrays()) { + for (const auto* dataArray : logicEngine_->getCollection()) { sceneItems.emplace_back("DataArray", dataArray->getName().data(), -1); } - for (const auto* animation : logicEngine_->animationNodes()) { + for (const auto* animation : logicEngine_->getCollection()) { sceneItems.emplace_back("Animation", animation->getName().data(), -1); } - for (const auto* binding : logicEngine_->ramsesAppearanceBindings()) { + for (const auto* binding : logicEngine_->getCollection()) { auto parentIdx = parents[&binding->getRamsesAppearance()]; sceneItems.emplace_back("AppearanceBinding", binding->getName().data(), parentIdx); } - for (const auto* binding : logicEngine_->ramsesNodeBindings()) { + for (const auto* binding : logicEngine_->getCollection()) { auto parentIdx = parents[&binding->getRamsesNode()]; sceneItems.emplace_back("NodeBinding", binding->getName().data(), parentIdx); } - for (const auto* binding : logicEngine_->ramsesCameraBindings()) { + for (const auto* binding : logicEngine_->getCollection()) { auto parentIdx = parents[&binding->getRamsesCamera()]; sceneItems.emplace_back("CameraBinding", binding->getName().data(), parentIdx); } diff --git a/components/libRamsesBase/src/ramses_adaptor/TextureSamplerAdaptor.cpp b/components/libRamsesBase/src/ramses_adaptor/TextureSamplerAdaptor.cpp index bcc92698..1575a174 100644 --- a/components/libRamsesBase/src/ramses_adaptor/TextureSamplerAdaptor.cpp +++ b/components/libRamsesBase/src/ramses_adaptor/TextureSamplerAdaptor.cpp @@ -108,7 +108,7 @@ RamsesTexture2D TextureSamplerAdaptor::createTexture() { ramses::MipLevelData mipLevelData(static_cast(data.size()), data.data()); - return ramses_base::ramsesTexture2D(sceneAdaptor_->scene(), ramses::ETextureFormat::RGBA8, width, height, 1, &mipLevelData, false, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); + return ramses_base::ramsesTexture2D(sceneAdaptor_->scene(), ramses::ETextureFormat::RGBA8, width, height, 1, &mipLevelData, *editorObject()->generateMipmaps_, {}, ramses::ResourceCacheFlag_DoNotCache, nullptr); } RamsesTexture2D TextureSamplerAdaptor::getFallbackTexture() { diff --git a/components/libRamsesBase/src/ramses_base/CoreInterfaceImpl.cpp b/components/libRamsesBase/src/ramses_base/CoreInterfaceImpl.cpp index 8541445b..7c0e9456 100644 --- a/components/libRamsesBase/src/ramses_base/CoreInterfaceImpl.cpp +++ b/components/libRamsesBase/src/ramses_base/CoreInterfaceImpl.cpp @@ -22,8 +22,21 @@ bool CoreInterfaceImpl::parseShader(const std::string& vertexShader, const std:: return raco::ramses_base::parseShaderText(backend_->internalScene(), vertexShader, geometryShader, fragmentShader, shaderDefines, outUniforms, outAttributes, outError); } -bool CoreInterfaceImpl::parseLuaScript(const std::string& luaScript, raco::core::PropertyInterfaceList& outInputs, raco::core::PropertyInterfaceList& outOutputs, std::string& outError) { - return raco::ramses_base::parseLuaScript(backend_->logicEngine(), luaScript, outInputs, outOutputs, outError); +bool CoreInterfaceImpl::parseLuaScript(const std::string& luaScript, const raco::data_storage::Table &modules, raco::core::PropertyInterfaceList& outInputs, raco::core::PropertyInterfaceList& outOutputs, std::string& outError) { + return raco::ramses_base::parseLuaScript(backend_->logicEngine(), luaScript, modules, outInputs, outOutputs, outError); +} + +bool CoreInterfaceImpl::parseLuaScriptModule(const std::string& luaScriptModule, std::string& error) { + return raco::ramses_base::parseLuaScriptModule(backend_->logicEngine(), luaScriptModule, error); +} + +bool CoreInterfaceImpl::extractLuaDependencies(const std::string& luaScript, std::vector& moduleList, std::string& outError) { + auto callback = [&moduleList](const std::string& module) { moduleList.emplace_back(module); }; + auto extractStatus = backend_->logicEngine().extractLuaDependencies(luaScript, callback); + if (!extractStatus) { + outError = backend_->logicEngine().getErrors().at(0).message; + } + return extractStatus; } const std::map& CoreInterfaceImpl::enumerationDescription(raco::core::EngineEnumeration type) const { diff --git a/components/libRamsesBase/src/ramses_base/Utils.cpp b/components/libRamsesBase/src/ramses_base/Utils.cpp index a13d4e57..8e4c1984 100644 --- a/components/libRamsesBase/src/ramses_base/Utils.cpp +++ b/components/libRamsesBase/src/ramses_base/Utils.cpp @@ -9,9 +9,12 @@ */ #include "ramses_base/Utils.h" +#include "user_types/LuaScriptModule.h" +#include "data_storage/Table.h" #include "ramses_base/LogicEngine.h" #include #include +#include #include #include #include @@ -152,9 +155,21 @@ void fillLuaScriptInterface(std::vector &interfac } } // namespace -bool parseLuaScript(LogicEngine &engine, const std::string &luaScript, raco::core::PropertyInterfaceList &outInputs, raco::core::PropertyInterfaceList &outOutputs, std::string &outError) { - rlogic::LuaConfig luaConfig; - luaConfig.addStandardModuleDependency(rlogic::EStandardModule::All); + +bool parseLuaScript(LogicEngine &engine, const std::string &luaScript, const raco::data_storage::Table &modules, raco::core::PropertyInterfaceList &outInputs, raco::core::PropertyInterfaceList &outOutputs, std::string &outError) { + rlogic::LuaConfig luaConfig = defaultLuaConfig(); + std::vector tempModules; + + for (auto i = 0; i < modules.size(); ++i) { + if (auto moduleRef = modules.get(i)->asRef()) { + rlogic::LuaConfig tempConfig = defaultLuaConfig(); + auto tempModule = tempModules.emplace_back(engine.createLuaModule(moduleRef->as()->currentScriptContents_, tempConfig, "Stage::PreprocessModule::" + moduleRef->objectName())); + if (tempModule) { + luaConfig.addDependency(modules.name(i), *tempModule); + } + } + } + auto script = engine.createLuaScript(luaScript, luaConfig, "Stage::PreprocessScript"); if (script) { if (auto inputs = script->getInputs()) { @@ -164,13 +179,40 @@ bool parseLuaScript(LogicEngine &engine, const std::string &luaScript, raco::cor fillLuaScriptInterface(outOutputs, outputs); } engine.destroy(*script); + for (auto module : tempModules) { + if (module) { + engine.destroy(*module); + } + } return true; } else { outError = engine.getErrors().at(0).message; + for (auto module : tempModules) { + if (module) { + engine.destroy(*module); + } + } return false; } } +bool parseLuaScriptModule(LogicEngine &engine, const std::string &luaScriptModule, std::string &outError) { + rlogic::LuaConfig tempConfig = defaultLuaConfig(); + if (auto tempModule = engine.createLuaModule(luaScriptModule, tempConfig, "Stage::PreprocessModule")) { + engine.destroy(*tempModule); + return true; + } else { + outError = engine.getErrors().at(0).message; + return false; + } +} + +rlogic::LuaConfig defaultLuaConfig() { + rlogic::LuaConfig config; + config.addStandardModuleDependency(rlogic::EStandardModule::All); + return config; +} + ramses::RamsesVersion getRamsesVersion() { return ramses::GetRamsesVersion(); } diff --git a/components/libRamsesBase/tests/AnimationAdaptor_test.cpp b/components/libRamsesBase/tests/AnimationAdaptor_test.cpp index 05149e0b..82dea29b 100644 --- a/components/libRamsesBase/tests/AnimationAdaptor_test.cpp +++ b/components/libRamsesBase/tests/AnimationAdaptor_test.cpp @@ -26,8 +26,8 @@ TEST_F(AnimationAdaptorTest, defaultConstruction) { dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_STREQ(sceneContext.logicEngine().animationNodes().begin()->getName().data(), raco::ramses_adaptor::defaultAnimationName); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_STREQ(sceneContext.logicEngine().getCollection().begin()->getName().data(), raco::ramses_adaptor::defaultAnimationName); } TEST_F(AnimationAdaptorTest, defaultConstruction_2_empty_anims) { @@ -36,8 +36,8 @@ TEST_F(AnimationAdaptorTest, defaultConstruction_2_empty_anims) { dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_STREQ(sceneContext.logicEngine().animationNodes().begin()->getName().data(), raco::ramses_adaptor::defaultAnimationName); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_STREQ(sceneContext.logicEngine().getCollection().begin()->getName().data(), raco::ramses_adaptor::defaultAnimationName); } TEST_F(AnimationAdaptorTest, animNode_Creation) { @@ -54,7 +54,7 @@ TEST_F(AnimationAdaptorTest, animNode_Creation) { context.set({anim, {"animationChannels", "Channel 0"}}, animChannel); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Name"), nullptr); } @@ -75,8 +75,8 @@ TEST_F(AnimationAdaptorTest, animNode_Deletion) { context.set({anim, {"animationChannels", "Channel 0"}}, SEditorObject{}); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_STREQ(sceneContext.logicEngine().animationNodes().begin()->getName().data(), raco::ramses_adaptor::defaultAnimationName); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_STREQ(sceneContext.logicEngine().getCollection().begin()->getName().data(), raco::ramses_adaptor::defaultAnimationName); } TEST_F(AnimationAdaptorTest, animNode_animName) { @@ -96,7 +96,7 @@ TEST_F(AnimationAdaptorTest, animNode_animName) { context.set({anim, {"objectName"}}, "Changed"); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); ASSERT_NE(select(sceneContext.logicEngine(), "Changed"), nullptr); } @@ -120,10 +120,10 @@ TEST_F(AnimationAdaptorTest, prefab_noAnimNode) { auto prefab = create("Prefab"); dispatch(); - commandInterface.moveScenegraphChild(anim, prefab); + commandInterface.moveScenegraphChildren({anim}, prefab); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); } TEST_F(AnimationAdaptorTest, animNode_multiple_channels) { @@ -176,7 +176,7 @@ TEST_F(AnimationAdaptorTest, afterSync_dataArrays_get_cleaned_up) { context.set({anim, {"animationChannels", "Channel 0"}}, animChannel); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 4); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 4); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.timestamps"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.tangentIn"), nullptr); @@ -187,7 +187,7 @@ TEST_F(AnimationAdaptorTest, afterSync_dataArrays_get_cleaned_up) { dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 2); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 2); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.timestamps"), nullptr); ASSERT_EQ(select(sceneContext.logicEngine(), "Animation Sampler Name.tangentIn"), nullptr); @@ -221,7 +221,7 @@ end context.set({luaScript, {"luaInputs", "in_value"}}, true); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().begin()->getInputs()->getChild("play")->get(), true); + ASSERT_EQ(sceneContext.logicEngine().getCollection().begin()->getInputs()->getChild("play")->get(), true); } TEST_F(AnimationAdaptorTest, link_with_meshNode_mesh_changed) { @@ -390,8 +390,8 @@ TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_link_inside_prefabins commandInterface.set({mesh, &raco::user_types::AnimationChannel::uri_}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(anim, prefab); - commandInterface.moveScenegraphChild(meshNode, prefab); + commandInterface.moveScenegraphChildren({anim}, prefab); + commandInterface.moveScenegraphChildren({meshNode}, prefab); commandInterface.set({anim, {"animationChannels", "Channel 1"}}, animChannel); commandInterface.set({anim, &raco::user_types::Animation::play_}, true); commandInterface.set({anim, &raco::user_types::Animation::loop_}, true); @@ -429,12 +429,12 @@ TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_gets_propag commandInterface.set({animChannel, &raco::user_types::AnimationChannel::uri_}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(anim, prefab); + commandInterface.moveScenegraphChildren({anim}, prefab); commandInterface.set({anim, {"animationChannels", "Channel 1"}}, animChannel); commandInterface.set({prefabInstance, {"template"}}, prefab); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); commandInterface.set({anim, &raco::user_types::Animation::play_}, true); dispatch(); @@ -443,8 +443,8 @@ TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_gets_propag dispatch(); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_NE(sceneContext.logicEngine().animationNodes().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_NE(sceneContext.logicEngine().getCollection().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); } TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_pause_gets_propagated) { @@ -458,12 +458,12 @@ TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_pause_gets_ commandInterface.set({animChannel, &raco::user_types::AnimationChannel::uri_}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(anim, prefab); + commandInterface.moveScenegraphChildren({anim}, prefab); commandInterface.set({anim, {"animationChannels", "Channel 1"}}, animChannel); commandInterface.set({prefabInstance, {"template"}}, prefab); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); commandInterface.set({anim, &raco::user_types::Animation::play_}, true); dispatch(); @@ -472,14 +472,14 @@ TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_pause_gets_ dispatch(); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_NE(sceneContext.logicEngine().animationNodes().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_NE(sceneContext.logicEngine().getCollection().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); commandInterface.set({anim, &raco::user_types::Animation::play_}, false); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_NE(sceneContext.logicEngine().animationNodes().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_NE(sceneContext.logicEngine().getCollection().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); } TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_stop_gets_propagated) { @@ -493,12 +493,12 @@ TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_stop_gets_p commandInterface.set({animChannel, &raco::user_types::AnimationChannel::uri_}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(anim, prefab); + commandInterface.moveScenegraphChildren({anim}, prefab); commandInterface.set({anim, {"animationChannels", "Channel 1"}}, animChannel); commandInterface.set({prefabInstance, {"template"}}, prefab); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); commandInterface.set({anim, &raco::user_types::Animation::play_}, true); commandInterface.set({anim, &raco::user_types::Animation::rewindOnStop_}, true); @@ -508,12 +508,12 @@ TEST_F(AnimationAdaptorTest, anim_in_prefab_prefabinstance_animation_stop_gets_p dispatch(); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_NE(sceneContext.logicEngine().animationNodes().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_NE(sceneContext.logicEngine().getCollection().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); commandInterface.set({anim, &raco::user_types::Animation::play_}, false); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().size(), 1); - ASSERT_EQ(sceneContext.logicEngine().animationNodes().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().begin()->getOutputs()->getChild("progress")->get().value(), 0.0); } \ No newline at end of file diff --git a/components/libRamsesBase/tests/AnimationChannelAdaptor_test.cpp b/components/libRamsesBase/tests/AnimationChannelAdaptor_test.cpp index 03c00e41..f9f72335 100644 --- a/components/libRamsesBase/tests/AnimationChannelAdaptor_test.cpp +++ b/components/libRamsesBase/tests/AnimationChannelAdaptor_test.cpp @@ -22,7 +22,7 @@ TEST_F(AnimationChannelAdaptorTest, defaultConstruction) { dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); } TEST_F(AnimationChannelAdaptorTest, validAnim_validSampler_dataArrays) { @@ -34,7 +34,7 @@ TEST_F(AnimationChannelAdaptorTest, validAnim_validSampler_dataArrays) { commandInterface.set({animChannel, &raco::user_types::AnimationChannel::uri_}, uriPath); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 2); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 2); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.timestamps"), nullptr); } @@ -51,7 +51,7 @@ TEST_F(AnimationChannelAdaptorTest, validAnim_validSampler_dataArrays_rename_obj context.set({animChannel, {"objectName"}}, std::string("Changed")); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 2); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 2); ASSERT_NE(select(sceneContext.logicEngine(), "Changed.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Changed.timestamps"), nullptr); } @@ -72,7 +72,7 @@ TEST_F(AnimationChannelAdaptorTest, validAnim_validSampler_animAssigned_dataArra context.set({animChannel, {"objectName"}}, std::string("Changed")); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 2); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 2); ASSERT_NE(select(sceneContext.logicEngine(), "Changed.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Changed.timestamps"), nullptr); } @@ -89,7 +89,7 @@ TEST_F(AnimationChannelAdaptorTest, validAnim_invalidSampler_noDataArrays) { context.set({animChannel, &raco::user_types::AnimationChannel::samplerIndex_}, -1); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); } TEST_F(AnimationChannelAdaptorTest, validAnim_invalidSampler_animAssigned_defaultDataArrays) { @@ -108,7 +108,7 @@ TEST_F(AnimationChannelAdaptorTest, validAnim_invalidSampler_animAssigned_defaul context.set({animChannel, &raco::user_types::AnimationChannel::samplerIndex_}, -1); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 2); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 2); ASSERT_NE(select(sceneContext.logicEngine(), raco::ramses_adaptor::defaultAnimationChannelKeyframesName), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), raco::ramses_adaptor::defaultAnimationChannelTimestampsName), nullptr); } @@ -125,7 +125,7 @@ TEST_F(AnimationChannelAdaptorTest, invalidAnim_invalidSampler_noDataArrays) { context.set({animChannel, &raco::user_types::AnimationChannel::animationIndex_}, -1); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); } TEST_F(AnimationChannelAdaptorTest, noAnim_noDataArrays) { @@ -137,7 +137,7 @@ TEST_F(AnimationChannelAdaptorTest, noAnim_noDataArrays) { commandInterface.set({animChannel, &raco::user_types::AnimationChannel::uri_}, uriPath); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); } TEST_F(AnimationChannelAdaptorTest, interpolationTest_dynamicDataArrays) { @@ -152,7 +152,7 @@ TEST_F(AnimationChannelAdaptorTest, interpolationTest_dynamicDataArrays) { context.set({animChannel, &raco::user_types::AnimationChannel::animationIndex_}, 4); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 4); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 4); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.timestamps"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.tangentIn"), nullptr); @@ -161,7 +161,7 @@ TEST_F(AnimationChannelAdaptorTest, interpolationTest_dynamicDataArrays) { context.set({animChannel, &raco::user_types::AnimationChannel::animationIndex_}, 3); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 2); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 2); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.timestamps"), nullptr); ASSERT_EQ(select(sceneContext.logicEngine(), "Animation Sampler Name.tangentIn"), nullptr); @@ -182,7 +182,7 @@ TEST_F(AnimationChannelAdaptorTest, mesh_baked_flag_true_anim_data_gets_imported commandInterface.set({animChannel, &raco::user_types::AnimationChannel::uri_}, uriPath); dispatch(); - ASSERT_EQ(sceneContext.logicEngine().dataArrays().size(), 2); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 2); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.keyframes"), nullptr); ASSERT_NE(select(sceneContext.logicEngine(), "Animation Sampler Name.timestamps"), nullptr); } \ No newline at end of file diff --git a/components/libRamsesBase/tests/CMakeLists.txt b/components/libRamsesBase/tests/CMakeLists.txt index 388ea0af..ae0ed0e6 100644 --- a/components/libRamsesBase/tests/CMakeLists.txt +++ b/components/libRamsesBase/tests/CMakeLists.txt @@ -16,6 +16,7 @@ set(TEST_SOURCES RamsesBaseFixture.h LinkAdaptor_test.cpp LuaScriptAdaptor_test.cpp + LuaScriptModuleAdaptor_test.cpp MaterialAdaptor_test.cpp MeshAdaptor_test.cpp MeshNodeAdaptor_test.cpp diff --git a/components/libRamsesBase/tests/LinkAdaptor_test.cpp b/components/libRamsesBase/tests/LinkAdaptor_test.cpp index 4827a16f..ed94b107 100644 --- a/components/libRamsesBase/tests/LinkAdaptor_test.cpp +++ b/components/libRamsesBase/tests/LinkAdaptor_test.cpp @@ -237,7 +237,7 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_EQ(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); auto rotationProperty = nodeBinding->getInputs()->getChild("rotation"); ASSERT_EQ(rotationProperty->getType(), rlogic::EPropertyType::Vec4f); @@ -283,7 +283,7 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_NE(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); auto rotationProperty = nodeBinding->getInputs()->getChild("rotation"); ASSERT_EQ(rotationProperty->getType(), rlogic::EPropertyType::Vec3f); @@ -329,7 +329,7 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_EQ(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); auto rotationProperty = nodeBinding->getInputs()->getChild("rotation"); ASSERT_EQ(rotationProperty->getType(), rlogic::EPropertyType::Vec4f); @@ -372,7 +372,7 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_NE(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); } @@ -411,7 +411,7 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_EQ(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); } @@ -448,7 +448,7 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_EQ(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); } @@ -504,14 +504,14 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); ASSERT_FALSE(commandInterface.project()->links()[0]->isValid()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_NE(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); context.set({luaScriptOut, {"uri"}}, (cwd_path() / "lua_script_out2.lua").string()); ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); ASSERT_TRUE(commandInterface.project()->links()[0]->isValid()); - nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_NE(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); } @@ -556,13 +556,13 @@ end ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); ASSERT_FALSE(commandInterface.project()->links()[0]->isValid()); - auto nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + auto nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_NE(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); context.set({luaScriptOut, {"uri"}}, (cwd_path() / "lua_script_out1.lua").string()); ASSERT_NO_FATAL_FAILURE(dispatch()); ASSERT_TRUE(backend.logicEngine().update()); ASSERT_TRUE(commandInterface.project()->links()[0]->isValid()); - nodeBinding = backend.logicEngine().findNodeBinding("node_NodeBinding"); + nodeBinding = backend.logicEngine().findByName("node_NodeBinding"); ASSERT_EQ(nodeBinding->getRotationType(), rlogic::ERotationType::Quaternion); } \ No newline at end of file diff --git a/components/libRamsesBase/tests/LuaScriptAdaptor_test.cpp b/components/libRamsesBase/tests/LuaScriptAdaptor_test.cpp index 0b566f18..1da41e29 100644 --- a/components/libRamsesBase/tests/LuaScriptAdaptor_test.cpp +++ b/components/libRamsesBase/tests/LuaScriptAdaptor_test.cpp @@ -470,9 +470,9 @@ end commandInterface.set({luaScriptChild, {"uri"}}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(node, prefab); - commandInterface.moveScenegraphChild(luaScriptChild, node); - commandInterface.moveScenegraphChild(luaScriptTopLevel, prefab); + commandInterface.moveScenegraphChildren({node}, prefab); + commandInterface.moveScenegraphChildren({luaScriptChild}, node); + commandInterface.moveScenegraphChildren({luaScriptTopLevel}, prefab); dispatch(); auto engineObj{select(sceneContext.logicEngine(), "PrefabInstance.LuaScript Name")}; @@ -484,7 +484,7 @@ end engineObj = select(sceneContext.logicEngine(), "Child LuaScript Name"); ASSERT_TRUE(engineObj != nullptr); - commandInterface.moveScenegraphChild(luaScriptTopLevel, {}); + commandInterface.moveScenegraphChildren({luaScriptTopLevel}, {}); dispatch(); engineObj = select(sceneContext.logicEngine(), "PrefabInstance.LuaScript Name"); @@ -493,13 +493,13 @@ end engineObj = select(sceneContext.logicEngine(), "LuaScript Name"); ASSERT_TRUE(engineObj != nullptr); - commandInterface.moveScenegraphChild(luaScriptChild, prefab); + commandInterface.moveScenegraphChildren({luaScriptChild}, prefab); dispatch(); engineObj = select(sceneContext.logicEngine(), "PrefabInstance.Child LuaScript Name"); ASSERT_TRUE(engineObj != nullptr); - commandInterface.moveScenegraphChild(luaScriptChild, node); + commandInterface.moveScenegraphChildren({luaScriptChild}, node); dispatch(); engineObj = select(sceneContext.logicEngine(), "PrefabInstance.Child LuaScript Name"); @@ -532,7 +532,7 @@ end commandInterface.set({luaScript, {"uri"}}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(luaScript, prefab); + commandInterface.moveScenegraphChildren({luaScript}, prefab); dispatch(); commandInterface.set({prefabInst, {"objectName"}}, std::string("New PrefabInstance")); @@ -570,7 +570,7 @@ end commandInterface.set({luaScript, {"uri"}}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(luaScript, prefab); + commandInterface.moveScenegraphChildren({luaScript}, prefab); dispatch(); auto copiedObjs = commandInterface.copyObjects({prefabInst, luaScript}, false); @@ -607,7 +607,7 @@ end commandInterface.set({luaScript, {"uri"}}, uriPath); dispatch(); - commandInterface.moveScenegraphChild(luaScript, prefab); + commandInterface.moveScenegraphChildren({luaScript}, prefab); dispatch(); commandInterface.set({prefabInst, {"objectName"}}, std::string("New PrefabInstance")); diff --git a/components/libRamsesBase/tests/LuaScriptModuleAdaptor_test.cpp b/components/libRamsesBase/tests/LuaScriptModuleAdaptor_test.cpp new file mode 100644 index 00000000..3a0d8760 --- /dev/null +++ b/components/libRamsesBase/tests/LuaScriptModuleAdaptor_test.cpp @@ -0,0 +1,323 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of Ramses Composer + * (see https://github.com/GENIVI/ramses-composer). + * + * 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 + +#include "RamsesBaseFixture.h" +#include "ramses_adaptor/LuaScriptModuleAdaptor.h" + +using namespace raco::user_types; + +class LuaScriptModuleAdaptorTest : public RamsesBaseFixture<> { +protected: + TextFile generateModule(const std::string& moduleName) { + return makeFile(fmt::format("{}.lua", moduleName), + "local " + moduleName + "Module = {}\n\nreturn " + moduleName + "Module"); + } + + TextFile generateLuaScript(const std::string& fileName) { + return makeFile(fmt::format("{}.lua", fileName), + R"( +modules("neededModule") + +function interface() +end + +function run() +end +)"); + } +}; + +TEST_F(LuaScriptModuleAdaptorTest, defaultConstruction) { + context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); +} + +TEST_F(LuaScriptModuleAdaptorTest, invalidModule) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + + dispatch(); + + auto moduleFile = generateLuaScript("script"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + ASSERT_TRUE(commandInterface.errors().hasError(module)); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_NE(select(sceneContext.logicEngine(), "Module"), nullptr); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_unassign) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, std::string()); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_rename_obj) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + context.set({module, &LuaScriptModule::objectName_}, std::string("Changed")); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_NE(select(sceneContext.logicEngine(), "Changed"), nullptr); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_validLua_moduleAssigned_rename_obj) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + auto script = context.createObject(LuaScript::typeDescription.typeName, "Script"); + auto scriptFile = generateLuaScript("script"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, moduleFile); + dispatch(); + + commandInterface.set({script, {"luaModules", "neededModule"}}, module); + dispatch(); + + context.set({module, {"objectName"}}, std::string("Changed")); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_NE(select(sceneContext.logicEngine(), "Changed"), nullptr); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_validLua_moduleAssigned_delete_uri_noLuaModules) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + auto script = context.createObject(LuaScript::typeDescription.typeName, "Script"); + auto scriptFile = generateLuaScript("script"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, moduleFile); + dispatch(); + + commandInterface.set({script, {"luaModules", "neededModule"}}, module); + dispatch(); + + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, std::string()); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_variousLuaScripts_noModuleCopies) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + auto script1 = context.createObject(LuaScript::typeDescription.typeName, "Script1"); + auto script2 = context.createObject(LuaScript::typeDescription.typeName, "Script2"); + auto script3 = context.createObject(LuaScript::typeDescription.typeName, "Script3"); + auto scriptFile1 = generateLuaScript("script1"); + auto scriptFile2 = generateLuaScript("script2"); + auto scriptFile3 = generateLuaScript("script3"); + commandInterface.set({script1, &raco::user_types::LuaScript::uri_}, scriptFile1); + commandInterface.set({script2, &raco::user_types::LuaScript::uri_}, scriptFile2); + commandInterface.set({script3, &raco::user_types::LuaScript::uri_}, scriptFile3); + + dispatch(); + + commandInterface.set({script1, {"luaModules", "neededModule"}}, module); + commandInterface.set({script2, {"luaModules", "neededModule"}}, module); + commandInterface.set({script3, {"luaModules", "neededModule"}}, module); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_invalidLuaScript_syntaxError) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + auto script = context.createObject(LuaScript::typeDescription.typeName, "Script"); + auto scriptFile = generateLuaScript("script1"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, scriptFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); + + commandInterface.set({script, {"luaModules", "neededModule"}}, module); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + + auto errorScriptFile = makeFile("error.lua", + R"( +modules("neededModule") + +function interface() +error +end + +function run() +end +)"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, errorScriptFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); + + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, std::string()); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_invalidLuaScript_runtimeError) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + auto script = context.createObject(LuaScript::typeDescription.typeName, "Script"); + auto scriptFile = generateLuaScript("script1"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, scriptFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); + + commandInterface.set({script, {"luaModules", "neededModule"}}, module); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + + auto errorScriptFile = makeFile("error.lua", + R"( +modules("neededModule") + +function interface() +IN.val = VEC3F +OUT.val = INT +end + +function run() +OUT.val = IN.val +end +)"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, errorScriptFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, std::string()); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); +} + +TEST_F(LuaScriptModuleAdaptorTest, validModule_invalidLuaScript_thenNoModuleAndValidScriptruntimeError) { + auto module = context.createObject(LuaScriptModule::typeDescription.typeName, "Module"); + dispatch(); + + auto moduleFile = generateModule("coalas"); + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + dispatch(); + + auto script = context.createObject(LuaScript::typeDescription.typeName, "Script"); + auto scriptFile = generateLuaScript("script1"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, scriptFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); + + commandInterface.set({script, {"luaModules", "neededModule"}}, module); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + + auto errorScriptFile = makeFile("error.lua", + R"( +modules("neededModule") + +function interface() +IN.val = VEC3F +OUT.val = INT +end + +function run() +OUT.val = IN.val +end +)"); + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, errorScriptFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 1); + + commandInterface.set({module, &raco::user_types::LuaScriptModule::uri_}, std::string()); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); + + + commandInterface.set({script, &raco::user_types::LuaScript::uri_}, scriptFile); + dispatch(); + + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); + ASSERT_EQ(sceneContext.logicEngine().getCollection().size(), 0); +} \ No newline at end of file diff --git a/components/libRamsesBase/tests/RamsesBaseFixture.h b/components/libRamsesBase/tests/RamsesBaseFixture.h index 37596f54..402f2fea 100644 --- a/components/libRamsesBase/tests/RamsesBaseFixture.h +++ b/components/libRamsesBase/tests/RamsesBaseFixture.h @@ -35,16 +35,7 @@ inline std::vector select(const ramses::Scene& scene, ramses::ERamsesObjectT template inline const T* select(const rlogic::LogicEngine& engine, const char* name) { - if constexpr (std::is_same_v) { - return engine.findScript(name); - } else if constexpr (std::is_same_v) { - return engine.findDataArray(name); - } else if constexpr (std::is_same_v) { - return engine.findAnimationNode(name); - } else { - static_assert(std::is_same::value); - } - return nullptr; + return engine.findByName(name); } template @@ -70,7 +61,7 @@ class RamsesBaseFixture : public TestEnvironmentCoreT { raco::ramses_adaptor::SceneAdaptor sceneContext; void dispatch() { - for (auto animNode : sceneContext.logicEngine().animationNodes()) { + for (auto animNode : sceneContext.logicEngine().getCollection()) { // arbitrary 17 ms update animNode->getInputs()->getChild("timeDelta")->set(0.017f); } diff --git a/components/libRamsesBase/tests/RamsesLogic_test.cpp b/components/libRamsesBase/tests/RamsesLogic_test.cpp index c45f3a37..dcf95f12 100644 --- a/components/libRamsesBase/tests/RamsesLogic_test.cpp +++ b/components/libRamsesBase/tests/RamsesLogic_test.cpp @@ -10,6 +10,7 @@ #include +#include "ramses_base/Utils.h" #include #include #include @@ -394,8 +395,7 @@ function run() end )"}; - rlogic::LuaConfig luaConfig; - luaConfig.addStandardModuleDependency(rlogic::EStandardModule::All); + auto luaConfig = raco::ramses_base::defaultLuaConfig(); auto* script = logicEngine.createLuaScript(scriptContentFloat, luaConfig); const float pi = static_cast(std::acos(-1.0)); diff --git a/components/libRamsesBase/tests/RenderLayerAdaptor_test.cpp b/components/libRamsesBase/tests/RenderLayerAdaptor_test.cpp index 4777ac3a..4f65028f 100644 --- a/components/libRamsesBase/tests/RenderLayerAdaptor_test.cpp +++ b/components/libRamsesBase/tests/RenderLayerAdaptor_test.cpp @@ -112,8 +112,8 @@ TEST_F(RenderLayerAdaptorTest, renderables_meshnode_root_add_layer_renderable) { TEST_F(RenderLayerAdaptorTest, renderables_meshnode_move_scenegraph_child) { auto root = create("root", nullptr, {"render_main"}); - auto meshnode = create("meshnode", root); - auto layer = create_layer("layer", {}, {{"render_main",0}}); + auto meshNode = create("meshnode", root); + auto layer = create_layer("layer", {}, {{"render_main", 0}}); dispatch(); @@ -122,12 +122,12 @@ TEST_F(RenderLayerAdaptorTest, renderables_meshnode_move_scenegraph_child) { ASSERT_TRUE(engineGroup->containsMeshNode(*engineMeshNode)); - context.moveScenegraphChild(meshnode, nullptr); + context.moveScenegraphChildren({meshNode}, nullptr); dispatch(); ASSERT_FALSE(engineGroup->containsMeshNode(*engineMeshNode)); - context.moveScenegraphChild(meshnode, root); + context.moveScenegraphChildren({meshNode}, root); dispatch(); ASSERT_TRUE(engineGroup->containsMeshNode(*engineMeshNode)); diff --git a/components/libRamsesBase/tests/SceneContext_test.cpp b/components/libRamsesBase/tests/SceneContext_test.cpp index 2d132cc2..a453c823 100644 --- a/components/libRamsesBase/tests/SceneContext_test.cpp +++ b/components/libRamsesBase/tests/SceneContext_test.cpp @@ -83,7 +83,7 @@ TEST_F(SceneContextTest, construction_createMeshNodeWithMesh) { context.set(raco::core::ValueHandle{material, {"uriVertex"}}, (cwd_path() / "shaders/simple_texture.vert").string()); context.set(raco::core::ValueHandle{material, {"uriFragment"}}, (cwd_path() / "shaders/simple_texture.frag").string()); - context.moveScenegraphChild(meshNode, node); + context.moveScenegraphChildren({meshNode}, node); context.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); context.set(raco::core::ValueHandle{meshNode, {"materials", "material", "material"}}, material); @@ -139,7 +139,7 @@ TEST_F(SceneContextTest, construction_createMeshNodeWithMeshThenUnassignMesh) { context.set(raco::core::ValueHandle{mesh, {"uri"}}, (cwd_path() / "meshes/Duck.glb").string()); - context.moveScenegraphChild(meshNode, node); + context.moveScenegraphChildren({meshNode}, node); context.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); context.set(raco::core::ValueHandle{meshNode, {"materials", "material", "material"}}, material); dispatch(); @@ -166,7 +166,7 @@ TEST_F(SceneContextTest, construction_createMeshNodeWithMeshThenUnassignMesh) { TEST_F(SceneContextTest, construction_createSceneWithSimpleHierarchy) { auto parent = context.createObject(Node::typeDescription.typeName); auto child = context.createObject(MeshNode::typeDescription.typeName); - context.moveScenegraphChild({child}, {parent}); + context.moveScenegraphChildren({child}, {parent}); dispatch(); auto sceneNodes{select(*sceneContext.scene(), ramses::ERamsesObjectType::ERamsesObjectType_Node)}; @@ -180,7 +180,7 @@ TEST_F(SceneContextTest, construction_createSceneWithSimpleHierarchy) { TEST_F(SceneContextTest, construction_createSceneWithSimpleHierarchy_reverseNodeCreation) { auto child = context.createObject(MeshNode::typeDescription.typeName); auto parent = context.createObject(Node::typeDescription.typeName); - context.moveScenegraphChild({child}, {parent}); + context.moveScenegraphChildren({child}, {parent}); dispatch(); auto nodeSceneElements(select(*sceneContext.scene(), ramses::ERamsesObjectType_Node)); @@ -193,10 +193,10 @@ TEST_F(SceneContextTest, construction_createSceneWithSimpleHierarchy_reverseNode TEST_F(SceneContextTest, construction_createSceneWithDeeperHierarchy) { auto root = context.createObject(Node::typeDescription.typeName, "root"); - auto child_1 = context.createObject(Node::typeDescription.typeName, "child_1"); - auto child_2 = context.createObject(MeshNode::typeDescription.typeName, "child_2"); - context.moveScenegraphChild({child_1}, {root}); - context.moveScenegraphChild({child_2}, {child_1}); + auto child1 = context.createObject(Node::typeDescription.typeName, "child1"); + auto child2 = context.createObject(MeshNode::typeDescription.typeName, "child2"); + context.moveScenegraphChildren({child1}, {root}); + context.moveScenegraphChildren({child2}, {child1}); dispatch(); auto nodeSceneElements(select(*sceneContext.scene(), ramses::ERamsesObjectType_Node)); @@ -206,7 +206,7 @@ TEST_F(SceneContextTest, construction_createSceneWithDeeperHierarchy) { EXPECT_EQ(meshNodeSceneElements.size(), 1); // Check and retrive proper ramses elements - auto ramsesChild1 = select(*sceneContext.scene(), "child_1"); + auto ramsesChild1 = select(*sceneContext.scene(), "child1"); EXPECT_TRUE(ramsesChild1); auto ramsesRoot = select(*sceneContext.scene(), "root"); EXPECT_TRUE(ramsesRoot); @@ -218,11 +218,11 @@ TEST_F(SceneContextTest, construction_createSceneWithDeeperHierarchy) { } TEST_F(SceneContextTest, construction_createSceneWithDeeperHierarchy_reverseNodeCreation) { - auto child_2 = context.createObject(MeshNode::typeDescription.typeName, "child_2"); - auto child_1 = context.createObject(Node::typeDescription.typeName, "child_1"); + auto child2 = context.createObject(MeshNode::typeDescription.typeName, "child2"); + auto child1 = context.createObject(Node::typeDescription.typeName, "child1"); auto root = context.createObject(Node::typeDescription.typeName, "root"); - context.moveScenegraphChild({child_1}, {root}); - context.moveScenegraphChild({child_2}, {child_1}); + context.moveScenegraphChildren({child1}, {root}); + context.moveScenegraphChildren({child2}, {child1}); dispatch(); auto nodeSceneElements(select(*sceneContext.scene(), ramses::ERamsesObjectType_Node)); @@ -232,7 +232,7 @@ TEST_F(SceneContextTest, construction_createSceneWithDeeperHierarchy_reverseNode EXPECT_EQ(meshNodeSceneElements.size(), 1); // Check and retrive proper ramses elements - auto ramsesChild1 = select(*sceneContext.scene(), "child_1"); + auto ramsesChild1 = select(*sceneContext.scene(), "child1"); EXPECT_TRUE(ramsesChild1); auto ramsesRoot = select(*sceneContext.scene(), "root"); EXPECT_TRUE(ramsesRoot); @@ -256,7 +256,7 @@ TEST_F(SceneContextTest, dataChange_dynamicInsert_childNode) { dispatch(); auto child = context.createObject(MeshNode::typeDescription.typeName); - context.moveScenegraphChild({child}, {root}); + context.moveScenegraphChildren({child}, {root}); dispatch(); auto nodeSceneElements(select(*sceneContext.scene(), ramses::ERamsesObjectType_Node)); @@ -282,10 +282,10 @@ TEST_F(SceneContextTest, dataChange_dynamicReparenting_move) { auto parent_1 = context.createObject(Node::typeDescription.typeName, "Parent 1"); auto parent_2 = context.createObject(Node::typeDescription.typeName, "Parent 2"); auto child = context.createObject(MeshNode::typeDescription.typeName, "Child"); - context.moveScenegraphChild({child}, {parent_1}); + context.moveScenegraphChildren({child}, {parent_1}); dispatch(); - context.moveScenegraphChild({child}, {parent_2}); + context.moveScenegraphChildren({child}, {parent_2}); dispatch(); auto nodeSceneElements(select(*sceneContext.scene(), ramses::ERamsesObjectType_Node)); @@ -302,10 +302,10 @@ TEST_F(SceneContextTest, dataChange_dynamicReparenting_move) { TEST_F(SceneContextTest, dataChange_dynamicReparenting_noParent) { auto parent_1 = context.createObject(Node::typeDescription.typeName, "Parent 1"); auto child = context.createObject(MeshNode::typeDescription.typeName, "Child"); - context.moveScenegraphChild({child}, {parent_1}); + context.moveScenegraphChildren({child}, {parent_1}); dispatch(); - context.moveScenegraphChild({child}, {}); + context.moveScenegraphChildren({child}, {}); dispatch(); auto nodeSceneElements(select(*sceneContext.scene(), ramses::ERamsesObjectType_Node)); @@ -320,11 +320,11 @@ TEST_F(SceneContextTest, dataChange_dynamicReparenting_noParent) { TEST_F(SceneContextTest, construction_createSceneWithDeeperHierarchy_reverseNodeCreation2) { auto rootNode = context.createObject(Node::typeDescription.typeName, "Root", "root1"); auto childNode = context.createObject(Node::typeDescription.typeName, "Child1", "child1"); - auto child2Node = context.createObject(Node::typeDescription.typeName, "Child2", "child2"); + auto childNode2 = context.createObject(Node::typeDescription.typeName, "Child2", "child2"); auto child21Node = context.createObject(Node::typeDescription.typeName, "Child2_1", "child2_1"); - context.moveScenegraphChild({child21Node}, {child2Node}); - context.moveScenegraphChild({child2Node}, {rootNode}); - context.moveScenegraphChild({childNode}, {rootNode}); + context.moveScenegraphChildren({child21Node}, {childNode2}); + context.moveScenegraphChildren({childNode2}, {rootNode}); + context.moveScenegraphChildren({childNode}, {rootNode}); dispatch(); } @@ -394,7 +394,7 @@ TEST_P(SceneContextParamTestFixture, contextCreationOrder_init) { context.set(raco::core::ValueHandle{mesh, {"uri"}}, (cwd_path() / "meshes/Duck.glb").string()); - context.moveScenegraphChild(meshNode, node); + context.moveScenegraphChildren({meshNode}, node); context.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); context.set(raco::core::ValueHandle{meshNode, {"materials", "material", "material"}}, material); @@ -417,7 +417,7 @@ TEST_P(SceneContextParamTestFixture, contextCreationOrder_dispatch) { context.set(raco::core::ValueHandle{mesh, {"uri"}}, (cwd_path() / "meshes/Duck.glb").string()); - context.moveScenegraphChild(meshNode, node); + context.moveScenegraphChildren({meshNode}, node); context.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); context.set(raco::core::ValueHandle{meshNode, {"materials", "material", "material"}}, material); diff --git a/components/libRamsesBase/tests/Utils_test.cpp b/components/libRamsesBase/tests/Utils_test.cpp index 9f4203b0..584ea67b 100644 --- a/components/libRamsesBase/tests/Utils_test.cpp +++ b/components/libRamsesBase/tests/Utils_test.cpp @@ -32,7 +32,7 @@ end std::string error; raco::core::PropertyInterfaceList in; raco::core::PropertyInterfaceList out; - parseLuaScript(backend.logicEngine(), script, in, out, error); + parseLuaScript(backend.logicEngine(), script, {}, in, out, error); EXPECT_EQ(1, in.size()); const auto& structProperty = in.at(0); @@ -63,7 +63,7 @@ end std::string error; raco::core::PropertyInterfaceList in; raco::core::PropertyInterfaceList out; - parseLuaScript(backend.logicEngine(), script, in, out, error); + parseLuaScript(backend.logicEngine(), script, {}, in, out, error); EXPECT_EQ(1, in.size()); EXPECT_EQ(EnginePrimitive::Array, in.at(0).type); diff --git a/datamodel/libCore/include/core/CommandInterface.h b/datamodel/libCore/include/core/CommandInterface.h index 5151650e..7848c39f 100644 --- a/datamodel/libCore/include/core/CommandInterface.h +++ b/datamodel/libCore/include/core/CommandInterface.h @@ -61,10 +61,10 @@ class CommandInterface { // since dependent objects may need to be included. size_t deleteObjects(std::vector const& objects); - // Move scenegraph node to new parent at before the specified index. + // Move scenegraph nodes to new parent at before the specified index. // - If ValueHandle is invalid/empty the scenegraph parent is removed. // - If insertionBeforeIndex = -1 the node will be appended at the end of the new parent children. - void moveScenegraphChild(SEditorObject const& object, SEditorObject const& newParent, int insertBeforeIndex = -1); + void moveScenegraphChildren(std::vector const& objects, SEditorObject const& newParent, int insertBeforeIndex = -1); // Calls Context::insertAssetScenegraph and generates a composite undo command. void insertAssetScenegraph(const raco::core::MeshScenegraph& scenegraph, const std::string& absPath, SEditorObject const& parent); diff --git a/datamodel/libCore/include/core/Context.h b/datamodel/libCore/include/core/Context.h index 64837c12..948358eb 100644 --- a/datamodel/libCore/include/core/Context.h +++ b/datamodel/libCore/include/core/Context.h @@ -126,10 +126,10 @@ class BaseContext { size_t deleteObjects(std::vector const& objects, bool gcExternalProjectMap = true, bool includeChildren = true); - // Move scenegraph node to new parent at before the specified index. + // Move scenegraph nodes to new parent at before the specified index. // - If ValueHandle is invalid/empty the scenegraph parent is removed. // - If insertionBeforeIndex = -1 the node will be appended at the end of the new parent children. - void moveScenegraphChild(SEditorObject const& object, SEditorObject const& newParent, int insertBeforeIndex = -1); + void moveScenegraphChildren(std::vector const& objects, SEditorObject const& newParent, int insertBeforeIndex = -1); // Import scenegraph as a hierarchy of EditorObjects and move that scenegraph root noder under parent. // This includes generating Mesh resources, Nodes and MeshNodes as well as searching for already created Materials. diff --git a/datamodel/libCore/include/core/EngineInterface.h b/datamodel/libCore/include/core/EngineInterface.h index 1f0fb9e6..cf0e0d77 100644 --- a/datamodel/libCore/include/core/EngineInterface.h +++ b/datamodel/libCore/include/core/EngineInterface.h @@ -97,7 +97,9 @@ class EngineInterface { public: virtual ~EngineInterface() = default; virtual bool parseShader(const std::string& vertexShader, const std::string& geometryShader, const std::string& fragmentShader, const std::string& shaderDefines, PropertyInterfaceList& outUniforms, raco::core::PropertyInterfaceList& outAttributes, std::string& error) = 0; - virtual bool parseLuaScript(const std::string& luaScript, PropertyInterfaceList& outInputs, PropertyInterfaceList& outOutputs, std::string& error) = 0; + virtual bool parseLuaScript(const std::string& luaScript, const raco::data_storage::Table& modules, PropertyInterfaceList& outInputs, PropertyInterfaceList& outOutputs, std::string& error) = 0; + virtual bool parseLuaScriptModule(const std::string& luaScriptModule, std::string& outError) = 0; + virtual bool extractLuaDependencies(const std::string& luaScript, std::vector& moduleList, std::string &outError) = 0; virtual const std::map& enumerationDescription(EngineEnumeration type) const = 0; }; diff --git a/datamodel/libCore/include/core/Queries.h b/datamodel/libCore/include/core/Queries.h index a45f306b..4dbe0d71 100644 --- a/datamodel/libCore/include/core/Queries.h +++ b/datamodel/libCore/include/core/Queries.h @@ -26,30 +26,30 @@ namespace Queries { // which point into the object set. std::vector findAllReferencesTo(Project const &project, std::vector const& objects); - bool objectsReferencedByExtrefs(Project const& project, std::vector const& objects); - std::vector findAllReferencesFrom(SEditorObjectSet const& objects); std::vector findAllReferences(Project const &project); std::vector findAllReferences(const SEditorObject& object); std::vector findAllUnreferencedObjects(Project const& project, std::function predicate = nullptr); - - std::vector findAllValidReferenceTargets(Project const& project, const ValueHandle& handle ); + SEditorObject findById(const Project& project, const std::string& id); SEditorObject findById(const std::vector& objects, const std::string& id); SEditorObject findByName(const std::vector& objects, const std::string& name); ValueHandle findByIdAndPath(const Project& project, const std::string& object_id, const std::string& path); - bool canMoveScenegraphChild(Project const &project, SEditorObject const& object, SEditorObject const& newParent); - bool canDeleteObjects(Project const& project, const std::vector& objects); bool canPasteIntoObject(Project const& project, SEditorObject const& object); - bool canPasteObjectAsExternalReference(SEditorObject editorObject, bool isTopLevelObject); + bool canPasteObjectAsExternalReference(const SEditorObject& editorObject, bool wasTopLevelObjectInSourceProject); bool canDeleteUnreferencedResources(const Project& project); + SEditorObjectSet collectAllChildren(std::vector baseObjects); + std::vector filterForVisibleObjects(const std::vector& objects); std::vector filterForNotResource(const std::vector& objects); std::vector filterByTypeName(const std::vector& objects, const std::vector& typeNames); + std::vector filterForDeleteableObjects(Project const& project, const std::vector& objects); + std::vector filterForMoveableScenegraphChildren(Project const& project, const std::vector& objects, SEditorObject const& newParent); + template std::vector> filterByType(const std::vector& objects) { std::vector> r; @@ -65,6 +65,7 @@ namespace Queries { bool isNotResource(const SEditorObject& object); bool isProjectSettings(const SEditorObject& object); bool isChildHandle(const ValueHandle& handle); + bool isChildObject(const SEditorObject& child, const SEditorObject& parent); // Determines if the property value (for linkState = false) or the link state (for linkState = true) // is changeable in the data model. diff --git a/datamodel/libCore/include/core/RamsesProjectMigration.h b/datamodel/libCore/include/core/RamsesProjectMigration.h index ccf8a4e0..1b98b3c3 100644 --- a/datamodel/libCore/include/core/RamsesProjectMigration.h +++ b/datamodel/libCore/include/core/RamsesProjectMigration.h @@ -49,7 +49,9 @@ namespace raco::core { * 18: Added Animation and AnimationChannel types * 19: Changed ProjectSettings::backgroundColor from Vec3f to Vec4f * Links from Vec4f to Node::rotation are now allowed + * 20: Added LuaScriptModule type as well as basic Lua module support + * 21: Added mipmap flag to textures. */ -constexpr int RAMSES_PROJECT_FILE_VERSION = 19; +constexpr int RAMSES_PROJECT_FILE_VERSION = 21; QJsonDocument migrateProject(const QJsonDocument& doc, std::unordered_map& migrationWarnings); } // namespace raco::core diff --git a/datamodel/libCore/include/core/Undo.h b/datamodel/libCore/include/core/Undo.h index 6b5a99dc..2d01ff35 100644 --- a/datamodel/libCore/include/core/Undo.h +++ b/datamodel/libCore/include/core/Undo.h @@ -59,7 +59,7 @@ class UndoStack { void reset(); -private: +protected: void saveProjectState(const Project *src, Project *dest, Project *ref, const DataChangeRecorder &changes, UserObjectFactoryInterface &factory); void updateProjectState(const Project *src, Project *dest, const DataChangeRecorder &changes, UserObjectFactoryInterface &factory); diff --git a/datamodel/libCore/src/CommandInterface.cpp b/datamodel/libCore/src/CommandInterface.cpp index 8a64c58d..a13dc4b1 100644 --- a/datamodel/libCore/src/CommandInterface.cpp +++ b/datamodel/libCore/src/CommandInterface.cpp @@ -120,8 +120,8 @@ SEditorObject CommandInterface::createObject(std::string type, std::string name, auto types = context_->objectFactory()->getTypes(); if (types.find(type) != types.end()) { auto newObject = context_->createObject(type, name, id); - if (parent && Queries::canMoveScenegraphChild(*project(), newObject, parent)) { - context_->moveScenegraphChild(newObject, parent); + if (parent) { + context_->moveScenegraphChildren(Queries::filterForMoveableScenegraphChildren(*project(), {newObject}, parent), parent); } PrefabOperations::globalPrefabUpdate(*context_, context_->modelChanges()); undoStack_->push(fmt::format("Create '{}' object '{}'", type, name)); @@ -131,24 +131,31 @@ SEditorObject CommandInterface::createObject(std::string type, std::string name, } size_t CommandInterface::deleteObjects(std::vector const& objects) { - if (!objects.empty()) { - if (Queries::canDeleteObjects(*project(), objects)) { - auto numDeleted = context_->deleteObjects(objects); - PrefabOperations::globalPrefabUpdate(*context_, context_->modelChanges()); - undoStack_->push(fmt::format("Delete {} objects", objects.size())); - return numDeleted; - } + auto deletableObjects = Queries::filterForDeleteableObjects(*project(), objects); + if (!deletableObjects.empty()) { + auto numDeleted = context_->deleteObjects(deletableObjects); + PrefabOperations::globalPrefabUpdate(*context_, context_->modelChanges()); + undoStack_->push(fmt::format("Delete {} objects", numDeleted)); + return numDeleted; } return 0; } -void CommandInterface::moveScenegraphChild(SEditorObject const& object, SEditorObject const& newParent, int insertBeforeIndex) { - if (Queries::canMoveScenegraphChild(*project(), object, newParent)) { - context_->moveScenegraphChild(object, newParent, insertBeforeIndex); +void CommandInterface::moveScenegraphChildren(std::vector const& objects, SEditorObject const& newParent, int insertBeforeIndex) { + auto moveableChildren = Queries::filterForMoveableScenegraphChildren(*project(), objects, newParent); + + if (moveableChildren.size() > 0) { + context_->moveScenegraphChildren(moveableChildren, newParent, insertBeforeIndex); PrefabOperations::globalPrefabUpdate(*context_, context_->modelChanges()); - undoStack_->push(fmt::format("Move object '{}' to new parent '{}' before index {}", object->objectName(), - newParent ? newParent->objectName() : "", - insertBeforeIndex)); + if (moveableChildren.size() == 1) { + undoStack_->push(fmt::format("Move object '{}' to new parent '{}' before index {}", moveableChildren.front()->objectName(), + newParent ? newParent->objectName() : "", + insertBeforeIndex)); + } else { + undoStack_->push(fmt::format("Move {} objects to new parent '{}' before index {}", moveableChildren.size(), + newParent ? newParent->objectName() : "", + insertBeforeIndex)); + } } } @@ -164,10 +171,11 @@ std::string CommandInterface::copyObjects(const std::vector& obje } std::string CommandInterface::cutObjects(const std::vector& objects, bool deepCut) { - if (Queries::canDeleteObjects(*project(), objects)) { - auto result = context_->cutObjects(objects, deepCut); + auto deletableObjects = Queries::filterForDeleteableObjects(*project(), objects); + if (!deletableObjects.empty()) { + auto result = context_->cutObjects(deletableObjects, deepCut); PrefabOperations::globalPrefabUpdate(*context_, context_->modelChanges()); - undoStack_->push(fmt::format("Cut {} objects with deep = {}", objects.size(), deepCut)); + undoStack_->push(fmt::format("Cut {} objects with deep = {}", deletableObjects.size(), deepCut)); return result; } return std::string(); diff --git a/datamodel/libCore/src/Context.cpp b/datamodel/libCore/src/Context.cpp index 32a01732..e8fcb785 100644 --- a/datamodel/libCore/src/Context.cpp +++ b/datamodel/libCore/src/Context.cpp @@ -385,7 +385,7 @@ std::string BaseContext::cutObjects(const std::vector& objects, b auto rootObjectIDs{findRootObjectIDs(allObjects)}; auto originFolders{findOriginFolders(*project_, allObjects)}; std::string serialization{raco::serialization::serialize(allObjects, rootObjectIDs, allLinks, project_->currentFolder(), project_->currentFileName(), project_->projectID(), project_->projectName(), project_->externalProjectsMap(), originFolders).c_str()}; - deleteObjects(allObjects); + deleteObjects(Queries::filterForDeleteableObjects(*project_, allObjects)); return serialization; } @@ -580,9 +580,8 @@ std::vector BaseContext::pasteObjects(const std::string& seralize if (!pasteAsExtref) { // move all top level objects onto the target if it is allowed. for (const SEditorObject& obj : topLevelObjects) { - if (!obj->query() && - Queries::canMoveScenegraphChild(*project_, obj, target)) { - moveScenegraphChild(obj, target); + if (!obj->query()) { + moveScenegraphChildren(Queries::filterForMoveableScenegraphChildren(*project_, {obj}, target), target); } } @@ -711,14 +710,6 @@ SEditorObject BaseContext::createObject(std::string type, std::string name, std: return object; } -SEditorObjectSet allChildren(std::vector const& objects) { - SEditorObjectSet children; - for (auto obj : objects) { - std::copy(TreeIteratorAdaptor(obj).begin(), TreeIteratorAdaptor(obj).end(), std::inserter(children, children.end())); - } - return children; -} - void BaseContext::removeReferencesTo(SEditorObjectSet const& objects) { SEditorObjectSet srcObjects; for (auto obj : objects) { @@ -779,7 +770,7 @@ bool BaseContext::deleteWithVolatileSideEffects(Project* project, const SEditorO size_t BaseContext::deleteObjects(std::vector const& objects, bool gcExternalProjectMap, bool includeChildren) { SEditorObjectSet toRemove; if (includeChildren) { - toRemove = allChildren(objects); + toRemove = Queries::collectAllChildren(objects); } else { std::copy(objects.begin(), objects.end(), std::inserter(toRemove, toRemove.end())); } @@ -806,58 +797,64 @@ size_t BaseContext::deleteObjects(std::vector const& objects, boo return toRemove.size(); } -void BaseContext::moveScenegraphChild(SEditorObject const& object, SEditorObject const& newParent, int insertBeforeIndex) { - if (!object) { +void BaseContext::moveScenegraphChildren(std::vector const& objects, SEditorObject const& newParent, int insertBeforeIndex) { + if (objects.size() == 0) { return; } - auto oldParent = object->parent_.lock(); - int oldChildIndex = -1; - if (oldParent) { - oldChildIndex = oldParent->findChildIndex(object.get()); - } + for (const auto& object : objects) { + auto oldParent = object->parent_.lock(); - // Moving the object to before itself or to before its successor is a NOP: - if (oldParent == newParent && - (oldChildIndex == insertBeforeIndex || oldChildIndex + 1 == insertBeforeIndex)) { - return; - } + if (oldParent) { + int oldChildIndex = oldParent->findChildIndex(object.get()); - if (oldParent) { - // Special case: move inside the same object: - // if moving towards the end we need to adjust the insertion index since removing the - // object in its current position will shift the insertion index by one. - if (oldParent == newParent && insertBeforeIndex > oldChildIndex) { - --insertBeforeIndex; - } + if (oldParent == newParent) { - ValueHandle oldParentChildren{oldParent, &EditorObject::children_}; - removeProperty(oldParentChildren, oldChildIndex); - } + // Moving the object to before itself or to before its successor is a NOP: + if (oldChildIndex == insertBeforeIndex || oldChildIndex + 1 == insertBeforeIndex) { + ++insertBeforeIndex; + continue; + } + // Special case: move inside the same object: + // if moving towards the end we need to adjust the insertion index since removing the + // object in its current position will shift the insertion index by one. + else if (insertBeforeIndex > oldChildIndex) { + --insertBeforeIndex; + } + } - if (newParent) { - ValueBase* newChildEntry = newParent->children_->addProperty(PrimitiveType::Ref, insertBeforeIndex); - *newChildEntry = object; + ValueHandle oldParentChildren{oldParent, &EditorObject::children_}; + removeProperty(oldParentChildren, oldChildIndex); + } - int newChildIndex = newParent->findChildIndex(object.get()); + if (newParent) { + ValueBase* newChildEntry = newParent->children_->addProperty(PrimitiveType::Ref, insertBeforeIndex); + *newChildEntry = object; - ValueHandle newParentChildren{newParent, &EditorObject::children_}; - object->onAfterAddReferenceToThis(newParentChildren[newChildIndex]); + int newChildIndex = newParent->findChildIndex(object.get()); - callReferencedObjectChangedHandlers(object); + ValueHandle newParentChildren{newParent, &EditorObject::children_}; + object->onAfterAddReferenceToThis(newParentChildren[newChildIndex]); - changeMultiplexer_.recordValueChanged(newParentChildren); - } + callReferencedObjectChangedHandlers(object); - // Remove links attached to the moved object subtree that are not allowed with the new parent by the prefab-related constraints. - std::vector linksToRemove; - for (auto child : TreeIteratorAdaptor(object)) { - for (auto link : Queries::getLinksConnectedToObject(*project_, child, true, true)) { - if (!Queries::linkSatisfiesConstraints(link->startProp(), link->endProp())) { - removeLink(link->endProp()); - LOG_WARNING(log_system::CONTEXT, "Removed link violating prefab constraints: {}", link); + changeMultiplexer_.recordValueChanged(newParentChildren); + } + + // Remove links attached to the moved object subtree that are not allowed with the new parent by the prefab-related constraints. + std::vector linksToRemove; + for (auto child : TreeIteratorAdaptor(object)) { + for (auto link : Queries::getLinksConnectedToObject(*project_, child, true, true)) { + if (!Queries::linkSatisfiesConstraints(link->startProp(), link->endProp())) { + removeLink(link->endProp()); + LOG_WARNING(log_system::CONTEXT, "Removed link violating prefab constraints: {}", link); + } } } + + if (insertBeforeIndex != -1) { + ++insertBeforeIndex; + } } } @@ -911,8 +908,8 @@ void BaseContext::insertAssetScenegraph(const raco::core::MeshScenegraph& sceneg auto meshPath = std::filesystem::path(relativeFilePath).filename().string(); meshPath = project_->findAvailableUniqueName(topLevelObjects.begin(), topLevelObjects.end(), nullptr, meshPath); auto sceneRootNode = createObject(raco::user_types::Node::typeDescription.typeName, meshPath); - if (parent && core::Queries::canMoveScenegraphChild(*project(), sceneRootNode, parent)) { - moveScenegraphChild(sceneRootNode, parent); + if (parent) { + moveScenegraphChildren(core::Queries::filterForMoveableScenegraphChildren(*project(), {sceneRootNode}, parent), parent); } LOG_DEBUG(log_system::CONTEXT, "Traversing through scenegraph nodes..."); @@ -939,7 +936,7 @@ void BaseContext::insertAssetScenegraph(const raco::core::MeshScenegraph& sceneg LOG_DEBUG(log_system::CONTEXT, "Found node {} with multiple submeshes -> creating MeshNode for each submesh...", meshScenegraphNode.name); newNode = meshScenegraphNodes.emplace_back(createObject(raco::user_types::Node::typeDescription.typeName, meshScenegraphNode.name)); submeshRootNode = createObject(raco::user_types::Node::typeDescription.typeName, meshScenegraphNode.name + "_meshnodes"); - moveScenegraphChild(submeshRootNode, newNode); + moveScenegraphChildren({submeshRootNode}, newNode); } for (size_t submeshIndex{0}; submeshIndex < meshScenegraphNode.subMeshIndeces.size(); ++submeshIndex) { @@ -955,7 +952,7 @@ void BaseContext::insertAssetScenegraph(const raco::core::MeshScenegraph& sceneg submeshNode = newNode; } else { submeshNode = createObject(raco::user_types::MeshNode::typeDescription.typeName, meshScenegraphNode.name + "_meshnode_" + std::to_string(submeshIndex)); - moveScenegraphChild(submeshNode, submeshRootNode); + moveScenegraphChildren({submeshNode}, submeshRootNode); } if (assignedSubmeshIndex < 0) { @@ -979,8 +976,8 @@ void BaseContext::insertAssetScenegraph(const raco::core::MeshScenegraph& sceneg } } - if (!meshScenegraphNode.hasParent() && core::Queries::canMoveScenegraphChild(*project(), newNode, sceneRootNode)) { - moveScenegraphChild(newNode, sceneRootNode); + if (!meshScenegraphNode.hasParent()) { + moveScenegraphChildren(core::Queries::filterForMoveableScenegraphChildren(*project(), {newNode}, sceneRootNode), sceneRootNode); } LOG_DEBUG(log_system::CONTEXT, "All nodes traversed."); @@ -1003,8 +1000,8 @@ void BaseContext::insertAssetScenegraph(const raco::core::MeshScenegraph& sceneg LOG_INFO(log_system::CONTEXT, "Restoring scenegraph structure..."); for (size_t i{0}; i < scenegraph.nodes.size(); ++i) { auto meshScenegraphNode = scenegraph.nodes[i]; - if (meshScenegraphNode.has_value() && meshScenegraphNode->hasParent() && core::Queries::canMoveScenegraphChild(*project(), meshScenegraphNodes[i], meshScenegraphNodes[meshScenegraphNode->parentIndex])) { - moveScenegraphChild(meshScenegraphNodes[i], meshScenegraphNodes[meshScenegraphNode->parentIndex]); + if (meshScenegraphNode.has_value() && meshScenegraphNode->hasParent()) { + moveScenegraphChildren(core::Queries::filterForMoveableScenegraphChildren(*project(), {meshScenegraphNodes[i]}, meshScenegraphNodes[meshScenegraphNode->parentIndex]), meshScenegraphNodes[meshScenegraphNode->parentIndex]); } } LOG_INFO(log_system::CONTEXT, "Scenegraph structure restored."); @@ -1053,7 +1050,7 @@ void BaseContext::insertAssetScenegraph(const raco::core::MeshScenegraph& sceneg newAnim->as()->setChannelAmount(samplerSize); set({newAnim, {"play"}}, true); set({newAnim, {"loop"}}, true); - moveScenegraphChild(newAnim, sceneRootNode); + moveScenegraphChildren({newAnim}, sceneRootNode); LOG_INFO(log_system::CONTEXT, "Assigning animation samplers to animation '{}'...", meshAnim.name); for (auto samplerIndex = 0; samplerIndex < samplerSize; ++samplerIndex) { if (!sceneChannels[animationIndex][samplerIndex]) { diff --git a/datamodel/libCore/src/ExtrefOperations.cpp b/datamodel/libCore/src/ExtrefOperations.cpp index fe369f63..040b9973 100644 --- a/datamodel/libCore/src/ExtrefOperations.cpp +++ b/datamodel/libCore/src/ExtrefOperations.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include namespace raco::core { @@ -145,17 +147,19 @@ void ExtrefOperations::updateExternalObjects(BaseContext& context, Project* proj auto extProjectMapCopy = project->externalProjectsMap(); // local = collect all extref objects in current project - SEditorObjectSet localObjects; - std::copy_if(project->instances().begin(), project->instances().end(), std::inserter(localObjects, localObjects.end()), [](SEditorObject object) -> bool { - return object->query() != nullptr; - }); + std::map localObjects; + for (const auto& object : project->instances()) { + if (object->query()) { + localObjects[object->objectID()] = object; + } + } // remote = collect current state of extref objects in external projects // walk tree following all references but use objects from correct external project when following references. std::map externalObjects; try { - for (auto object : localObjects) { + for (const auto& [id, object] : localObjects) { if (object->getParent() == nullptr) { collectExternalObjects(project, ExternalObjectDescriptor{object, project}, externalProjectsStore, externalObjects, pathStack, true); } @@ -174,11 +178,9 @@ void ExtrefOperations::updateExternalObjects(BaseContext& context, Project* proj auto translateToLocal = [&localObjects](SEditorObject extObj) -> SEditorObject { if (extObj) { - auto it = std::find_if(localObjects.begin(), localObjects.end(), [extObj](SEditorObject obj) { - return obj->objectID() == extObj->objectID(); - }); + auto it = localObjects.find(extObj->objectID()); if (it != localObjects.end()) { - return *it; + return it->second; } } return nullptr; @@ -191,8 +193,8 @@ void ExtrefOperations::updateExternalObjects(BaseContext& context, Project* proj { auto it = localObjects.begin(); while (it != localObjects.end()) { - if (externalObjects.find((*it)->objectID()) == externalObjects.end()) { - toRemove.emplace_back(*it); + if (externalObjects.find(it->second->objectID()) == externalObjects.end()) { + toRemove.emplace_back(it->second); it = localObjects.erase(it); } else { ++it; @@ -236,7 +238,7 @@ void ExtrefOperations::updateExternalObjects(BaseContext& context, Project* proj localObj->addAnnotation(std::make_shared(item.second.project->projectID())); context.project()->addInstance(localObj); localChanges.recordCreateObject(localObj); - localObjects.insert(localObj); + localObjects[localObj->objectID()] = localObj; } } diff --git a/datamodel/libCore/src/PathManager.cpp b/datamodel/libCore/src/PathManager.cpp index fb875576..14bdc4fc 100644 --- a/datamodel/libCore/src/PathManager.cpp +++ b/datamodel/libCore/src/PathManager.cpp @@ -12,6 +12,7 @@ #include "user_types/AnimationChannel.h" #include "user_types/CubeMap.h" #include "user_types/LuaScript.h" +#include "user_types/LuaScriptModule.h" #include "user_types/Material.h" #include "user_types/Mesh.h" #include "user_types/Texture.h" @@ -131,7 +132,7 @@ PathManager::FolderTypeKeys PathManager::getCachedPathKeyCorrespondingToUserType return raco::core::PathManager::FolderTypeKeys::Mesh; } - if (&type == &raco::user_types::LuaScript::typeDescription) { + if (&type == &raco::user_types::LuaScript::typeDescription || &type == &raco::user_types::LuaScriptModule::typeDescription) { return raco::core::PathManager::FolderTypeKeys::Script; } diff --git a/datamodel/libCore/src/Queries.cpp b/datamodel/libCore/src/Queries.cpp index cd757559..f7d11885 100644 --- a/datamodel/libCore/src/Queries.cpp +++ b/datamodel/libCore/src/Queries.cpp @@ -47,23 +47,6 @@ std::vector Queries::findAllReferencesTo(Project const& project, st return refs; } -bool Queries::objectsReferencedByExtrefs(Project const& project, std::vector const& objects) { - for (auto instance : project.instances()) { - if (instance->query() && - std::find(objects.begin(), objects.end(), instance) == objects.end()) { - for (auto const& prop : ValueTreeIteratorAdaptor(ValueHandle(instance))) { - if (prop.type() == PrimitiveType::Ref) { - auto refValue = prop.asTypedRef(); - if (refValue && (std::find(objects.begin(), objects.end(), refValue) != objects.end())) { - return true; - } - } - } - } - } - return false; -} - std::vector Queries::findAllReferencesFrom(SEditorObjectSet const& objects) { std::vector refs; for (auto instance : objects) { @@ -261,61 +244,6 @@ bool wouldObjectInPrefabCauseLoop(SEditorObject object, user_types::SPrefab pref return false; } -bool Queries::canMoveScenegraphChild(Project const& project, SEditorObject const& object, SEditorObject const& newParent) { - // This query is disallowed for objects not in the project. - assert(project.getInstanceByID(object->objectID()) != nullptr); - - using namespace user_types; - - if (object->query() || newParent && newParent->query()) { - return false; - } - - // An object can't be moved below itself or below one of its children in the scenegraph hierarchy - for (auto parent = newParent; parent; parent = parent->getParent()) { - if (parent == object) { - return false; - } - } - - // Prefab instance children can't be moved - if (PrefabOperations::findContainingPrefabInstance(object->getParent())) { - return false; - } - - // Prefab instance subtree is locked: can't move anything into it - if (PrefabOperations::findContainingPrefabInstance(newParent)) { - return false; - } - - // Prefab instance loop prevention - if (auto newParentPrefab = PrefabOperations::findContainingPrefab(newParent)) { - if (wouldObjectInPrefabCauseLoop(object, newParentPrefab)) { - return false; - } - } - - return (object->as() || object->as() || object->as()) && - (newParent == nullptr || newParent->as() || newParent->as()); -} - - -bool Queries::canDeleteObjects(Project const& project, const std::vector& objects) { - for (const auto object : objects) { - // Objects nested inside PrefabInstances can't be deleted - if (auto parent = object->getParent()) { - if (parent && PrefabOperations::findContainingPrefabInstance(parent)) { - return false; - } - } - } - - if (objectsReferencedByExtrefs(project, objects)) { - return false; - } - - return true; -} bool Queries::canPasteIntoObject(Project const& project, SEditorObject const& object) { // Can always paste into the toplevel: @@ -343,10 +271,8 @@ bool Queries::canPasteIntoObject(Project const& project, SEditorObject const& ob return true; } -bool Queries::canPasteObjectAsExternalReference(SEditorObject editorObject, bool isTopLevelObject) { - return ((editorObject->getTypeDescription().isResource && !editorObject->as()) - || editorObject->as() - || (editorObject->as() && isTopLevelObject)); +bool Queries::canPasteObjectAsExternalReference(const SEditorObject& editorObject, bool wasTopLevelObjectInSourceProject) { + return (editorObject->getTypeDescription().isResource && !editorObject->as()) || editorObject->as() || (editorObject->as() && wasTopLevelObjectInSourceProject); } bool Queries::isProjectSettings(const SEditorObject& obj) { @@ -365,6 +291,16 @@ bool Queries::isChildHandle(const ValueHandle& handle) { return handle.type() == PrimitiveType::Ref && handle.depth() == 2 && handle.parent().getPropName() == "children"; } +bool Queries::isChildObject(const SEditorObject& child, const SEditorObject& parent) { + for (auto current = child->getParent(); current; current = current->getParent()) { + if (current == parent) { + return true; + } + } + + return false; +} + bool Queries::isReadOnly(SEditorObject editorObj) { if (editorObj->query()) { return true; @@ -594,6 +530,33 @@ std::vector Queries::getLinksConnectedToObject(const Project& project, co } +namespace { +void getLinksConnectedToObjectsHelper( + const std::map>& linkStartPoints, + const std::map>& linkEndPoints, + const std::string& propertyObjID, + bool includeStarting, + bool includeEnding, + std::map>& result) { + + if (includeStarting) { + auto linkIt = linkStartPoints.find(propertyObjID); + if (linkIt != linkStartPoints.end()) { + for (const auto& link : linkIt->second) { + result[(*link->endObject_)->objectID()].insert(link); + } + } + } + + if (includeEnding) { + auto linkIt = linkEndPoints.find(propertyObjID); + if (linkIt != linkEndPoints.end()) { + result[propertyObjID].insert(linkIt->second.begin(), linkIt->second.end()); + } + } +} +} // namespace + template inline std::map> Queries::getLinksConnectedToObjects(const Project& project, const Container& objects, bool includeStarting, bool includeEnding) { // use a set to avoid duplicate link entries @@ -603,22 +566,24 @@ inline std::map> Queries::getLinksConnectedToObject for (const auto& object : objects) { const auto& propertyObjID = object->objectID(); + + getLinksConnectedToObjectsHelper(linkStartPoints, linkEndPoints, propertyObjID, includeStarting, includeEnding, result); + } - if (includeStarting) { - auto linkIt = linkStartPoints.find(propertyObjID); - if (linkIt != linkStartPoints.end()) { - for (const auto& link : linkIt->second) { - result[(*link->endObject_)->objectID()].insert(link); - } - } - } + return result; +} - if (includeEnding) { - auto linkIt = linkEndPoints.find(propertyObjID); - if (linkIt != linkEndPoints.end()) { - result[propertyObjID].insert(linkIt->second.begin(), linkIt->second.end()); - } - } +template<> +std::map> Queries::getLinksConnectedToObjects(const Project& project, const std::map& objects, bool includeStarting, bool includeEnding) { + // use a set to avoid duplicate link entries + std::map> result; + const auto& linkStartPoints = project.linkStartPoints(); + const auto& linkEndPoints = project.linkEndPoints(); + + for (const auto& [id, object] : objects) { + const auto& propertyObjID = object->objectID(); + + getLinksConnectedToObjectsHelper(linkStartPoints, linkEndPoints, propertyObjID, includeStarting, includeEnding, result); } return result; @@ -809,11 +774,115 @@ std::vector Queries::filterForNotResource(const std::vector Queries::filterByTypeName(const std::vector& objects, const std::vector& typeNames) { std::vector result{}; - std::copy_if(objects.begin(), objects.end(), std::back_inserter(result), + std::copy_if(objects.begin(), objects.end(), std::back_inserter(result), [&typeNames](const SEditorObject& object) { return std::find(typeNames.begin(), typeNames.end(), object->getTypeDescription().typeName) != typeNames.end(); }); return result; } +SEditorObjectSet Queries::collectAllChildren(std::vector baseObjects) { + SEditorObjectSet children; + for (auto obj : baseObjects) { + std::copy(TreeIteratorAdaptor(obj).begin(), TreeIteratorAdaptor(obj).end(), std::inserter(children, children.end())); + } + return children; +} + + +std::vector Queries::filterForDeleteableObjects(Project const& project, const std::vector& objects) { + + // Since there is no case where a child is deleteable but its parent is not, we can just collect all children first before determining what is deleteable. + auto objectsWithChildren = Queries::collectAllChildren(objects); + std::vector partionedObjects{objectsWithChildren.begin(), objectsWithChildren.end()}; + + auto begin = partionedObjects.begin(); + auto end = partionedObjects.end(); + + auto partitionPoint = begin; + auto previousPartitionPoint = end; + + // Iteratively partion the list, so that deleteable items are moved to the front and non deleteable items to the back. + while (partitionPoint != previousPartitionPoint && partitionPoint != end) { + + previousPartitionPoint = partitionPoint; + partitionPoint = std::partition( + partitionPoint, end, + [project, partitionPoint, begin, end](const SEditorObject& object) { + + // Block deletion if this object is a child of a prefab instance + if (auto parent = object->getParent(); parent) { + auto instance = PrefabOperations::findContainingPrefabInstance(parent); + if (instance && std::find(begin, partitionPoint, instance) == partitionPoint) { + return false; + } + } + + // Block deletion if this object is referenced by external reference objects that are not yet in the deleteable partion. + for (const auto& instanceWeak : object->referencesToThis()) { + const auto instance = instanceWeak.lock(); + if (instance->query() && std::find(begin, partitionPoint, instance) == partitionPoint) { + return false; + } + } + + // Block deletion if this object has output properties linked to by external reference objects that are not yet in the deleteable partion. + auto links = getLinksConnectedToObject(project, object, true, false); + for (const auto& link : links) { + auto instance = link->endObject_.asRef(); + if (instance->query() && std::find(begin, partitionPoint, instance) == partitionPoint) { + return false; + } + } + + return true; + }); + } + + std::vector deletableObjects{begin, partitionPoint}; + assert(deletableObjects.size() == Queries::collectAllChildren(deletableObjects).size()); + return deletableObjects; +} + +std::vector Queries::filterForMoveableScenegraphChildren(Project const& project, const std::vector& objects, SEditorObject const& newParent) { + + if (newParent && !canPasteIntoObject(project, newParent)) { + return {}; + } + + std::vector result; + std::copy_if(objects.begin(), objects.end(), std::back_inserter(result), + [project, objects, newParent](const SEditorObject& object) { + using namespace user_types; + + // This query is disallowed for objects not in the project. + assert(project.getInstanceByID(object->objectID()) != nullptr); + + if (object->query()) { + return false; + } + + // An object can't be moved below itself or below one of its children in the scenegraph hierarchy + if (object == newParent || (newParent && isChildObject(newParent, object))) { + return false; + } + + // Prefab instance children can't be moved + if (PrefabOperations::findContainingPrefabInstance(object->getParent())) { + return false; + } + + // Prefab instance loop prevention + if (auto newParentPrefab = PrefabOperations::findContainingPrefab(newParent)) { + if (wouldObjectInPrefabCauseLoop(object, newParentPrefab)) { + return false; + } + } + + return (object->as() || object->as() || object->as()); + }); + + return result; +} + } // namespace raco::core diff --git a/datamodel/libCore/src/RamsesProjectMigration.cpp b/datamodel/libCore/src/RamsesProjectMigration.cpp index 5cf41514..bf2f4192 100644 --- a/datamodel/libCore/src/RamsesProjectMigration.cpp +++ b/datamodel/libCore/src/RamsesProjectMigration.cpp @@ -931,7 +931,22 @@ QJsonDocument migrateProject(const QJsonDocument& document, std::unordered_map generateMipMaps{false, DisplayNameAnnotation("Generate Mipmaps")}; + generateMipMaps = false; + + addprop(instanceproperties, u"generateMipmaps", generateMipMaps); + + return true; + }); + } QJsonDocument newDocument{documentObject}; // for debugging: diff --git a/datamodel/libCore/src/Undo.cpp b/datamodel/libCore/src/Undo.cpp index c58ba7dc..144a590d 100644 --- a/datamodel/libCore/src/Undo.cpp +++ b/datamodel/libCore/src/Undo.cpp @@ -341,7 +341,11 @@ void UndoStack::restoreProjectState(Project *src, Project *dest, BaseContext &co // Use the change recorder in the context from here on context_->uiChanges().mergeChanges(changes); - + + // Reset model changes here to make sure the next undo stack push will see + // all changes relative to the last undo stack entry + context_->modelChanges().reset(); + // Sync from external files for new or changed objects auto changedObjects = changes.getAllChangedObjects(); context_->performExternalFileReload({changedObjects.begin(), changedObjects.end()}); @@ -349,9 +353,11 @@ void UndoStack::restoreProjectState(Project *src, Project *dest, BaseContext &co if (extrefDirty) { std::vector stack; stack.emplace_back(dest->currentPath()); + // TODO needed to remove model change reset here since this allows the undo stack to get into inconsistent state. + // See commment above. // Reset model changes here to make sure that the prefab update which runs at the end of the external reference update // will only see the changed external reference objects and will not perform unnecessary prefab updates. - context_->modelChanges().reset(); + //context_->modelChanges().reset(); try { context_->updateExternalReferences(stack); } catch (ExtrefError &e) { @@ -360,7 +366,6 @@ void UndoStack::restoreProjectState(Project *src, Project *dest, BaseContext &co // to throwing this exception. } } - context_->modelChanges().reset(); } UndoStack::UndoStack(BaseContext* context, const Callback& onChange) : context_(context), onChange_ { onChange } { diff --git a/datamodel/libCore/tests/CMakeLists.txt b/datamodel/libCore/tests/CMakeLists.txt index bebfb876..6888cea4 100644 --- a/datamodel/libCore/tests/CMakeLists.txt +++ b/datamodel/libCore/tests/CMakeLists.txt @@ -46,11 +46,13 @@ raco_package_add_test_resouces( meshes/InterpolationTest/InterpolationTest.gltf meshes/InterpolationTest/interpolation.bin meshes/InterpolationTest/l.jpg + scripts/camera-control.lua + scripts/moduleDefinition.lua + scripts/moduleDependency.lua scripts/types-scalar.lua scripts/struct-simple.lua scripts/struct-nested.lua scripts/SimpleScript.lua - scripts/camera-control.lua ) diff --git a/datamodel/libCore/tests/Context_test.cpp b/datamodel/libCore/tests/Context_test.cpp index 7e5cbe0c..51b3d34d 100644 --- a/datamodel/libCore/tests/Context_test.cpp +++ b/datamodel/libCore/tests/Context_test.cpp @@ -306,50 +306,50 @@ TEST_F(ContextTest, Scenegraph) { EXPECT_EQ(bar->children_->size(), 0); // Move child from scenegraph root -> foo - context.moveScenegraphChild(child, foo); + context.moveScenegraphChildren({child}, foo); EXPECT_EQ(child->getParent(), foo); EXPECT_EQ(foo->children_->asVector(), std::vector({child})); EXPECT_EQ(bar->children_->size(), 0); // Insert child2 before child - context.moveScenegraphChild(child2, foo, 0); + context.moveScenegraphChildren({child2}, foo, 0); EXPECT_EQ(child->getParent(), foo); EXPECT_EQ(child2->getParent(), foo); EXPECT_EQ(foo->children_->asVector(), std::vector({child2, child})); // Remove first child - context.moveScenegraphChild(child2, nullptr); + context.moveScenegraphChildren({child2}, nullptr); EXPECT_EQ(child->getParent(), foo); EXPECT_EQ(child2->getParent(), nullptr); EXPECT_EQ(foo->children_->asVector(), std::vector({child})); // Move child from foo -> bar - context.moveScenegraphChild(child, bar); + context.moveScenegraphChildren({child}, bar); EXPECT_EQ(child->getParent(), bar); EXPECT_EQ(foo->children_->size(), 0); EXPECT_EQ(bar->children_->asVector(), std::vector({child})); // Move child from bar -> scenegraph root - context.moveScenegraphChild(child, nullptr); + context.moveScenegraphChildren({child}, nullptr); EXPECT_EQ(child->getParent(), nullptr); EXPECT_EQ(foo->children_->size(), 0); EXPECT_EQ(bar->children_->size(), 0); // Check correct behaviour for moving towards the back - context.moveScenegraphChild(child, foo); - context.moveScenegraphChild(child2, foo); - context.moveScenegraphChild(child3, foo); + context.moveScenegraphChildren({child}, foo); + context.moveScenegraphChildren({child2}, foo); + context.moveScenegraphChildren({child3}, foo); EXPECT_EQ(foo->children_->asVector(), std::vector({child, child2, child3})); - context.moveScenegraphChild(child, foo, 2); + context.moveScenegraphChildren({child}, foo, 2); EXPECT_EQ(foo->children_->asVector(), std::vector({child2, child, child3})); // Check for NOP handling recorder.reset(); - context.moveScenegraphChild(child, foo, 1); + context.moveScenegraphChildren({child}, foo, 1); EXPECT_EQ(recorder.getChangedValues().size(), 0); - context.moveScenegraphChild(child, foo, 2); + context.moveScenegraphChildren({child}, foo, 2); EXPECT_EQ(recorder.getChangedValues().size(), 0); // iterator testing @@ -452,7 +452,7 @@ TEST_F(ContextTest, DeleteNodeWithChild) { auto node = context.createObject(Node::typeDescription.typeName, "parent"); auto child = context.createObject(Node::typeDescription.typeName, "child"); - context.moveScenegraphChild(child, node); + context.moveScenegraphChildren({child}, node); EXPECT_EQ(node->children_->asVector(), std::vector({child})); checkedDeleteObjects({node}); @@ -462,7 +462,7 @@ TEST_F(ContextTest, DeleteNodeAndChild) { auto node = context.createObject(Node::typeDescription.typeName, "parent"); auto child = context.createObject(Node::typeDescription.typeName, "child"); - context.moveScenegraphChild(child, node); + context.moveScenegraphChildren({child}, node); EXPECT_EQ(node->children_->asVector(), std::vector({child})); checkedDeleteObjects({node, child}); @@ -472,7 +472,7 @@ TEST_F(ContextTest, DeleteNodeInParent) { auto node = context.createObject(Node::typeDescription.typeName, "parent"); auto child = context.createObject(Node::typeDescription.typeName, "child"); - context.moveScenegraphChild(child, node); + context.moveScenegraphChildren({child}, node); EXPECT_EQ(node->children_->asVector(), std::vector({child})); checkedDeleteObjects({child}); @@ -483,19 +483,19 @@ TEST_F(ContextTest, DeleteMultiNodeInParent) { auto node = context.createObject(Node::typeDescription.typeName, "parent"); auto child1 = context.createObject(Node::typeDescription.typeName, "child1"); - context.moveScenegraphChild(child1, node); + context.moveScenegraphChildren({child1}, node); auto child2 = context.createObject(Node::typeDescription.typeName, "child2"); - context.moveScenegraphChild(child2, node); + context.moveScenegraphChildren({child2}, node); auto child3 = context.createObject(Node::typeDescription.typeName, "child3"); - context.moveScenegraphChild(child3, node); + context.moveScenegraphChildren({child3}, node); auto child4 = context.createObject(Node::typeDescription.typeName, "child4"); - context.moveScenegraphChild(child4, node); + context.moveScenegraphChildren({child4}, node); auto child5 = context.createObject(Node::typeDescription.typeName, "child5"); - context.moveScenegraphChild(child5, node); + context.moveScenegraphChildren({child5}, node); EXPECT_EQ(node->children_->asVector(), std::vector({child1, child2, child3, child4, child5})); @@ -507,19 +507,19 @@ TEST_F(ContextTest, DeleteMultiNodeWithParent) { auto node = context.createObject(Node::typeDescription.typeName, "parent"); auto child1 = context.createObject(Node::typeDescription.typeName, "child1"); - context.moveScenegraphChild(child1, node); + context.moveScenegraphChildren({child1}, node); auto child2 = context.createObject(Node::typeDescription.typeName, "child2"); - context.moveScenegraphChild(child2, node); + context.moveScenegraphChildren({child2}, node); auto child3 = context.createObject(Node::typeDescription.typeName, "child3"); - context.moveScenegraphChild(child3, node); + context.moveScenegraphChildren({child3}, node); auto child4 = context.createObject(Node::typeDescription.typeName, "child4"); - context.moveScenegraphChild(child4, node); + context.moveScenegraphChildren({child4}, node); auto child5 = context.createObject(Node::typeDescription.typeName, "child5"); - context.moveScenegraphChild(child5, node); + context.moveScenegraphChildren({child5}, node); EXPECT_EQ(node->children_->asVector(), std::vector({child1, child2, child3, child4, child5})); @@ -617,7 +617,7 @@ TEST_F(ContextTest, ObjectMovingOnTopLevel) { ASSERT_EQ(project.instances(), std::vector({firstRoot, secondRoot})); - context.moveScenegraphChild({secondRoot}, {}, 0); + context.moveScenegraphChildren({secondRoot}, {}, 0); ASSERT_NE(project.instances(), std::vector({secondRoot, firstRoot})) << "[!!!] Moving top-level scenegraph nodes has been implemented / fixed. Replace ASSERT_NE macro with ASSERT_EQ macro in ContextTest unit test ObjectMovingOnTopLevel!"; } @@ -755,7 +755,7 @@ TEST_F(ContextTest, copy_paste_loses_uniforms) { TEST_F(ContextTest, copyAndPasteHierarchy) { auto parent = context.createObject(Node::typeDescription.typeName); auto child = context.createObject(Node::typeDescription.typeName); - context.moveScenegraphChild(child, parent); + context.moveScenegraphChildren({child}, parent); auto copyResult = context.pasteObjects(context.copyObjects({ parent })); auto parentCopy = std::dynamic_pointer_cast(copyResult.at(0)); @@ -766,7 +766,7 @@ TEST_F(ContextTest, copyAndPasteHierarchy) { TEST_F(ContextTest, cutAndPasteHierarchy) { auto parent = context.createObject(Node::typeDescription.typeName); auto child = context.createObject(Node::typeDescription.typeName); - context.moveScenegraphChild(child, parent); + context.moveScenegraphChildren({child}, parent); auto clipboardContent = context.cutObjects({ parent }); ASSERT_EQ(0, project.instances().size()); @@ -779,9 +779,9 @@ TEST_F(ContextTest, cutAndPasteHierarchy) { TEST_F(ContextTest, copyAndPasteDeeperHierarchy) { auto parent = context.createObject(Node::typeDescription.typeName, "parent"); auto child = context.createObject(Node::typeDescription.typeName, "child"); - auto sub_child = context.createObject(Node::typeDescription.typeName, "sub_child"); - context.moveScenegraphChild(child, parent); - context.moveScenegraphChild(sub_child, child); + auto subChild = context.createObject(Node::typeDescription.typeName, "subChild"); + context.moveScenegraphChildren({child}, parent); + context.moveScenegraphChildren({subChild}, child); auto pasteResult = context.pasteObjects(context.copyObjects({ parent })); ASSERT_EQ(1, pasteResult.size()); @@ -791,16 +791,16 @@ TEST_F(ContextTest, copyAndPasteDeeperHierarchy) { ASSERT_EQ(6, project.instances().size()); ASSERT_EQ("parent (1)", parentCopy->objectName()); ASSERT_EQ("child", parentCopy->children_->get(0)->asRef()->objectName()); - ASSERT_EQ("sub_child", parentCopy->children_->get(0)->asRef()->children_->get(0)->asRef()->objectName()); + ASSERT_EQ("subChild", parentCopy->children_->get(0)->asRef()->children_->get(0)->asRef()->objectName()); } TEST_F(ContextTest, cutAndPasteDeeperHierarchy) { auto parent = context.createObject(Node::typeDescription.typeName, "parent"); auto child = context.createObject(Node::typeDescription.typeName, "child"); - auto sub_child = context.createObject(Node::typeDescription.typeName, "sub_child"); - context.moveScenegraphChild(child, parent); - context.moveScenegraphChild(sub_child, child); - context.moveScenegraphChild(child, parent); + auto subChild = context.createObject(Node::typeDescription.typeName, "subChild"); + context.moveScenegraphChildren({child}, parent); + context.moveScenegraphChildren({subChild}, child); + context.moveScenegraphChildren({child}, parent); auto clipboardContent = context.cutObjects({ parent }); @@ -907,7 +907,7 @@ TEST_F(ContextTest, deepCut) { auto node = context.createObject(Node::typeDescription.typeName, "node"); auto meshNode = context.createObject(MeshNode::typeDescription.typeName, "meshNode"); auto mesh = context.createObject(Mesh::typeDescription.typeName, "mesh"); - context.moveScenegraphChild(meshNode, node); + context.moveScenegraphChildren({meshNode}, node); context.set({ meshNode, { "mesh" }}, mesh); auto serial = context.cutObjects({node}, true); @@ -1017,9 +1017,9 @@ TEST_F(ContextTest, copyAndPasteStructureUniqueName) { auto child1 = context.createObject(Node::typeDescription.typeName, "Child1"); auto child11 = context.createObject(Node::typeDescription.typeName, "Child1_1"); auto child2 = context.createObject(Node::typeDescription.typeName, "Child2"); - context.moveScenegraphChild(child1, node); - context.moveScenegraphChild(child2, node); - context.moveScenegraphChild(child11, child1); + context.moveScenegraphChildren({child1}, node); + context.moveScenegraphChildren({child2}, node); + context.moveScenegraphChildren({child11}, child1); auto copiedObjs = context.copyObjects({node}); @@ -1036,9 +1036,9 @@ TEST_F(ContextTest, cutAndPasteStructureUniqueName) { auto child1 = context.createObject(Node::typeDescription.typeName, "Child1"); auto child11 = context.createObject(Node::typeDescription.typeName, "Child1_1"); auto child2 = context.createObject(Node::typeDescription.typeName, "Child2"); - context.moveScenegraphChild(child1, node); - context.moveScenegraphChild(child2, node); - context.moveScenegraphChild(child11, child1); + context.moveScenegraphChildren({child1}, node); + context.moveScenegraphChildren({child2}, node); + context.moveScenegraphChildren({child11}, child1); auto cutObjs = context.cutObjects({node}); diff --git a/datamodel/libCore/tests/ExternalReference_test.cpp b/datamodel/libCore/tests/ExternalReference_test.cpp index 9cf0f13f..cafa22f9 100644 --- a/datamodel/libCore/tests/ExternalReference_test.cpp +++ b/datamodel/libCore/tests/ExternalReference_test.cpp @@ -24,6 +24,7 @@ #include "user_types/Animation.h" #include "user_types/AnimationChannel.h" +#include "user_types/LuaScriptModule.h" #include "user_types/Mesh.h" #include "user_types/MeshNode.h" #include "user_types/Node.h" @@ -57,7 +58,7 @@ class ExtrefTest : public RacoBaseTest<> { std::shared_ptr create(std::string name, SEditorObject parent = nullptr) { auto obj = std::dynamic_pointer_cast(cmd->createObject(C::typeDescription.typeName, name)); if (parent) { - cmd->moveScenegraphChild(obj, parent); + cmd->moveScenegraphChildren({obj}, parent); } return obj; } @@ -682,15 +683,58 @@ TEST_F(ExtrefTest, extref_can_delete_only_unused) { auto prefab = findExt("prefab"); auto material = findExt("material"); auto mesh = findExt("mesh"); + auto meshnode = findExt("prefab_meshnode"); + + EXPECT_FALSE(Queries::filterForDeleteableObjects(*project, {material}).empty()); + EXPECT_TRUE(Queries::filterForDeleteableObjects(*project, {mesh}).empty()); + EXPECT_FALSE(Queries::filterForDeleteableObjects(*project, {prefab}).empty()); - EXPECT_TRUE(Queries::canDeleteObjects(*project, {material})); - EXPECT_FALSE(Queries::canDeleteObjects(*project, {mesh})); - EXPECT_TRUE(Queries::canDeleteObjects(*project, {prefab})); + EXPECT_EQ(Queries::filterForDeleteableObjects(*project, {material, mesh}).size(), 1); + EXPECT_EQ(Queries::filterForDeleteableObjects(*project, {prefab, mesh}).size(), 3); + EXPECT_EQ(Queries::filterForDeleteableObjects(*project, {prefab, meshnode}).size(), 2); + EXPECT_EQ(Queries::filterForDeleteableObjects(*project, {prefab, mesh, meshnode}).size(), 3); + EXPECT_EQ(Queries::filterForDeleteableObjects(*project, {prefab, meshnode, mesh, material}).size(), 4); + EXPECT_EQ(Queries::filterForDeleteableObjects(*project, {prefab, material}).size(), 3); cmd->deleteObjects({prefab}); - EXPECT_TRUE(Queries::canDeleteObjects(*project, {material})); - EXPECT_TRUE(Queries::canDeleteObjects(*project, {mesh})); + EXPECT_FALSE(Queries::filterForDeleteableObjects(*project, {material}).empty()); + EXPECT_FALSE(Queries::filterForDeleteableObjects(*project, {mesh}).empty()); + }); +} + +TEST_F(ExtrefTest, extref_can_delete_only_unused_with_links) { + auto basePathName{(cwd_path() / "base.rcp").string()}; + auto compositePathName{(cwd_path() / "composite.rcp").string()}; + + setupBase(basePathName, [this]() { + auto luaSource = create("luaSource"); + auto luaSink = create("luaSink"); + + TextFile scriptFile = makeFile("script.lua", R"( +function interface() + IN.v = VEC3F + OUT.v = VEC3F +end +function run() +end +)"); + cmd->set({luaSource, {"uri"}}, scriptFile); + cmd->set({luaSink, {"uri"}}, scriptFile); + cmd->addLink({luaSource, {"luaOutputs", "v"}}, {luaSink, {"luaInputs", "v"}}); + }); + + setupComposite(basePathName, compositePathName, {"luaSink"}, [this]() { + auto luaSource = findExt("luaSource"); + auto luaSink = findExt("luaSink"); + + EXPECT_FALSE(Queries::filterForDeleteableObjects(*project, {luaSink}).empty()); + EXPECT_EQ(Queries::filterForDeleteableObjects(*project, {luaSink, luaSource}).size(), 2); + EXPECT_TRUE(Queries::filterForDeleteableObjects(*project, {luaSource}).empty()); + + cmd->deleteObjects({luaSink}); + + EXPECT_FALSE(Queries::filterForDeleteableObjects(*project, {luaSource}).empty()); }); } @@ -714,9 +758,9 @@ TEST_F(ExtrefTest, extref_cant_move) { auto mesh = findExt("mesh"); auto meshnode = findExt("meshnode"); - EXPECT_FALSE(Queries::canMoveScenegraphChild(*project, meshnode, node)); - EXPECT_FALSE(Queries::canMoveScenegraphChild(*project, meshnode, nullptr)); - EXPECT_FALSE(Queries::canMoveScenegraphChild(*project, node, meshnode)); + EXPECT_TRUE(Queries::filterForMoveableScenegraphChildren(*project, {meshnode}, node).empty()); + EXPECT_TRUE(Queries::filterForMoveableScenegraphChildren(*project, {meshnode}, nullptr).empty()); + EXPECT_TRUE(Queries::filterForMoveableScenegraphChildren(*project, {node}, meshnode).empty()); }); } @@ -818,7 +862,7 @@ TEST_F(ExtrefTest, prefab_update_stop_using_child) { updateBase(basePathName, [this]() { auto right = find("prefab_right"); auto node = find("node"); - cmd->moveScenegraphChild(node, right); + cmd->moveScenegraphChildren({node}, right); }); updateComposite(compositePathName, [this]() { @@ -848,7 +892,7 @@ TEST_F(ExtrefTest, prefab_update_inter_prefab_scenegraph_move) { updateBase(basePathName, [this]() { auto right = find("prefab_right"); auto node = find("node"); - cmd->moveScenegraphChild(node, right); + cmd->moveScenegraphChildren({node}, right); }); updateComposite(compositePathName, [this]() { @@ -882,7 +926,7 @@ TEST_F(ExtrefTest, prefab_update_move_and_delete_parent) { auto prefab = find("prefab"); auto node = find("prefab_node"); auto child = find("prefab_child"); - cmd->moveScenegraphChild(child, prefab); + cmd->moveScenegraphChildren({child}, prefab); cmd->deleteObjects({node}); }); @@ -930,7 +974,7 @@ end auto node = find("prefab_node"); auto child = find("prefab_child"); auto lua = find("prefab_lua"); - cmd->moveScenegraphChild(child, prefab); + cmd->moveScenegraphChildren({child}, prefab); cmd->deleteObjects({node}); }); @@ -1621,7 +1665,7 @@ end updateBase(basePathName, [this]() { auto node = create("dummyNode"); auto lua = find("global_control"); - cmd->moveScenegraphChild(lua, node); + cmd->moveScenegraphChildren({lua}, node); cmd->set({lua, {"luaInputs", "scalar"}}, 0.5); }); @@ -1828,8 +1872,8 @@ function run() OUT.v = IN.v end )"); - cmd->moveScenegraphChild(node, prefab); - cmd->moveScenegraphChild(lua, prefab); + cmd->moveScenegraphChildren({node}, prefab); + cmd->moveScenegraphChildren({lua}, prefab); cmd->set({lua, {"uri"}}, std::string("script.lua")); cmd->addLink({lua, {"luaOutputs", "v"}}, {node, {"rotation"}}); }); @@ -1877,15 +1921,14 @@ TEST_F(ExtrefTest, animation_channel_data_gets_propagated) { TEST_F(ExtrefTest, animation_in_extref_prefab_gets_propagated) { auto basePathName1{(cwd_path() / "base.rcp").string()}; auto basePathName2{(cwd_path() / "base2.rcp").string()}; - auto compositePathName{(cwd_path() / "composite.rcp").string()}; setupBase(basePathName1, [this]() { auto anim = create_animation("anim"); auto animNode = create("node"); - cmd->moveScenegraphChild(anim, animNode); + cmd->moveScenegraphChildren({anim}, animNode); auto prefab = create("prefab"); - cmd->moveScenegraphChild(animNode, prefab); + cmd->moveScenegraphChildren({animNode}, prefab); auto animChannel = create_animationchannel("animCh", "meshse/InterpolationTest/InterpolationTest.gltf"); cmd->set({anim, {"animationChannels", "Channel 0"}}, animChannel); @@ -1902,3 +1945,176 @@ TEST_F(ExtrefTest, animation_in_extref_prefab_gets_propagated) { ASSERT_EQ(anim->get("animationChannels")->asTable()[0]->asRef(), animChannel); }); } + +TEST_F(ExtrefTest, prefab_cut_deep_linked_does_not_delete_shared) { + auto basePathName{(cwd_path() / "base.rcp").string()}; + auto compositePathName{(cwd_path() / "composite.rcp").string()}; + + setupBase(basePathName, [this]() { + + auto mesh1 = create_mesh("mesh1", "meshes/Duck.glb"); + auto mesh2 = create_mesh("mesh2", "meshes/Duck.glb"); + auto sharedMaterial = create_material("sharedMaterial", "shaders/basic.vert", "shaders/basic.frag"); + + auto prefab1 = create("prefab1"); + auto prefab2 = create("prefab2"); + + auto meshNode1 = create_meshnode("prefab1_meshNode", mesh1, sharedMaterial, prefab1); + auto meshNode2 = create_meshnode("prefab2_meshNode", mesh2, sharedMaterial, prefab2); + + }); + + setupComposite(basePathName, compositePathName, {"prefab1", "prefab2"}, [this]() { + auto prefab1 = find("prefab1"); + auto prefab2 = find("prefab2"); + + find("sharedMaterial"); + + cmd->cutObjects({prefab2}, true); + + find("sharedMaterial"); + + cmd->cutObjects({prefab1}, true); + + dontFind("sharedMaterial"); + }); +} + +TEST_F(ExtrefTest, module_gets_propagated) { + auto basePathName1{(cwd_path() / "base.rcp").string()}; + auto basePathName2{(cwd_path() / "base2.rcp").string()}; + + setupBase(basePathName1, [this]() { + auto module = create("module"); + cmd->set({module, &LuaScriptModule::uri_}, (cwd_path() / "scripts" / "moduleDefinition.lua").string()); + }); + + setupBase(basePathName2, [this, basePathName1]() { + ASSERT_TRUE(pasteFromExt(basePathName1, {"module"}, true)); + auto lua = create("script"); + cmd->set({lua, &LuaScript::uri_}, (cwd_path() / "scripts" / "moduleDependency.lua").string()); + + auto module = findExt("module"); + cmd->set({lua, {"luaModules", "coalas"}}, module); + + ASSERT_NE(module, nullptr); + ASSERT_FALSE(cmd->errors().hasError({lua})); + }); +} + +TEST_F(ExtrefTest, prefab_instance_with_lua_and_module) { + auto basePathName1{(cwd_path() / "base.rcp").string()}; + auto basePathName2{(cwd_path() / "base2.rcp").string()}; + + setupBase(basePathName1, [this]() { + auto prefab = create("prefab"); + auto inst = create("inst"); + cmd->set({inst, &PrefabInstance::template_}, prefab); + + auto lua = create("lua", prefab); + cmd->set({lua, &LuaScript::uri_}, (cwd_path() / "scripts/moduleDependency.lua").string()); + cmd->moveScenegraphChildren({lua}, prefab); + + auto luaModule = create("luaModule"); + cmd->set({luaModule, &LuaScriptModule::uri_}, (cwd_path() / "scripts/moduleDefinition.lua").string()); + + cmd->set({lua, {"luaModules", "coalas"}}, luaModule); + }); + + setupBase(basePathName2, [this, basePathName1]() { + ASSERT_TRUE(pasteFromExt(basePathName1, {"prefab"}, true)); + auto lua = findExt("lua"); + auto module = findExt("luaModule"); + + ASSERT_NE(lua, nullptr); + ASSERT_NE(module, nullptr); + ASSERT_FALSE(cmd->errors().hasError({lua})); + }); +} +TEST_F(ExtrefTest, prefab_instance_update_lua_and_module_remove_module_ref) { + auto basePathName1{(cwd_path() / "base.rcp").string()}; + auto basePathName2{(cwd_path() / "base2.rcp").string()}; + + setupBase(basePathName1, [this]() { + auto prefab = create("prefab"); + auto inst = create("inst"); + cmd->set({inst, &PrefabInstance::template_}, prefab); + + auto lua = create("lua", prefab); + cmd->set({lua, &LuaScript::uri_}, (cwd_path() / "scripts/moduleDependency.lua").string()); + cmd->moveScenegraphChildren({lua}, prefab); + + auto luaModule = create("luaModule"); + cmd->set({luaModule, &LuaScriptModule::uri_}, (cwd_path() / "scripts/moduleDefinition.lua").string()); + + cmd->set({lua, {"luaModules", "coalas"}}, luaModule); + }); + + setupBase(basePathName2, [this, basePathName1]() { + ASSERT_TRUE(pasteFromExt(basePathName1, {"prefab"}, true)); + auto lua = findExt("lua"); + auto module = findExt("luaModule"); + + ASSERT_NE(lua, nullptr); + ASSERT_NE(module, nullptr); + ASSERT_FALSE(cmd->errors().hasError({lua})); + }); + + updateBase(basePathName1, [this]() { + auto lua = find("lua"); + cmd->set({lua, {"luaModules", "coalas"}}, SEditorObject{}); + }); + + updateBase(basePathName2, [this]() { + auto lua = findExt("lua"); + auto module = findExt("luaModule"); + + ASSERT_NE(lua, nullptr); + ASSERT_NE(module, nullptr); + ASSERT_TRUE(cmd->errors().hasError({lua})); + }); +} + +TEST_F(ExtrefTest, prefab_instance_update_lua_and_module_change_lua_uri) { + auto basePathName1{(cwd_path() / "base.rcp").string()}; + auto basePathName2{(cwd_path() / "base2.rcp").string()}; + + setupBase(basePathName1, [this]() { + auto prefab = create("prefab"); + auto inst = create("inst"); + cmd->set({inst, &PrefabInstance::template_}, prefab); + + auto lua = create("lua", prefab); + cmd->set({lua, &LuaScript::uri_}, (cwd_path() / "scripts/moduleDependency.lua").string()); + cmd->moveScenegraphChildren({lua}, prefab); + + auto luaModule = create("luaModule"); + cmd->set({luaModule, &LuaScriptModule::uri_}, (cwd_path() / "scripts/moduleDefinition.lua").string()); + + cmd->set({lua, {"luaModules", "coalas"}}, luaModule); + }); + + setupBase(basePathName2, [this, basePathName1]() { + ASSERT_TRUE(pasteFromExt(basePathName1, {"prefab"}, true)); + auto lua = findExt("lua"); + auto module = findExt("luaModule"); + + ASSERT_NE(lua, nullptr); + ASSERT_NE(module, nullptr); + ASSERT_FALSE(cmd->errors().hasError({lua})); + }); + + updateBase(basePathName1, [this]() { + auto lua = find("lua"); + cmd->set({lua, &LuaScript::uri_}, (cwd_path() / "scripts/types-scalar.lua").string()); + }); + + updateBase(basePathName2, [this]() { + auto lua = findExt("lua"); + auto module = findExt("luaModule"); + + ASSERT_NE(lua, nullptr); + ASSERT_NE(module, nullptr); + ASSERT_FALSE(cmd->errors().hasError({lua})); + }); +} diff --git a/datamodel/libCore/tests/Link_test.cpp b/datamodel/libCore/tests/Link_test.cpp index f8341080..3423969a 100644 --- a/datamodel/libCore/tests/Link_test.cpp +++ b/datamodel/libCore/tests/Link_test.cpp @@ -549,7 +549,7 @@ TEST_F(LinkTest, removal_move_start_lua_lua) { auto [sprop, eprop] = link(start, {"luaOutputs", "ofloat"}, end, {"luaInputs", "float"}); - commandInterface.moveScenegraphChild(start, node); + commandInterface.moveScenegraphChildren({start}, node); ASSERT_EQ(project.links().size(), 0); } @@ -561,16 +561,16 @@ TEST_F(LinkTest, removal_move_end_lua_lua) { auto [sprop, eprop] = link(start, {"luaOutputs", "ofloat"}, end, {"luaInputs", "float"}); - commandInterface.moveScenegraphChild(end, node); + commandInterface.moveScenegraphChildren({end}, node); checkLinks({{sprop, eprop, true}}); - commandInterface.moveScenegraphChild(start, node); + commandInterface.moveScenegraphChildren({start}, node); checkLinks({{sprop, eprop, true}}); - commandInterface.moveScenegraphChild(start, meshnode); + commandInterface.moveScenegraphChildren({start}, meshnode); checkLinks({{sprop, eprop, true}}); - commandInterface.moveScenegraphChild(end, nullptr); + commandInterface.moveScenegraphChildren({end}, nullptr); ASSERT_EQ(project.links().size(), 0); } @@ -582,7 +582,7 @@ TEST_F(LinkTest, removal_move_start_lua_mat) { auto [sprop, eprop] = link(lua, {"luaOutputs", "ovector3f"}, material, {"uniforms", "u_color"}); checkLinks({{sprop, eprop, true}}); - commandInterface.moveScenegraphChild(lua, node); + commandInterface.moveScenegraphChildren({lua}, node); ASSERT_EQ(project.links().size(), 0); } @@ -596,7 +596,7 @@ TEST_F(LinkTest, removal_move_middle_lua_lua_mat) { auto [oprop, eprop] = link(end, {"luaOutputs", "ovector3f"}, material, {"uniforms", "u_color"}); checkLinks({{sprop, iprop, true}, {oprop, eprop, true}}); - commandInterface.moveScenegraphChild(end, node); + commandInterface.moveScenegraphChildren({end}, node); checkLinks({{sprop, iprop, true}}); } diff --git a/datamodel/libCore/tests/Prefab_test.cpp b/datamodel/libCore/tests/Prefab_test.cpp index 75234224..f0aa13a1 100644 --- a/datamodel/libCore/tests/Prefab_test.cpp +++ b/datamodel/libCore/tests/Prefab_test.cpp @@ -22,7 +22,7 @@ #include "ramses_adaptor/SceneBackend.h" #include "ramses_base/BaseEngineBackend.h" - +#include "user_types/LuaScriptModule.h" #include "user_types/Mesh.h" #include "user_types/MeshNode.h" #include "user_types/Node.h" @@ -45,7 +45,7 @@ TEST_F(PrefabTest, move_node_in) { auto node = create("node"); commandInterface.set({inst, {"template"}}, prefab); - checkUndoRedo([this, node, prefab]() { commandInterface.moveScenegraphChild(node, prefab); }, + checkUndoRedo([this, node, prefab]() { commandInterface.moveScenegraphChildren({node}, prefab); }, [this, inst]() { EXPECT_EQ(inst->children_->size(), 0); }, @@ -59,9 +59,9 @@ TEST_F(PrefabTest, move_node_out) { auto inst = create("inst"); auto node = create("node"); commandInterface.set({inst, {"template"}}, prefab); - commandInterface.moveScenegraphChild(node, prefab); + commandInterface.moveScenegraphChildren({node}, prefab); - checkUndoRedo([this, node, prefab]() { commandInterface.moveScenegraphChild(node, nullptr); }, + checkUndoRedo([this, node, prefab]() { commandInterface.moveScenegraphChildren({node}, nullptr); }, [this, inst]() { EXPECT_EQ(inst->children_->size(), 1); }, @@ -75,7 +75,7 @@ TEST_F(PrefabTest, del_node) { auto inst = create("inst"); auto node = create("node"); commandInterface.set({inst, {"template"}}, prefab); - commandInterface.moveScenegraphChild(node, prefab); + commandInterface.moveScenegraphChildren({node}, prefab); checkUndoRedo([this, node, prefab]() { commandInterface.deleteObjects({node}); }, [this, inst]() { @@ -91,7 +91,7 @@ TEST_F(PrefabTest, set_node_prop) { auto inst = create("inst"); auto node = create("node"); commandInterface.set({inst, {"template"}}, prefab); - commandInterface.moveScenegraphChild(node, prefab); + commandInterface.moveScenegraphChildren({node}, prefab); auto instChildren = inst->children_->asVector(); EXPECT_EQ(instChildren.size(), 1); @@ -167,13 +167,13 @@ TEST_F(PrefabTest, link_broken_inside_prefab_status_gets_propagated_to_instances commandInterface.set({inst, {"template"}}, prefab); auto node = create("node"); - commandInterface.moveScenegraphChild(node, prefab); + commandInterface.moveScenegraphChildren({node}, prefab); auto luaPrefabGlobal = create("luaPrefab"); - commandInterface.moveScenegraphChild(luaPrefabGlobal, prefab); + commandInterface.moveScenegraphChildren({luaPrefabGlobal}, prefab); auto luaPrefabNodeChild = create("luaPrefabNode"); - commandInterface.moveScenegraphChild(luaPrefabNodeChild, node); + commandInterface.moveScenegraphChildren({luaPrefabNodeChild}, node); commandInterface.set({luaPrefabGlobal, {"uri"}}, cwd_path().append("scripts/types-scalar.lua").string()); commandInterface.set({luaPrefabNodeChild, {"uri"}}, cwd_path().append("scripts/SimpleScript.lua").string()); @@ -290,24 +290,24 @@ end TEST_F(PrefabTest, nesting_move_node_in) { - auto prefab_1 = create("prefab 1"); - auto prefab_2 = create("prefab 2"); + auto prefab1 = create("prefab 1"); + auto prefab2 = create("prefab 2"); - auto inst_1 = create("inst 1"); - commandInterface.set({inst_1, {"template"}}, prefab_1); - commandInterface.moveScenegraphChild(inst_1, prefab_2); + auto inst1 = create("inst 1"); + commandInterface.set({inst1, {"template"}}, prefab1); + commandInterface.moveScenegraphChildren({inst1}, prefab2); - auto inst_2 = create("inst 2"); - commandInterface.set({inst_2, {"template"}}, prefab_2); + auto inst2 = create("inst 2"); + commandInterface.set({inst2, {"template"}}, prefab2); - auto inst_2_children = inst_2->children_->asVector(); + auto inst_2_children = inst2->children_->asVector(); EXPECT_EQ(inst_2_children.size(), 1); auto inst_2_child = inst_2_children[0]->as(); EXPECT_TRUE(inst_2_child); auto node = create("node"); - checkUndoRedo([this, node, prefab_1]() { commandInterface.moveScenegraphChild(node, prefab_1); }, + checkUndoRedo([this, node, prefab1]() { commandInterface.moveScenegraphChildren({node}, prefab1); }, [this, inst_2_child]() { EXPECT_EQ(inst_2_child->children_->size(), 0); }, @@ -318,23 +318,23 @@ TEST_F(PrefabTest, nesting_move_node_in) { } TEST_F(PrefabTest, nesting_set_node_prop) { - auto prefab_1 = create("prefab 1"); - auto prefab_2 = create("prefab 2"); + auto prefab1 = create("prefab 1"); + auto prefab2 = create("prefab 2"); - auto inst_1 = create("inst 1"); - commandInterface.set({inst_1, {"template"}}, prefab_1); - commandInterface.moveScenegraphChild(inst_1, prefab_2); + auto inst1 = create("inst 1"); + commandInterface.set({inst1, {"template"}}, prefab1); + commandInterface.moveScenegraphChildren({inst1}, prefab2); - auto inst_2 = create("inst 2"); - commandInterface.set({inst_2, {"template"}}, prefab_2); + auto inst2 = create("inst 2"); + commandInterface.set({inst2, {"template"}}, prefab2); - auto inst_2_children = inst_2->children_->asVector(); + auto inst_2_children = inst2->children_->asVector(); EXPECT_EQ(inst_2_children.size(), 1); auto inst_2_child = inst_2_children[0]->as(); EXPECT_TRUE(inst_2_child); auto node = create("node"); - commandInterface.moveScenegraphChild(node, prefab_1); + commandInterface.moveScenegraphChildren({node}, prefab1); EXPECT_EQ(inst_2_child->children_->size(), 1); auto instNode = inst_2_child->children_->asVector()[0]->as(); @@ -388,17 +388,17 @@ end } TEST_F(PrefabTest, loop_instance_no_valid_template_check) { - auto prefab_1 = create("prefab 1"); - auto prefab_2 = create("prefab 2"); - auto inst_1 = create("inst 1"); - auto inst_2 = create("inst 2"); - commandInterface.moveScenegraphChild(inst_1, prefab_1); - commandInterface.moveScenegraphChild(inst_2, prefab_2); + auto prefab1 = create("prefab 1"); + auto prefab2 = create("prefab 2"); + auto inst1 = create("inst 1"); + auto inst2 = create("inst 2"); + commandInterface.moveScenegraphChildren({inst1}, prefab1); + commandInterface.moveScenegraphChildren({inst2}, prefab2); - commandInterface.set({inst_1, {"template"}}, prefab_2); - EXPECT_EQ(*inst_1->template_, prefab_2); + commandInterface.set({inst1, {"template"}}, prefab2); + EXPECT_EQ(*inst1->template_, prefab2); - auto valid = Queries::findAllValidReferenceTargets(project, {inst_2, {"template"}}); + auto valid = Queries::findAllValidReferenceTargets(project, {inst2, {"template"}}); EXPECT_EQ(valid.size(), 0); } @@ -408,8 +408,8 @@ TEST_F(PrefabTest, delete_prefab_with_node_with_meshnode_while_instance_exists) auto node = create("node"); auto meshNode = create("meshNode"); commandInterface.set({inst, {"template"}}, prefab); - commandInterface.moveScenegraphChild(node, prefab); - commandInterface.moveScenegraphChild(meshNode, node); + commandInterface.moveScenegraphChildren({node}, prefab); + commandInterface.moveScenegraphChildren({meshNode}, node); EXPECT_EQ(*inst->template_, prefab); @@ -517,7 +517,7 @@ end ASSERT_EQ(project.links().size(), 2); // Use context here to perform prefab update only after both operations are complete - context.moveScenegraphChild(meshnode, prefab); + context.moveScenegraphChildren({meshnode}, prefab); context.deleteObjects({node}); raco::core::PrefabOperations::globalPrefabUpdate(context, context.modelChanges()); @@ -537,3 +537,59 @@ end }); ASSERT_EQ(project.links().size(), 2); } + +TEST_F(PrefabTest, update_luascript_module_dependant_in_prefab_no_module) { + auto prefab = create("prefab"); + auto inst = create("inst"); + commandInterface.set({inst, &PrefabInstance::template_}, prefab); + + auto lua = create("lua", prefab); + commandInterface.set({lua, &LuaScript::uri_}, (cwd_path() / "scripts/moduleDependency.lua").string()); + commandInterface.moveScenegraphChildren({lua}, prefab); + + auto inst_lua = raco::select(inst->children_->asVector()); + ASSERT_NE(inst_lua, nullptr); + ASSERT_TRUE(commandInterface.errors().hasError({lua})); + ASSERT_TRUE(commandInterface.errors().hasError({inst_lua})); +} + +TEST_F(PrefabTest, update_luascript_module_dependant_in_prefab_add_module) { + auto prefab = create("prefab"); + auto inst = create("inst"); + commandInterface.set({inst, &PrefabInstance::template_}, prefab); + + auto lua = create("lua", prefab); + commandInterface.set({lua, &LuaScript::uri_}, (cwd_path() / "scripts/moduleDependency.lua").string()); + commandInterface.moveScenegraphChildren({lua}, prefab); + + auto luaModule = create("luaModule"); + commandInterface.set({luaModule, &LuaScriptModule::uri_}, (cwd_path() / "scripts/moduleDefinition.lua").string()); + + commandInterface.set({lua, {"luaModules", "coalas"}}, luaModule); + + auto inst_lua = raco::select(inst->children_->asVector()); + ASSERT_NE(inst_lua, nullptr); + ASSERT_FALSE(commandInterface.errors().hasError({lua})); + ASSERT_FALSE(commandInterface.errors().hasError({inst_lua})); +} + +TEST_F(PrefabTest, update_luascript_module_dependant_in_prefab_remove_module) { + auto prefab = create("prefab"); + auto inst = create("inst"); + commandInterface.set({inst, &PrefabInstance::template_}, prefab); + + auto lua = create("lua", prefab); + commandInterface.set({lua, &LuaScript::uri_}, (cwd_path() / "scripts/moduleDependency.lua").string()); + commandInterface.moveScenegraphChildren({lua}, prefab); + + auto luaModule = create("luaModule"); + commandInterface.set({luaModule, &LuaScriptModule::uri_}, (cwd_path() / "scripts/moduleDefinition.lua").string()); + + commandInterface.set({lua, {"luaModules", "coalas"}}, luaModule); + commandInterface.set({lua, {"luaModules", "coalas"}}, SEditorObject{}); + + auto inst_lua = raco::select(inst->children_->asVector()); + ASSERT_NE(inst_lua, nullptr); + ASSERT_TRUE(commandInterface.errors().hasError({lua})); + ASSERT_TRUE(commandInterface.errors().hasError({inst_lua})); +} diff --git a/datamodel/libCore/tests/Queries_Tags_test.cpp b/datamodel/libCore/tests/Queries_Tags_test.cpp index 2eb326c6..2f7f669d 100644 --- a/datamodel/libCore/tests/Queries_Tags_test.cpp +++ b/datamodel/libCore/tests/Queries_Tags_test.cpp @@ -42,8 +42,8 @@ class QueriesTagTest : public TestEnvironmentCore { commandInterface.set({meshnode_, {"mesh"}}, mesh_); commandInterface.set({meshnodeInPrefab_, {"mesh"}}, mesh_); - commandInterface.moveScenegraphChild(meshnode_, rootnode_); - commandInterface.moveScenegraphChild(meshnodeInPrefab_, prefab_); + commandInterface.moveScenegraphChildren({meshnode_}, rootnode_); + commandInterface.moveScenegraphChildren({meshnodeInPrefab_}, prefab_); commandInterface.set(ValueHandle{rootnode_, &Node::tags_}, std::vector{"rntag1", "rntag2"}); commandInterface.set(ValueHandle{meshnode_, &MeshNode::tags_}, std::vector{"tag1", "tag2"}); diff --git a/datamodel/libCore/tests/Serialization_test.cpp b/datamodel/libCore/tests/Serialization_test.cpp index 8a8bb799..78939b45 100644 --- a/datamodel/libCore/tests/Serialization_test.cpp +++ b/datamodel/libCore/tests/Serialization_test.cpp @@ -95,7 +95,7 @@ TEST_F(SerializationTest, serializeMeshNodeWithMesh) { TEST_F(SerializationTest, serializeNodeWithChildMeshNode) { const auto sMeshNode{commandInterface.createObject(raco::user_types::MeshNode::typeDescription.typeName, "mesh_node", "mesh_node_id")}; const auto sNode{commandInterface.createObject(raco::user_types::Node::typeDescription.typeName, "node", "node_id")}; - commandInterface.moveScenegraphChild(sMeshNode, sNode); + commandInterface.moveScenegraphChildren({sMeshNode}, sNode); auto result = raco::serialization::serialize(sNode); if (WRITE_RESULT) raco::utils::file::write((std::filesystem::path{CMAKE_CURRENT_SOURCE_DIR} / "expectations" / "NodeWithChildMeshNode.json").string(), result); diff --git a/datamodel/libCore/tests/Undo_test.cpp b/datamodel/libCore/tests/Undo_test.cpp index 2345bbbe..11d3a236 100644 --- a/datamodel/libCore/tests/Undo_test.cpp +++ b/datamodel/libCore/tests/Undo_test.cpp @@ -7,6 +7,7 @@ * 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 "core/Undo.h" #include "core/Context.h" #include "core/Handles.h" #include "core/MeshCacheInterface.h" @@ -18,9 +19,9 @@ #include "testing/TestEnvironmentCore.h" #include "testing/TestUtil.h" #include "user_types/UserObjectFactory.h" - #include "user_types/Animation.h" #include "user_types/AnimationChannel.h" +#include "user_types/LuaScriptModule.h" #include "user_types/Mesh.h" #include "user_types/MeshNode.h" #include "user_types/Node.h" @@ -35,7 +36,7 @@ using namespace raco::core; using namespace raco::user_types; -template +template class UndoTestT : public TestEnvironmentCoreT { public: template @@ -69,16 +70,21 @@ class UndoTestT : public TestEnvironmentCoreT { } template - std::shared_ptr getInstance(std::string objectName) { - auto it = std::find_if(this->project.instances().begin(), this->project.instances().end(), [objectName](const SEditorObject& obj) -> bool { + std::shared_ptr getInstance(Project& project, std::string objectName) { + auto it = std::find_if(project.instances().begin(), project.instances().end(), [objectName](const SEditorObject& obj) -> bool { return objectName == obj->objectName(); }); - if (it != this->project.instances().end()) { + if (it != project.instances().end()) { return std::dynamic_pointer_cast(*it); } return nullptr; } + template + std::shared_ptr getInstance(std::string objectName) { + return getInstance(this->project, objectName); + } + void checkInstances(const std::vector& present, const std::vector& absent) { EXPECT_EQ(this->project.instances().size(), present.size()); for (auto name : present) { @@ -90,7 +96,12 @@ class UndoTestT : public TestEnvironmentCoreT { } }; -using UndoTest = UndoTestT<>; +class UndoTest : public UndoTestT<> { +public: + std::vector>& stack() { + return undoStack.stack(); + } +}; TEST_F(UndoTest, Node_undoredo_single_op) { auto node = commandInterface.createObject(Node::typeDescription.typeName, "node"); @@ -191,7 +202,7 @@ TEST_F(UndoTest, ScenegraphMove_simple) { auto node = commandInterface.createObject(Node::typeDescription.typeName, "parent"); auto child = commandInterface.createObject(Node::typeDescription.typeName, "child"); - checkUndoRedo([this, node, child]() { commandInterface.moveScenegraphChild(child, node); }, + checkUndoRedo([this, node, child]() { commandInterface.moveScenegraphChildren({child}, node); }, [this, node, child]() { EXPECT_EQ(node->children_->size(), 0); EXPECT_EQ(child->getParent(), nullptr); @@ -205,12 +216,12 @@ TEST_F(UndoTest, ScenegraphMove_simple) { TEST_F(UndoTest, ScenegraphMove_multiple_children) { auto node = commandInterface.createObject(Node::typeDescription.typeName, "parent"); auto child1 = commandInterface.createObject(Node::typeDescription.typeName, "child1"); - commandInterface.moveScenegraphChild(child1, node); + commandInterface.moveScenegraphChildren({child1}, node); auto child2 = commandInterface.createObject(Node::typeDescription.typeName, "child2"); checkUndoRedoMultiStep<2>( - {[this, node, child1, child2]() { commandInterface.moveScenegraphChild(child2, node); }, - [this, node, child1, child2]() { commandInterface.moveScenegraphChild(child1, nullptr); }}, + {[this, node, child1, child2]() { commandInterface.moveScenegraphChildren({child2}, node); }, + [this, node, child1, child2]() { commandInterface.moveScenegraphChildren({child1}, nullptr); }}, {[this, node, child1, child2]() { EXPECT_EQ(node->children_->asVector(), std::vector({child1})); EXPECT_EQ(child1->getParent(), node); @@ -276,7 +287,7 @@ TEST_F(UndoTest, Delete_single) { TEST_F(UndoTest, DeleteNodeWithChild) { auto node = commandInterface.createObject(Node::typeDescription.typeName, "parent"); auto child = commandInterface.createObject(Node::typeDescription.typeName, "child"); - commandInterface.moveScenegraphChild(child, node); + commandInterface.moveScenegraphChildren({child}, node); checkUndoRedo([this, node]() { commandInterface.deleteObjects({node}); }, [this, node, child]() { @@ -290,7 +301,7 @@ TEST_F(UndoTest, DeleteNodeWithChild) { TEST_F(UndoTest, DeleteNodeInParent) { auto node = commandInterface.createObject(Node::typeDescription.typeName, "parent"); auto child = commandInterface.createObject(Node::typeDescription.typeName, "child"); - commandInterface.moveScenegraphChild(child, node); + commandInterface.moveScenegraphChildren({child}, node); checkUndoRedo([this, child]() { commandInterface.deleteObjects({child}); }, [this, node]() { @@ -418,7 +429,7 @@ TEST_F(UndoTest, deepCut) { auto node = create("node"); auto meshNode = create("meshnode"); auto mesh = create("mesh"); - commandInterface.moveScenegraphChild(meshNode, node); + commandInterface.moveScenegraphChildren({meshNode}, node); commandInterface.set({meshNode, {"mesh"}}, mesh); checkUndoRedo([this, node]() { commandInterface.cutObjects({node}, true); }, @@ -508,7 +519,7 @@ TEST_F(UndoTest, link_broken_fix_link_with_correct_output) { // link gets broken here commandInterface.set(ValueHandle{linkBase, {"uri"}}, cwd_path().append("scripts/SimpleScript.lua").string()); - // Simulate user doing stuff after changing URI to prevent undo stack merging when changing URI again. + // Simulate user doing stuff after changing URI to prevent undo stack merging when changing URI again. create("Node"); checkUndoRedo([this, linkBase]() { commandInterface.set(ValueHandle{linkBase, {"uri"}}, cwd_path().append("scripts/types-scalar.lua").string()); }, @@ -545,6 +556,68 @@ TEST_F(UndoTest, link_input_changed_add_another_link) { }); } +TEST_F(UndoTest, lua_module_added) { + auto script = create("script"); + commandInterface.set(ValueHandle{script, &raco::user_types::LuaScript::uri_}, cwd_path().append("scripts/moduleDependency.lua").string()); + + auto module = create("module"); + commandInterface.set(ValueHandle{module, &raco::user_types::LuaScriptModule::uri_}, cwd_path().append("scripts/moduleDefinition.lua").string()); + + checkUndoRedo([this, script, module]() { + commandInterface.set(ValueHandle{script, {"luaModules", "coalas"}}, module); + }, + [this, script, module]() { + ASSERT_TRUE(commandInterface.errors().hasError({script})); + auto coalasRef = ValueHandle{script, {"luaModules", "coalas"}}.asRef(); + ASSERT_EQ(coalasRef, nullptr); + }, + [this, script, module]() { + ASSERT_FALSE(commandInterface.errors().hasError({script})); + auto coalasRef = ValueHandle{script, {"luaModules", "coalas"}}.asRef(); + ASSERT_EQ(coalasRef, coalasRef); + }); +} + +TEST_F(UndoTest, lua_module_script_uri_changed) { + auto script = create("script"); + commandInterface.set(ValueHandle{script, &raco::user_types::LuaScript::uri_}, cwd_path().append("scripts/moduleDependency.lua").string()); + + auto module = create("module"); + commandInterface.set(ValueHandle{module, &raco::user_types::LuaScriptModule::uri_}, cwd_path().append("scripts/moduleDefinition.lua").string()); + commandInterface.set(ValueHandle{script, {"luaModules", "coalas"}}, module); + + checkUndoRedo([this, script, module]() { commandInterface.set(ValueHandle{script, &raco::user_types::LuaScript::uri_}, cwd_path().append("scripts/types-scalar.lua").string()); }, + [this, script]() { + ASSERT_FALSE(commandInterface.errors().hasError({script})); + auto coalasRef = ValueHandle{script, {"luaModules", "coalas"}}.asRef(); + ASSERT_EQ(coalasRef, coalasRef); + }, + [this, script]() { + ASSERT_FALSE(commandInterface.errors().hasError({script})); + ASSERT_EQ(script->get("luaModules")->asTable().size(), 0); + }); +} + +TEST_F(UndoTest, lua_module_script_module_made_invalid) { + auto script = create("script"); + commandInterface.set(ValueHandle{script, &raco::user_types::LuaScript::uri_}, cwd_path().append("scripts/moduleDependency.lua").string()); + + auto module = create("module"); + commandInterface.set(ValueHandle{module, &raco::user_types::LuaScriptModule::uri_}, cwd_path().append("scripts/moduleDefinition.lua").string()); + commandInterface.set(ValueHandle{script, {"luaModules", "coalas"}}, module); + + checkUndoRedo([this, script, module]() { commandInterface.set(ValueHandle{module, &raco::user_types::LuaScriptModule::uri_}, std::string()); }, + [this, script]() { + ASSERT_FALSE(commandInterface.errors().hasError({script})); + auto coalasRef = ValueHandle{script, {"luaModules", "coalas"}}.asRef(); + ASSERT_EQ(coalasRef, coalasRef); + }, + [this, script, module]() { + ASSERT_TRUE(commandInterface.errors().hasError({script})); + ASSERT_TRUE(commandInterface.errors().hasError({module, &raco::user_types::LuaScriptModule::uri_})); + }); +} + TEST_F(UndoTest, link_quaternion_euler_change) { raco::utils::file::write((cwd_path() / "lua_script_out1.lua").string(), R"( function interface() @@ -642,7 +715,7 @@ INSTANTIATE_TEST_SUITE_P( UndoTest, UndoTestWithIDParams, testing::Values( - std::vector({"AAA", "BBB"}), + std::vector({"AAA", "BBB"}), std::vector({"BBB", "AAA"}))); TEST_F(UndoTest, meshnode_uniform_update) { @@ -677,7 +750,7 @@ void main() { })"; auto mesh = create_mesh("mesh", "meshes/Duck.glb"); - + size_t preIndex = undoStack.getIndex(); auto material = commandInterface.createObject(Material::typeDescription.typeName, "material", "CCC"); @@ -806,4 +879,110 @@ end //ASSERT_FALSE(commandInterface.errors().hasError({node})); } +TEST_F(UndoTest, lua_link_create_inconsistent_undo_stack) { + TextFile luaFile = makeFile("test.lua", R"( +function interface() + OUT.vec = VEC3F +end + +function run() +end +)"); + + std::string altLuaScript = R"( +function interface() + OUT.renamed = VEC3F +end + +function run() +end +)"; + + auto lua = create_lua("lua", luaFile); + auto node = create("node"); + + ValueHandle luaOutputs(lua, &LuaScript::luaOutputs_); + EXPECT_TRUE(luaOutputs.hasProperty("vec")); + EXPECT_FALSE(luaOutputs.hasProperty("renamed")); + + recorder.reset(); + raco::utils::file::write(luaFile.path.string(), altLuaScript); + EXPECT_TRUE(raco::awaitPreviewDirty(recorder, lua)); + + EXPECT_FALSE(luaOutputs.hasProperty("vec")); + EXPECT_TRUE(luaOutputs.hasProperty("renamed")); + + // force restore from undo stack + commandInterface.undoStack().setIndex(commandInterface.undoStack().getIndex(), true); + EXPECT_FALSE(luaOutputs.hasProperty("vec")); + EXPECT_TRUE(luaOutputs.hasProperty("renamed")); + + auto [sprop, eprop] = link(lua, {"luaOutputs", "renamed"}, node, {"translation"}); + checkLinks({{sprop, eprop, true}}); + + { + Project& stackProject = stack().back()->state; + + auto stackLua = getInstance(stackProject, "lua"); + ASSERT_EQ(stackProject.links().size(), 1); + auto stackLink = stackProject.links()[0]; + + ValueHandle startProp{stackLua, stackLink->startPropertyNamesVector()}; + EXPECT_TRUE(startProp && *stackLink->isValid_ || !startProp && !*stackLink->isValid_); + } +} + +TEST_F(UndoTest, link_redo_creates_impossible_link) { + std::string origLuaScript = R"( +function interface() + OUT.vec = VEC3F +end + +function run() +end +)"; + + std::string altLuaScript = R"( +function interface() + OUT.renamed = VEC3F +end + +function run() +end +)"; + + TextFile luaFile = makeFile("test.lua", origLuaScript); + auto lua = create_lua("lua", luaFile); + auto node = create("node"); + + ValueHandle luaOutputs(lua, &LuaScript::luaOutputs_); + EXPECT_TRUE(luaOutputs.hasProperty("vec")); + EXPECT_FALSE(luaOutputs.hasProperty("renamed")); + + recorder.reset(); + raco::utils::file::write(luaFile.path.string(), altLuaScript); + EXPECT_TRUE(raco::awaitPreviewDirty(recorder, lua)); + + EXPECT_FALSE(luaOutputs.hasProperty("vec")); + EXPECT_TRUE(luaOutputs.hasProperty("renamed")); + + // force restore from undo stack + commandInterface.undoStack().setIndex(commandInterface.undoStack().getIndex(), true); + EXPECT_FALSE(luaOutputs.hasProperty("vec")); + EXPECT_TRUE(luaOutputs.hasProperty("renamed")); + + auto [sprop, eprop] = link(lua, {"luaOutputs", "renamed"}, node, {"translation"}); + checkLinks({{sprop, eprop, true}}); + + raco::utils::file::write(luaFile.path.string(), origLuaScript); + EXPECT_TRUE(raco::awaitPreviewDirty(recorder, lua)); + + undoStack.undo(); + checkLinks({}); + + undoStack.redo(); + checkLinks({{sprop, eprop, false}}); + ASSERT_FALSE(static_cast(ValueHandle(sprop))); +} + #endif \ No newline at end of file diff --git a/datamodel/libCore/tests/migrationTestData/version-current.rca b/datamodel/libCore/tests/migrationTestData/version-current.rca index 5588933b..2d8494c4 100644 --- a/datamodel/libCore/tests/migrationTestData/version-current.rca +++ b/datamodel/libCore/tests/migrationTestData/version-current.rca @@ -1,7 +1,7 @@ { "externalProjects": { }, - "fileVersion": 19, + "fileVersion": 21, "instances": [ { "properties": { @@ -841,6 +841,7 @@ "value": 1 }, "flipTexture": false, + "generateMipmaps": false, "magSamplingMethod": 0, "minSamplingMethod": 0, "objectID": "0d2c96e3-0ed2-4f95-ac54-03b9fa81e963", @@ -1865,6 +1866,14 @@ "rewindOnStop": false }, "typeName": "Animation" + }, + { + "properties": { + "objectID": "04ada1c4-7e3a-4f9e-983d-4724a073cad8", + "objectName": "LuaScriptModule", + "uri": "" + }, + "typeName": "LuaScriptModule" } ], "links": [ @@ -1876,8 +1885,8 @@ ], "racoVersion": [ 0, - 9, - 3 + 10, + 0 ], "ramsesVersion": [ 27, diff --git a/datamodel/libTesting/include/testing/TestEnvironmentCore.h b/datamodel/libTesting/include/testing/TestEnvironmentCore.h index a23f9b13..38fd9b15 100644 --- a/datamodel/libTesting/include/testing/TestEnvironmentCore.h +++ b/datamodel/libTesting/include/testing/TestEnvironmentCore.h @@ -45,6 +45,16 @@ inline void clearQEventLoop() { QCoreApplication::processEvents(); } + +class TestUndoStack : public raco::core::UndoStack { +public: + using Entry = raco::core::UndoStack::Entry; + + std::vector>& stack() { + return stack_; + } +}; + template struct TestEnvironmentCoreT : public RacoBaseTest { using BaseContext = raco::core::BaseContext; @@ -63,7 +73,7 @@ struct TestEnvironmentCoreT : public RacoBaseTest { std::shared_ptr create(std::string name, raco::core::SEditorObject parent = nullptr, const std::vector& tags = {}) { auto obj = std::dynamic_pointer_cast(commandInterface.createObject(C::typeDescription.typeName, name)); if (parent) { - commandInterface.moveScenegraphChild(obj, parent); + commandInterface.moveScenegraphChildren({obj}, parent); } if (!tags.empty()) { context.set({obj, {"tags"}}, tags); @@ -75,7 +85,7 @@ struct TestEnvironmentCoreT : public RacoBaseTest { std::shared_ptr create(raco::core::CommandInterface& cmd, std::string name, raco::core::SEditorObject parent = nullptr) { auto obj = std::dynamic_pointer_cast(cmd.createObject(C::typeDescription.typeName, name)); if (parent) { - cmd.moveScenegraphChild(obj, parent); + cmd.moveScenegraphChildren({obj}, parent); } return obj; } @@ -192,7 +202,7 @@ struct TestEnvironmentCoreT : public RacoBaseTest { raco::core::DataChangeRecorder recorder{}; Errors errors{&recorder}; BaseContext context{&project, backend.coreInterface(), objectFactory(), &recorder, &errors}; - raco::core::UndoStack undoStack{&context}; + TestUndoStack undoStack{&context}; raco::core::CommandInterface commandInterface{&context, &undoStack}; }; diff --git a/datamodel/libUserTypes/CMakeLists.txt b/datamodel/libUserTypes/CMakeLists.txt index 7e33742e..5645b5cf 100644 --- a/datamodel/libUserTypes/CMakeLists.txt +++ b/datamodel/libUserTypes/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(libUserTypes include/user_types/DefaultValues.h include/user_types/Enumerations.h src/Enumerations.cpp include/user_types/LuaScript.h src/LuaScript.cpp + include/user_types/LuaScriptModule.h src/LuaScriptModule.cpp include/user_types/Material.h src/Material.cpp include/user_types/Mesh.h src/Mesh.cpp include/user_types/MeshNode.h src/MeshNode.cpp diff --git a/datamodel/libUserTypes/include/user_types/LuaScript.h b/datamodel/libUserTypes/include/user_types/LuaScript.h index 6572c9f4..7fae6b37 100644 --- a/datamodel/libUserTypes/include/user_types/LuaScript.h +++ b/datamodel/libUserTypes/include/user_types/LuaScript.h @@ -24,7 +24,7 @@ class LuaScript : public BaseObject { return typeDescription; } - LuaScript(LuaScript const& other) : BaseObject(other), uri_(other.uri_), luaInputs_(other.luaInputs_), luaOutputs_(other.luaOutputs_) { + LuaScript(LuaScript const& other) : BaseObject(other), uri_(other.uri_), luaModules_(other.luaModules_), luaInputs_(other.luaInputs_), luaOutputs_(other.luaOutputs_) { fillPropertyDescription(); } @@ -35,6 +35,7 @@ class LuaScript : public BaseObject { void fillPropertyDescription() { properties_.emplace_back("uri", &uri_); + properties_.emplace_back("luaModules", &luaModules_); properties_.emplace_back("luaInputs", &luaInputs_); properties_.emplace_back("luaOutputs", &luaOutputs_); } @@ -42,22 +43,23 @@ class LuaScript : public BaseObject { void onBeforeDeleteObject(Errors& errors) const override; void onAfterContextActivated(BaseContext& context) override; + void onAfterReferencedObjectChanged(BaseContext& context, ValueHandle const& changedObject) override; void onAfterValueChanged(BaseContext& context, ValueHandle const& value) override; Property uri_{std::string{}, {"Lua script files(*.lua)"}, DisplayNameAnnotation("URI")}; + Property luaModules_{{}, DisplayNameAnnotation("Modules")}; Property luaInputs_ {{}, DisplayNameAnnotation("Inputs")}; Property luaOutputs_{{}, DisplayNameAnnotation("Outputs")}; private: void syncLuaInterface(BaseContext& context); + void syncLuaModules(BaseContext& context, const std::string& fileContents, std::string &outError); mutable FileChangeMonitor::UniqueListener uriListener_; OutdatedPropertiesStore cachedLuaInputValues_; - - std::string currentScriptContents_; - bool currentScriptValid_ = false; + std::map cachedModuleRefs_; }; using SLuaScript = std::shared_ptr; diff --git a/datamodel/libUserTypes/include/user_types/LuaScriptModule.h b/datamodel/libUserTypes/include/user_types/LuaScriptModule.h new file mode 100644 index 00000000..d044ebb3 --- /dev/null +++ b/datamodel/libUserTypes/include/user_types/LuaScriptModule.h @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of Ramses Composer + * (see https://github.com/GENIVI/ramses-composer). + * + * 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 "user_types/BaseObject.h" +#include "user_types/SyncTableWithEngineInterface.h" + +#include "core/FileChangeMonitor.h" +#include +#include + +namespace raco::user_types { + +class LuaScriptModule : public BaseObject { +public: + static inline const TypeDescriptor typeDescription = { "LuaScriptModule", true }; + TypeDescriptor const& getTypeDescription() const override { + return typeDescription; + } + + static inline const std::set LUA_STANDARD_MODULES = { + "string", + "table", + "math", + "debug"}; + + LuaScriptModule(LuaScriptModule const& other) : BaseObject(other), uri_(other.uri_) { + fillPropertyDescription(); + } + + LuaScriptModule(std::string name = std::string(), std::string id = std::string()) + : BaseObject(name, id) { + fillPropertyDescription(); + } + + void fillPropertyDescription() { + properties_.emplace_back("uri", &uri_); + } + + void onBeforeDeleteObject(Errors& errors) const override; + + void onAfterContextActivated(BaseContext& context) override; + void onAfterValueChanged(BaseContext& context, ValueHandle const& value) override; + + Property uri_{std::string{}, {"Lua script files(*.lua)"}, DisplayNameAnnotation("URI")}; + + std::string currentScriptContents_; +private: + + void syncLuaInterface(BaseContext& context); + + mutable FileChangeMonitor::UniqueListener uriListener_; +}; + +using SLuaScriptModule = std::shared_ptr; + +} diff --git a/datamodel/libUserTypes/include/user_types/Texture.h b/datamodel/libUserTypes/include/user_types/Texture.h index 7bc63486..7b9e1ccf 100644 --- a/datamodel/libUserTypes/include/user_types/Texture.h +++ b/datamodel/libUserTypes/include/user_types/Texture.h @@ -34,6 +34,7 @@ class Texture : public TextureSampler2DBase { void fillPropertyDescription() { properties_.emplace_back("uri", &uri_); properties_.emplace_back("flipTexture", &flipTexture_); + properties_.emplace_back("generateMipmaps", &generateMipmaps_); } void onBeforeDeleteObject(Errors& errors) const override; @@ -44,6 +45,7 @@ class Texture : public TextureSampler2DBase { Property uri_{std::string{}, {"Image files(*.png)"}, DisplayNameAnnotation("URI")}; Property flipTexture_{false, DisplayNameAnnotation("Flip Vertically")}; + Property generateMipmaps_{false, DisplayNameAnnotation("Generate Mipmaps")}; private: mutable FileChangeMonitor::UniqueListener uriListener_; diff --git a/datamodel/libUserTypes/src/LuaScript.cpp b/datamodel/libUserTypes/src/LuaScript.cpp index dafdba75..98bea504 100644 --- a/datamodel/libUserTypes/src/LuaScript.cpp +++ b/datamodel/libUserTypes/src/LuaScript.cpp @@ -13,7 +13,9 @@ #include "core/Handles.h" #include "core/PathQueries.h" #include "core/Project.h" +#include "core/Queries.h" #include "log_system/log.h" +#include "user_types/LuaScriptModule.h" #include "utils/FileUtils.h" namespace raco::user_types { @@ -25,11 +27,14 @@ void LuaScript::onBeforeDeleteObject(Errors& errors) const { void LuaScript::onAfterContextActivated(BaseContext& context) { uriListener_ = registerFileChangedHandler(context, {shared_from_this(), &LuaScript::uri_}, [this, &context]() { this->syncLuaInterface(context); }); - currentScriptValid_ = false; - currentScriptContents_.clear(); syncLuaInterface(context); } +void LuaScript::onAfterReferencedObjectChanged(BaseContext& context, ValueHandle const& changedObject) { + syncLuaInterface(context); + context.changeMultiplexer().recordPreviewDirty(shared_from_this()); +} + void LuaScript::onAfterValueChanged(BaseContext& context, ValueHandle const& value) { if (value.isRefToProp(&LuaScript::uri_)) { uriListener_ = registerFileChangedHandler(context, value, [this, &context]() { this->syncLuaInterface(context); }); @@ -39,24 +44,30 @@ void LuaScript::onAfterValueChanged(BaseContext& context, ValueHandle const& val if (ValueHandle(shared_from_this(), &LuaScript::objectName_) == value) { context.updateBrokenLinkErrorsAttachedTo(shared_from_this()); } + + const auto& moduleTable = luaModules_.asTable(); + for (auto i = 0; i < moduleTable.size(); ++i) { + if (value == ValueHandle{shared_from_this(), {"luaModules", moduleTable.name(i)}}) { + syncLuaInterface(context); + return; + } + } } void LuaScript::syncLuaInterface(BaseContext& context) { - std::string luaScript = utils::file::read(PathQueries::resolveUriPropertyToAbsolutePath(*context.project(), {shared_from_this(), &LuaScript::uri_})); - if (currentScriptValid_ && luaScript == currentScriptContents_) { - return; - } - currentScriptContents_ = luaScript; - currentScriptValid_ = true; + std::string luaScript = utils::file::read(PathQueries::resolveUriPropertyToAbsolutePath(*context.project(), {shared_from_this(), &LuaScript::uri_})); PropertyInterfaceList inputs{}; PropertyInterfaceList outputs{}; std::string error{}; - if (!luaScript.empty()) { - context.engineInterface().parseLuaScript(luaScript, inputs, outputs, error); + context.errors().removeError({shared_from_this()}); + + syncLuaModules(context, luaScript, error); + + if (error.empty() && !luaScript.empty()) { + context.engineInterface().parseLuaScript(luaScript, luaModules_.asTable(), inputs, outputs, error); } - context.errors().removeError({shared_from_this()}); if (validateURI(context, {shared_from_this(), &LuaScript::uri_})) { if (error.size() > 0) { context.errors().addError(ErrorCategory::PARSE_ERROR, ErrorLevel::ERROR, shared_from_this(), error); @@ -76,7 +87,47 @@ void LuaScript::syncLuaInterface(BaseContext& context) { context.updateBrokenLinkErrorsAttachedTo(shared_from_this()); + context.changeMultiplexer().recordValueChanged({shared_from_this(), &raco::user_types::LuaScript::luaModules_}); context.changeMultiplexer().recordPreviewDirty(shared_from_this()); } +void LuaScript::syncLuaModules(BaseContext& context, const std::string& fileContents, std::string& outError) { + std::vector moduleDeps; + auto& moduleTable = luaModules_.asTable(); + + context.engineInterface().extractLuaDependencies(fileContents, moduleDeps, outError); + if (!outError.empty()) { + moduleTable.clear(); + return; + } + + for (auto i = 0; i < moduleTable.size(); ++i) { + cachedModuleRefs_[moduleTable.name(i)] = moduleTable.get(i)->asRef(); + } + + moduleTable.clear(); + + std::vector redeclaredStandardModules; + for (const auto& dep : moduleDeps) { + if (raco::user_types::LuaScriptModule::LUA_STANDARD_MODULES.find(dep) != raco::user_types::LuaScriptModule::LUA_STANDARD_MODULES.end()) { + redeclaredStandardModules.emplace_back(dep); + } + } + + if (!redeclaredStandardModules.empty()) { + outError = fmt::format("Error while parsing Lua script file: Found redeclaration of standard Lua module{} {}", redeclaredStandardModules.size() == 1 ? "" : "s", fmt::join(redeclaredStandardModules, ", ")); + return; + } + + for (const auto& dep : moduleDeps) { + auto moduleRef = std::unique_ptr(new Value()); + auto newDep = context.addProperty({shared_from_this(), &LuaScript::luaModules_}, dep, std::move(moduleRef)); + + auto cachedModuleIt = cachedModuleRefs_.find(dep); + if (cachedModuleIt != cachedModuleRefs_.end()) { + newDep->setRef(cachedModuleIt->second); + } + } +} + } // namespace raco::user_types diff --git a/datamodel/libUserTypes/src/LuaScriptModule.cpp b/datamodel/libUserTypes/src/LuaScriptModule.cpp new file mode 100644 index 00000000..9f67c2c1 --- /dev/null +++ b/datamodel/libUserTypes/src/LuaScriptModule.cpp @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of Ramses Composer + * (see https://github.com/GENIVI/ramses-composer). + * + * 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 "user_types/LuaScriptModule.h" +#include "Validation.h" +#include "core/Context.h" +#include "core/Handles.h" +#include "core/PathQueries.h" +#include "core/Project.h" +#include "log_system/log.h" +#include "utils/FileUtils.h" + +namespace raco::user_types { + +void LuaScriptModule::onBeforeDeleteObject(Errors& errors) const { + EditorObject::onBeforeDeleteObject(errors); + uriListener_.reset(); +} + +void LuaScriptModule::onAfterContextActivated(BaseContext& context) { + uriListener_ = registerFileChangedHandler(context, {shared_from_this(), &LuaScriptModule::uri_}, [this, &context]() { this->syncLuaInterface(context); }); + syncLuaInterface(context); +} + +void LuaScriptModule::onAfterValueChanged(BaseContext& context, ValueHandle const& value) { + if (value.isRefToProp(&LuaScriptModule::uri_)) { + uriListener_ = registerFileChangedHandler(context, value, [this, &context]() { this->syncLuaInterface(context); }); + syncLuaInterface(context); + } +} + +void LuaScriptModule::syncLuaInterface(BaseContext& context) { + std::string luaScript = utils::file::read(PathQueries::resolveUriPropertyToAbsolutePath(*context.project(), {shared_from_this(), &LuaScriptModule::uri_})); + + std::string error; + if (!luaScript.empty()) { + context.engineInterface().parseLuaScriptModule(luaScript, error); + } + + context.errors().removeError({shared_from_this()}); + if (validateURI(context, {shared_from_this(), &LuaScriptModule::uri_})) { + if (error.size() > 0) { + context.errors().addError(ErrorCategory::PARSE_ERROR, ErrorLevel::ERROR, shared_from_this(), error); + } + currentScriptContents_ = luaScript; + } else { + currentScriptContents_.clear(); + } + + context.changeMultiplexer().recordPreviewDirty(shared_from_this()); +} + +} // namespace raco::user_types diff --git a/datamodel/libUserTypes/src/UserObjectFactory.cpp b/datamodel/libUserTypes/src/UserObjectFactory.cpp index 70345edc..532e943c 100644 --- a/datamodel/libUserTypes/src/UserObjectFactory.cpp +++ b/datamodel/libUserTypes/src/UserObjectFactory.cpp @@ -18,6 +18,7 @@ #include "user_types/BaseTexture.h" #include "user_types/CubeMap.h" #include "user_types/LuaScript.h" +#include "user_types/LuaScriptModule.h" #include "user_types/Material.h" #include "user_types/Mesh.h" #include "user_types/MeshNode.h" @@ -89,6 +90,7 @@ UserObjectFactory::UserObjectFactory() { OrthographicCamera, PerspectiveCamera, LuaScript, + LuaScriptModule, Texture, RenderBuffer, RenderLayer, diff --git a/datamodel/libUserTypes/tests/CMakeLists.txt b/datamodel/libUserTypes/tests/CMakeLists.txt index 38f1e38e..aacef0e7 100644 --- a/datamodel/libUserTypes/tests/CMakeLists.txt +++ b/datamodel/libUserTypes/tests/CMakeLists.txt @@ -14,6 +14,7 @@ set(TEST_SOURCES Animation_test.cpp AnimationChannel_test.cpp LuaScript_test.cpp + LuaScriptModule_test.cpp DefaultValues_test.cpp ) set(TEST_LIBRARIES @@ -34,7 +35,9 @@ raco_package_add_test_resouces( meshes/CesiumMilkTruck/CesiumMilkTruck.gltf meshes/CesiumMilkTruck/CesiumMilkTruck_data.bin meshes/Duck.glb - scripts/struct.lua scripts/array.lua scripts/compile-error.lua + scripts/moduleDefinition.lua + scripts/moduleDependency.lua + scripts/struct.lua ) diff --git a/datamodel/libUserTypes/tests/LuaScriptModule_test.cpp b/datamodel/libUserTypes/tests/LuaScriptModule_test.cpp new file mode 100644 index 00000000..c3908e30 --- /dev/null +++ b/datamodel/libUserTypes/tests/LuaScriptModule_test.cpp @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * + * This file is part of Ramses Composer + * (see https://github.com/GENIVI/ramses-composer). + * + * 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 "testing/TestEnvironmentCore.h" + +#include "user_types/LuaScriptModule.h" +#include "utils/FileUtils.h" +#include + +using namespace raco::core; +using namespace raco::user_types; + +class LuaScriptModuleTest : public TestEnvironmentCore {}; + +TEST_F(LuaScriptModuleTest, URI_setValidURI) { + auto module{commandInterface.createObject(LuaScriptModule::typeDescription.typeName)}; + ValueHandle m{module}; + ValueHandle m_uri{m.get("uri")}; + commandInterface.set(m_uri, cwd_path().append("scripts/moduleDefinition.lua").string()); + ASSERT_EQ(m_uri.asString(), cwd_path().append("scripts/moduleDefinition.lua").generic_string()); +} + +TEST_F(LuaScriptModuleTest, URI_emptyURI_error) { + auto module{commandInterface.createObject(LuaScriptModule::typeDescription.typeName)}; + ValueHandle uriHandle{module, {"uri"}}; + + ASSERT_TRUE(commandInterface.errors().hasError(uriHandle)); + ASSERT_EQ(commandInterface.errors().getError(uriHandle).level(), raco::core::ErrorLevel::WARNING); +} + +TEST_F(LuaScriptModuleTest, URI_setInvalidURI_error) { + auto module{commandInterface.createObject(LuaScriptModule::typeDescription.typeName)}; + ValueHandle uriHandle{module, {"uri"}}; + commandInterface.set(uriHandle, std::string("blah.lua")); + + ASSERT_TRUE(commandInterface.errors().hasError(uriHandle)); + ASSERT_EQ(commandInterface.errors().getError(uriHandle).level(), raco::core::ErrorLevel::ERROR); +} + +TEST_F(LuaScriptModuleTest, URI_setValidURI_noError) { + auto module{commandInterface.createObject(LuaScriptModule::typeDescription.typeName)}; + ValueHandle uriHandle{module, {"uri"}}; + commandInterface.set(uriHandle, cwd_path().append("scripts/moduleDefinition.lua").string()); + + ASSERT_FALSE(commandInterface.errors().hasError(uriHandle)); + ASSERT_FALSE(commandInterface.errors().hasError(module)); +} + +TEST_F(LuaScriptModuleTest, URI_setValidURI_noModules_error) { + auto module{commandInterface.createObject(LuaScriptModule::typeDescription.typeName)}; + ValueHandle uriHandle{module, {"uri"}}; + commandInterface.set(uriHandle, cwd_path().append("scripts/struct.lua").string()); + + ASSERT_TRUE(commandInterface.errors().hasError(module)); +} \ No newline at end of file diff --git a/datamodel/libUserTypes/tests/LuaScript_test.cpp b/datamodel/libUserTypes/tests/LuaScript_test.cpp index 012969ec..c2a7f62c 100644 --- a/datamodel/libUserTypes/tests/LuaScript_test.cpp +++ b/datamodel/libUserTypes/tests/LuaScript_test.cpp @@ -9,6 +9,7 @@ */ #include "testing/TestEnvironmentCore.h" #include "user_types/LuaScript.h" +#include "user_types/LuaScriptModule.h" #include "utils/FileUtils.h" #include @@ -247,4 +248,313 @@ end commandInterface.set(s.get("uri"), scriptFile2); ASSERT_EQ(newScript->luaInputs_->propertyNames(), std::vector({"aa", "abc", "ff", "za", "zz"})); ASSERT_EQ(newScript->luaOutputs_->propertyNames(), std::vector({"aa", "e", "zz", "zzz"})); -} \ No newline at end of file +} + +TEST_F(LuaScriptTest, modules_in_uri_are_rejected) { + auto newScript = create("script"); + ValueHandle s{newScript}; + ValueHandle uri{s.get("uri")}; + + commandInterface.set(s.get("uri"), cwd_path().append("scripts/moduleDependency.lua").string()); + + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(commandInterface.errors().getError({newScript}).level(), raco::core::ErrorLevel::ERROR); +} + + +TEST_F(LuaScriptTest, module_loaded_without_assigned_module_objects) { + auto newScript = create("script"); + ValueHandle s{newScript}; + ValueHandle uri{s.get("uri")}; + + auto scriptFile = makeFile("script.lua", R"( +modules("coalas", "module", "anothermodule") + +function interface() + IN.zz = INT + IN.aa = FLOAT + IN.abc = INT + IN.za = STRING + IN.ff = BOOL + + OUT.zz = INT + OUT.aa = FLOAT + OUT.zzz = FLOAT + OUT.e = STRING +end + +function run() +end +)"); + commandInterface.set(s.get("uri"), scriptFile); + + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(commandInterface.errors().getError({newScript}).level(), raco::core::ErrorLevel::ERROR); + ASSERT_EQ(newScript->luaModules_->propertyNames(), std::vector({"coalas", "module", "anothermodule"})); + ASSERT_TRUE(newScript->luaInputs_->propertyNames().empty()); + ASSERT_TRUE(newScript->luaOutputs_->propertyNames().empty()); +} + +TEST_F(LuaScriptTest, module_loaded_after_assigned_module_objects) { + std::array modules = { create("m1"), + create("m2"), + create("m3") }; + auto newScript = create("script"); + ValueHandle s{newScript}; + ValueHandle uri{s.get("uri")}; + + auto scriptFile = makeFile("script.lua", R"( +modules("coalas", "module", "anothermodule") + +function interface() + IN.zz = INT + IN.aa = FLOAT + IN.abc = INT + IN.za = STRING + IN.ff = BOOL + + OUT.zz = INT + OUT.aa = FLOAT + OUT.zzz = FLOAT + OUT.e = STRING +end + +function run() +end +)"); + auto moduleFile1 = makeFile("module1.lua", R"( +local coalaModule = {} + +return coalaModule +)"); + + auto moduleFile2 = makeFile("module2.lua", R"( +local mymodule = {} + +return mymodule +)"); + + auto moduleFile3 = makeFile("module3.lua", R"( +local anothermodule = {} + +return anothermodule +)"); + + commandInterface.set(s.get("uri"), scriptFile); + commandInterface.set(s.get("luaModules").get("coalas"), modules[0]); + commandInterface.set(s.get("luaModules").get("module"), modules[1]); + commandInterface.set(s.get("luaModules").get("anothermodule"), modules[2]); + + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(commandInterface.errors().getError({newScript}).level(), raco::core::ErrorLevel::ERROR); + + commandInterface.set({modules[0], &raco::user_types::LuaScriptModule::uri_}, moduleFile1); + + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(commandInterface.errors().getError({newScript}).level(), raco::core::ErrorLevel::ERROR); + + commandInterface.set({modules[1], &raco::user_types::LuaScriptModule::uri_}, moduleFile2); + + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(commandInterface.errors().getError({newScript}).level(), raco::core::ErrorLevel::ERROR); + + commandInterface.set({modules[2], &raco::user_types::LuaScriptModule::uri_}, moduleFile3); + + ASSERT_FALSE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(newScript->luaModules_->propertyNames(), std::vector({"coalas", "module", "anothermodule"})); + ASSERT_EQ(newScript->luaInputs_->propertyNames(), std::vector({"aa", "abc", "ff", "za", "zz"})); + ASSERT_EQ(newScript->luaOutputs_->propertyNames(), std::vector({"aa", "e", "zz", "zzz"})); + + + commandInterface.set({modules[2], &raco::user_types::LuaScriptModule::uri_}, std::string()); + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(commandInterface.errors().getError({newScript}).level(), raco::core::ErrorLevel::ERROR); + ASSERT_EQ(newScript->luaModules_->propertyNames(), std::vector({"coalas", "module", "anothermodule"})); + ASSERT_TRUE(newScript->luaInputs_->propertyNames().empty()); + ASSERT_TRUE(newScript->luaOutputs_->propertyNames().empty()); +} + +TEST_F(LuaScriptTest, module_amount_increased) { + std::array modules = {create("m1"), + create("m2"), + create("m3")}; + auto newScript = create("script"); + ValueHandle s{newScript}; + ValueHandle uri{s.get("uri")}; + + auto scriptFile1 = makeFile("script1.lua", R"( +modules("coalas", "module", "anothermodule") + +function interface() +end + +function run() +end +)"); + + auto scriptFile2 = makeFile("script2.lua", R"( +modules("coalas") + +function interface() +end + +function run() +end +)"); + + auto scriptFile3 = makeFile("script3.lua", R"( +modules("coalas", "module", "anothermodule", "fourthmodule") + +function interface() +end + +function run() +end +)"); + + auto moduleFile1 = makeFile("module1.lua", R"( +local coalaModule = {} + +return coalaModule +)"); + + auto moduleFile2 = makeFile("module2.lua", R"( +local mymodule = {} + +return mymodule +)"); + + auto moduleFile3 = makeFile("module3.lua", R"( +local anothermodule = {} + +return anothermodule +)"); + + commandInterface.set(s.get("uri"), scriptFile1); + commandInterface.set(s.get("luaModules").get("coalas"), modules[0]); + commandInterface.set(s.get("luaModules").get("module"), modules[1]); + commandInterface.set(s.get("luaModules").get("anothermodule"), modules[2]); + commandInterface.set({modules[0], &raco::user_types::LuaScriptModule::uri_}, moduleFile1); + commandInterface.set({modules[1], &raco::user_types::LuaScriptModule::uri_}, moduleFile2); + commandInterface.set({modules[2], &raco::user_types::LuaScriptModule::uri_}, moduleFile3); + + commandInterface.set(s.get("uri"), scriptFile2); + ASSERT_FALSE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(newScript->luaModules_->propertyNames(), std::vector({"coalas"})); + ASSERT_EQ(s.get("luaModules").get("coalas").asRef(), modules[0]); + + commandInterface.set(s.get("uri"), scriptFile3); + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(newScript->luaModules_->propertyNames(), std::vector({"coalas", "module", "anothermodule", "fourthmodule"})); + ASSERT_EQ(s.get("luaModules").get("coalas").asRef(), modules[0]); + ASSERT_EQ(s.get("luaModules").get("module").asRef(), modules[1]); + ASSERT_EQ(s.get("luaModules").get("anothermodule").asRef(), modules[2]); + ASSERT_EQ(s.get("luaModules").get("fourthmodule").asRef(), nullptr); +} + +TEST_F(LuaScriptTest, module_loaded_with_redeclaration_of_standard_lua_module) { + auto module = create("m"); + auto newScript = create("script"); + ValueHandle s{newScript}; + ValueHandle scriptUri{s.get("uri")}; + + auto scriptFile = makeFile("script.lua", R"( +modules("math", "coalas") + +function interface() + IN.zz = INT + IN.aa = FLOAT + IN.abc = INT + IN.za = STRING + IN.ff = BOOL + + OUT.zz = INT + OUT.aa = FLOAT + OUT.zzz = FLOAT + OUT.e = STRING +end + +function run() +end +)"); + + auto scriptFile2 = makeFile("script2.lua", R"( +modules("coalas") + +function interface() + IN.zz = INT + IN.aa = FLOAT + IN.abc = INT + IN.za = STRING + IN.ff = BOOL + + OUT.zz = INT + OUT.aa = FLOAT + OUT.zzz = FLOAT + OUT.e = STRING +end + +function run() +end +)"); + + auto moduleFile = makeFile("module.lua", R"( +local coalaModule = {} + +return coalaModule +)"); + + commandInterface.set(raco::core::ValueHandle{module, &raco::user_types::LuaScriptModule::uri_}, moduleFile); + commandInterface.set(scriptUri, scriptFile); + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_TRUE(newScript->luaModules_->propertyNames().empty()); + ASSERT_TRUE(newScript->luaInputs_->propertyNames().empty()); + ASSERT_TRUE(newScript->luaOutputs_->propertyNames().empty()); + + commandInterface.set(scriptUri, scriptFile2); + commandInterface.set(s.get("luaModules").get("coalas"), module); + ASSERT_FALSE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(newScript->luaModules_->propertyNames(), std::vector({"coalas"})); + ASSERT_EQ(newScript->luaInputs_->propertyNames(), std::vector({"aa", "abc", "ff", "za", "zz"})); + ASSERT_EQ(newScript->luaOutputs_->propertyNames(), std::vector({"aa", "e", "zz", "zzz"})); + + commandInterface.set(scriptUri, scriptFile); + ASSERT_TRUE(commandInterface.errors().hasError({newScript})); + ASSERT_TRUE(newScript->luaModules_->propertyNames().empty()); + ASSERT_TRUE(newScript->luaInputs_->propertyNames().empty()); + ASSERT_TRUE(newScript->luaOutputs_->propertyNames().empty()); +} + +TEST_F(LuaScriptTest, module_amount_to_zero) { + std::array modules = {create("m1"), + create("m2"), + create("m3")}; + auto newScript = create("script"); + ValueHandle s{newScript}; + ValueHandle uri{s.get("uri")}; + + auto scriptFile1 = makeFile("script1.lua", R"( +modules("coalas", "module", "anothermodule") + +function interface() +end + +function run() +end +)"); + + auto scriptFile2 = makeFile("script2.lua", R"( +function interface() +end + +function run() +end +)"); + + + commandInterface.set(uri, scriptFile1); + + commandInterface.set(uri, scriptFile2); + ASSERT_FALSE(commandInterface.errors().hasError({newScript})); + ASSERT_EQ(newScript->luaModules_.asTable().size(), 0); +} diff --git a/gui/libObjectTree/include/object_tree_view/ObjectTreeView.h b/gui/libObjectTree/include/object_tree_view/ObjectTreeView.h index 025069ac..b9c3b330 100644 --- a/gui/libObjectTree/include/object_tree_view/ObjectTreeView.h +++ b/gui/libObjectTree/include/object_tree_view/ObjectTreeView.h @@ -38,11 +38,14 @@ class ObjectTreeView : public QTreeView { void requestNewNode(EditorObject::TypeDescriptor nodeType, const std::string &nodeName, const QModelIndex &parent); void showContextMenu(const QPoint &p); - bool canCopy(const QModelIndex &parentIndex); - bool canPasteInto(const QModelIndex &parentIndex, bool asExtRef = false); + bool canCopyAtIndices(const QModelIndexList &indices); + bool canPasteIntoIndex(const QModelIndex &index, bool asExtref); QSortFilterProxyModel *proxyModel() const; + QModelIndexList getSelectedIndices(bool sorted = false) const; + QModelIndex getSelectedInsertionTargetIndex() const; + Q_SIGNALS: void dockSelectionFocusRequested(ObjectTreeView *focusTree); void newNodeRequested(EditorObject::TypeDescriptor nodeType, const std::string &nodeName, const QModelIndex &parent); diff --git a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewDefaultModel.h b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewDefaultModel.h index 83fd9bb1..83094472 100644 --- a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewDefaultModel.h +++ b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewDefaultModel.h @@ -66,7 +66,7 @@ class ObjectTreeViewDefaultModel : public QAbstractItemModel { bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; Qt::ItemFlags flags(const QModelIndex& index) const override; - QMimeData* mimeData(const QModelIndexList& indexes) const override; + QMimeData* mimeData(const QModelIndexList& indices) const override; QStringList mimeTypes() const override; Qt::DropActions supportedDropActions() const override; @@ -74,9 +74,9 @@ class ObjectTreeViewDefaultModel : public QAbstractItemModel { virtual void setUpTreeModificationFunctions(); void iterateThroughTree(std::function nodeFunc, QModelIndex& currentIndex); - QModelIndex getInvisibleRootIndex() const; ObjectTreeNode* indexToTreeNode(const QModelIndex& index) const; core::SEditorObject indexToSEditorObject(const QModelIndex& index) const; + std::vector indicesToSEditorObjects(const QModelIndexList& indices) const; QModelIndex indexFromObjectID(const std::string& id) const; void setProjectObjectFilterFunction(const ObjectFilterFunc& func); @@ -87,14 +87,20 @@ class ObjectTreeViewDefaultModel : public QAbstractItemModel { virtual Qt::TextElideMode textElideMode() const; - virtual std::vector allowedCreatableUserTypes(const QModelIndexList& selectedIndexes) const; + // Compare function that produces the following order: First sort by hierachy level then by row in scene graph. + static bool isIndexAboveInHierachyOrPosition(QModelIndex left, QModelIndex right); + +public: + std::pair, std::set> getObjectsAndRootIdsFromClipboardString(const std::string& serializedObjs) const; - virtual bool canCopy(const QModelIndex& index) const; - virtual bool canDelete(const QModelIndex& index) const; - virtual bool canInsertMeshAssets(const QModelIndex& index) const; - virtual bool canPasteInto(const QModelIndex& index, const std::string& serializedObjs = RaCoClipboard::get(), bool asExtRef = false) const; + virtual bool canCopyAtIndices(const QModelIndexList& index) const; + virtual bool canDeleteAtIndices(const QModelIndexList& indices) const; + virtual bool canPasteIntoIndex(const QModelIndex& index, const std::vector& objects, const std::set& sourceProjectTopLevelObjectIds, bool asExtRef = false) const; + virtual bool canDeleteUnusedResources() const; - virtual bool objectsAreAllowedInModel(const std::vector& objs, const QModelIndex& parentIndex) const; + + virtual bool isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const; + virtual std::vector typesAllowedIntoIndex(const QModelIndex& index) const; Q_SIGNALS: void repaintRequested(); @@ -102,11 +108,11 @@ class ObjectTreeViewDefaultModel : public QAbstractItemModel { public Q_SLOTS: core::SEditorObject createNewObject(const core::EditorObject::TypeDescriptor &typeDesc, const std::string &nodeName = "", const QModelIndex &parent = QModelIndex()); - virtual size_t deleteObjectAtIndex(const QModelIndex& index); - virtual void copyObjectAtIndex(const QModelIndex& index, bool deepCopy); - virtual void cutObjectAtIndex(const QModelIndex& index, bool deepCut); + virtual size_t deleteObjectsAtIndices(const QModelIndexList& indices); + virtual void copyObjectsAtIndices(const QModelIndexList& indices, bool deepCopy); + virtual void cutObjectsAtIndices(const QModelIndexList& indices, bool deepCut); virtual bool pasteObjectAtIndex(const QModelIndex& index, bool pasteAsExtref = false, std::string* outError = nullptr, const std::string& serializedObjects = RaCoClipboard::get()); - void moveScenegraphChild(core::SEditorObject child, core::SEditorObject parent, int row = -1); + void moveScenegraphChildren(const std::vector& objects, core::SEditorObject parent, int row = -1); void importMeshScenegraph(const QString& filePath, const QModelIndex& selectedIndex); void deleteUnusedResources(); @@ -137,20 +143,21 @@ public Q_SLOTS: using Pixmap = ::raco::style::Pixmap; static inline const std::map typeIconMap{ - {std::string("PerspectiveCamera"), Pixmap::typeCamera}, - {std::string("OrthographicCamera"), Pixmap::typeCamera}, - {std::string("Texture"), Pixmap::typeTexture}, - {std::string("CubeMap"), Pixmap::typeCubemap}, - {std::string("LuaScript"), Pixmap::typeScript}, - {std::string("Material"), Pixmap::typeMaterial}, - {std::string("Mesh"), Pixmap::typeMesh}, - {std::string("MeshNode"), Pixmap::typeMesh}, - {std::string("Node"), Pixmap::typeNode}, - {std::string("Prefab"), Pixmap::typePrefabInternal}, - {std::string("ExtrefPrefab"), Pixmap::typePrefabExternal}, - {std::string("PrefabInstance"), Pixmap::typePrefabInstance}, - {std::string{"AnimationChannel"}, Pixmap::typeAnimationChannel}, - {std::string{"Animation"}, Pixmap::typeAnimation} + {"PerspectiveCamera", Pixmap::typeCamera}, + {"OrthographicCamera", Pixmap::typeCamera}, + {"Texture", Pixmap::typeTexture}, + {"CubeMap", Pixmap::typeCubemap}, + {"LuaScript", Pixmap::typeScript}, + {"Material", Pixmap::typeMaterial}, + {"Mesh", Pixmap::typeMesh}, + {"MeshNode", Pixmap::typeMesh}, + {"Node", Pixmap::typeNode}, + {"Prefab", Pixmap::typePrefabInternal}, + {"ExtrefPrefab", Pixmap::typePrefabExternal}, + {"PrefabInstance", Pixmap::typePrefabInstance}, + {"LuaScriptModule", Pixmap::typeLuaScriptModule}, + {"AnimationChannel", Pixmap::typeAnimationChannel}, + {"Animation", Pixmap::typeAnimation} }; std::string getOriginPathFromMimeData(const QMimeData* data) const; diff --git a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewExternalProjectModel.h b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewExternalProjectModel.h index eecc0ec5..18159473 100644 --- a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewExternalProjectModel.h +++ b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewExternalProjectModel.h @@ -44,20 +44,21 @@ class ObjectTreeViewExternalProjectModel : public ObjectTreeViewDefaultModel { QVariant data(const QModelIndex& index, int role) const override; void addProject(const QString& projectPath); - void removeProject(const QModelIndex& itemIndex); - bool canRemoveProject(const QModelIndex& itemIndex); + void removeProjectsAtIndices(const QModelIndexList& indices); + bool canRemoveProjectsAtIndices(const QModelIndexList& indices); Qt::TextElideMode textElideMode() const override; - bool canCopy(const QModelIndex& index) const override; - bool canDelete(const QModelIndex& index) const override; - bool canInsertMeshAssets(const QModelIndex& index) const override; - bool canPasteInto(const QModelIndex& index, const std::string& deserializedObjs = RaCoClipboard::get(), bool asExtRef = false) const override; + bool canCopyAtIndices(const QModelIndexList& indices) const override; + bool canDeleteAtIndices(const QModelIndexList& indices) const override; + + bool isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const override; + bool canPasteIntoIndex(const QModelIndex& index, const std::vector& objects, const std::set& sourceProjectTopLevelObjectIds, bool asExtRef = false) const override; public Q_SLOTS: - size_t deleteObjectAtIndex(const QModelIndex& index) override; - void copyObjectAtIndex(const QModelIndex& index, bool deepCopy) override; - void cutObjectAtIndex(const QModelIndex& index, bool deepCut) override; + size_t deleteObjectsAtIndices(const QModelIndexList& index) override; + void copyObjectsAtIndices(const QModelIndexList& indices, bool deepCopy) override; + void cutObjectsAtIndices(const QModelIndexList& indices, bool deepCut) override; bool pasteObjectAtIndex(const QModelIndex& index, bool pasteAsExtref = false, std::string* outError = nullptr, const std::string& serializedObjects = RaCoClipboard::get()) override; protected: diff --git a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewPrefabModel.h b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewPrefabModel.h index ff223c4f..7c400968 100644 --- a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewPrefabModel.h +++ b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewPrefabModel.h @@ -19,11 +19,9 @@ class ObjectTreeViewPrefabModel : public ObjectTreeViewDefaultModel { public: ObjectTreeViewPrefabModel(raco::core::CommandInterface* commandInterface, components::SDataChangeDispatcher dispatcher, core::ExternalProjectsStoreInterface* externalProjectsStore, const std::vector& allowedCreatableUserTypes = {}); - bool objectsAreAllowedInModel(const std::vector& objs, const QModelIndex& parentIndex) const override; -protected: - std::vector allowedCreatableUserTypes(const QModelIndexList& selectedIndexes) const override; - bool canInsertMeshAssets(const QModelIndex& index) const override; + bool isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const override; + std::vector typesAllowedIntoIndex(const QModelIndex& index) const override; }; } // namespace raco::object_tree::model \ No newline at end of file diff --git a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewResourceModel.h b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewResourceModel.h index 244836a9..d09a1522 100644 --- a/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewResourceModel.h +++ b/gui/libObjectTree/include/object_tree_view_model/ObjectTreeViewResourceModel.h @@ -22,10 +22,9 @@ class ObjectTreeViewResourceModel : public ObjectTreeViewDefaultModel { ObjectTreeViewResourceModel(raco::core::CommandInterface* commandInterface, components::SDataChangeDispatcher dispatcher, core::ExternalProjectsStoreInterface* externalProjectStore, const std::vector& allowedCreatableUserTypes = {}); bool pasteObjectAtIndex(const QModelIndex& index, bool pasteAsExtref, std::string* outError, const std::string& serializedObjects = RaCoClipboard::get()) override; - bool objectsAreAllowedInModel(const std::vector& objs, const QModelIndex& parentIndex) const override; - bool canInsertMeshAssets(const QModelIndex& index) const override; - bool canPasteInto(const QModelIndex& index, const std::string& serializedObjs, bool asExtRef) const override; + std::vector typesAllowedIntoIndex(const QModelIndex& index) const override; + bool isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const override; }; } // namespace raco::object_tree::model \ No newline at end of file diff --git a/gui/libObjectTree/src/object_tree_view/ObjectTreeView.cpp b/gui/libObjectTree/src/object_tree_view/ObjectTreeView.cpp index 71e49bfb..b043d7ca 100644 --- a/gui/libObjectTree/src/object_tree_view/ObjectTreeView.cpp +++ b/gui/libObjectTree/src/object_tree_view/ObjectTreeView.cpp @@ -15,6 +15,7 @@ #include "core/PathManager.h" #include "core/Project.h" #include "core/UserObjectFactoryInterface.h" +#include "user_types/Node.h" #include "object_tree_view_model/ObjectTreeNode.h" #include "object_tree_view_model/ObjectTreeViewDefaultModel.h" @@ -45,7 +46,7 @@ ObjectTreeView::ObjectTreeView(const QString &viewTitle, ObjectTreeViewDefaultMo setDragDropMode(QAbstractItemView::DragDrop); setDragEnabled(true); setDropIndicatorShown(true); - setSelectionMode(QAbstractItemView::SingleSelection); + setSelectionMode(QAbstractItemView::ExtendedSelection); viewport()->setAcceptDrops(true); if (proxyModel_) { @@ -73,10 +74,8 @@ ObjectTreeView::ObjectTreeView(const QString &viewTitle, ObjectTreeViewDefaultMo return; } - std::set handles; for (const auto &selectedItemIndex : selectedItemList.indexes()) { auto selObj = indexToSEditorObject(selectedItemIndex); - handles.emplace(selObj); selectedItemIDs_.emplace(selObj->objectID()); } @@ -85,7 +84,7 @@ ObjectTreeView::ObjectTreeView(const QString &viewTitle, ObjectTreeViewDefaultMo selectedItemIDs_.erase(selObj->objectID()); } - Q_EMIT newObjectTreeItemsSelected(handles); + Q_EMIT newObjectTreeItemsSelected(getSelectedHandles()); }); connect(treeModel_, &ObjectTreeViewDefaultModel::modelReset, this, &ObjectTreeView::restoreItemExpansionStates); @@ -111,24 +110,19 @@ std::set ObjectTreeView::getSelectedHandles() const { } void ObjectTreeView::globalCopyCallback() { - if (selectionModel()->selectedIndexes().size() > 0) { - auto selectedItemIndex{selectionModel()->selectedIndexes().at(0)}; - if (proxyModel_) { - selectedItemIndex = proxyModel_->mapToSource(selectedItemIndex); - } - if (canCopy(selectedItemIndex)) { - treeModel_->copyObjectAtIndex(selectedItemIndex, false); + auto selectedIndices = getSelectedIndices(true); + if (!selectedIndices.empty()) { + if (canCopyAtIndices(selectedIndices)) { + treeModel_->copyObjectsAtIndices(selectedIndices, false); } } } void ObjectTreeView::shortcutDelete() { - if (selectionModel()->selectedIndexes().size() > 0) { - auto selectedItemIndex{selectionModel()->selectedIndexes().at(0)}; - if (proxyModel_) { - selectedItemIndex = proxyModel_->mapToSource(selectedItemIndex); - } - auto delObjAmount = treeModel_->deleteObjectAtIndex(selectedItemIndex); + auto selectedIndices = getSelectedIndices(); + if (!selectedIndices.empty()) { + auto delObjAmount = treeModel_->deleteObjectsAtIndices(selectedIndices); + if (delObjAmount > 0) { selectionModel()->Q_EMIT selectionChanged({}, {}); } @@ -157,19 +151,15 @@ void ObjectTreeView::expandAllParentsOfObject(const QString &objectID) { } void ObjectTreeView::cut() { - if (selectionModel()->selectedIndexes().size() > 0) { - auto selectedItemIndex{selectionModel()->selectedIndexes().at(0)}; - if (proxyModel_) { - selectedItemIndex = proxyModel_->mapToSource(selectedItemIndex); - } - treeModel_->cutObjectAtIndex(selectedItemIndex, false); + auto selectedIndices = getSelectedIndices(true); + if (!selectedIndices.isEmpty()) { + treeModel_->cutObjectsAtIndices(selectedIndices, false); } } void ObjectTreeView::globalPasteCallback(const QModelIndex &index, bool asExtRef) { - auto selectionIndex = (proxyModel_) ? proxyModel_->mapToSource(index) : index; - if (canPasteInto(selectionIndex)) { - treeModel_->pasteObjectAtIndex(selectionIndex, asExtRef); + if (canPasteIntoIndex(index, asExtRef)) { + treeModel_->pasteObjectAtIndex(index, asExtRef); } } @@ -190,15 +180,18 @@ void ObjectTreeView::showContextMenu(const QPoint &p) { treeViewMenu->exec(viewport()->mapToGlobal(p)); } -bool ObjectTreeView::canCopy(const QModelIndex &parentIndex) { - return treeModel_->canCopy(parentIndex); +bool ObjectTreeView::canCopyAtIndices(const QModelIndexList &parentIndices) { + return treeModel_->canCopyAtIndices(parentIndices); } -bool ObjectTreeView::canPasteInto(const QModelIndex &parentIndex, bool asExtref) { - if (RaCoClipboard::hasEditorObject()) { - return treeModel_->canPasteInto(parentIndex, RaCoClipboard::get(), asExtref); +bool ObjectTreeView::canPasteIntoIndex(const QModelIndex &index, bool asExtref) { + if (!RaCoClipboard::hasEditorObject()) { + return false; + } else { + auto [pasteObjects, sourceProjectTopLevelObjectIds] = treeModel_->getObjectsAndRootIdsFromClipboardString(RaCoClipboard::get()); + return treeModel_->canPasteIntoIndex(index, pasteObjects, sourceProjectTopLevelObjectIds, asExtref); } - return false; + } QSortFilterProxyModel* ObjectTreeView::proxyModel() const { @@ -214,69 +207,92 @@ void ObjectTreeView::resetSelection() { QMenu* ObjectTreeView::createCustomContextMenu(const QPoint &p) { auto treeViewMenu = new QMenu(this); - auto selectedItemIndex = indexAt(p); - if (proxyModel_) { - selectedItemIndex = proxyModel_->mapToSource(selectedItemIndex); - } + auto selectedItemIndices = getSelectedIndices(true); + auto insertionTargetIndex = getSelectedInsertionTargetIndex(); + + bool canDeleteSelectedIndices = treeModel_->canDeleteAtIndices(selectedItemIndices); + bool canCopySelectedIndices = treeModel_->canCopyAtIndices(selectedItemIndices); auto externalProjectModel = (dynamic_cast(treeModel_)); auto prefabModel = (dynamic_cast(treeModel_)); auto resourceModel = (dynamic_cast(treeModel_)); auto allTypes = treeModel_->objectFactory()->getTypes(); - auto allowedCreatableUserTypes = treeModel_->allowedCreatableUserTypes({selectedItemIndex}); + auto allowedCreatableUserTypes = treeModel_->typesAllowedIntoIndex(insertionTargetIndex); + auto canInsertMeshAsset = false; for (auto type : allowedCreatableUserTypes) { if (allTypes.count(type) > 0 && treeModel_->objectFactory()->isUserCreatable(type)) { auto typeDescriptor = allTypes[type]; - treeViewMenu->addAction(QString::fromStdString("Create " + type), [this, typeDescriptor, selectedItemIndex]() { - requestNewNode(typeDescriptor.description, "", selectedItemIndex); + auto actionCreate = treeViewMenu->addAction(QString::fromStdString("Create " + type), [this, typeDescriptor, insertionTargetIndex]() { + requestNewNode(typeDescriptor.description, "", insertionTargetIndex); }); } + if (type == raco::user_types::Node::typeDescription.typeName) { + canInsertMeshAsset = true; + } } - treeViewMenu->addSeparator(); + if (canInsertMeshAsset) { + treeViewMenu->addSeparator(); - auto actionImport = treeViewMenu->addAction("Import glTF Assets...", [this, selectedItemIndex]() { - auto sceneFolder = raco::core::PathManager::getCachedPath(raco::core::PathManager::FolderTypeKeys::Mesh, treeModel_->project()->currentFolder()); - auto file = QFileDialog::getOpenFileName(this, "Load Asset File", QString::fromStdString(sceneFolder), "glTF files (*.gltf *.glb)"); - if (!file.isEmpty()) { - treeModel_->importMeshScenegraph(file, selectedItemIndex); - } - }); - actionImport->setEnabled(treeModel_->canInsertMeshAssets(selectedItemIndex)); + auto actionImport = treeViewMenu->addAction("Import glTF Assets...", [this, insertionTargetIndex]() { + auto sceneFolder = raco::core::PathManager::getCachedPath(raco::core::PathManager::FolderTypeKeys::Mesh, treeModel_->project()->currentFolder()); + auto file = QFileDialog::getOpenFileName(this, "Load Asset File", QString::fromStdString(sceneFolder), "glTF files (*.gltf *.glb)"); + if (!file.isEmpty()) { + treeModel_->importMeshScenegraph(file, insertionTargetIndex); + } + }); + actionImport->setEnabled(canInsertMeshAsset); + } if (!externalProjectModel || !allowedCreatableUserTypes.empty()) { treeViewMenu->addSeparator(); } auto actionDelete = treeViewMenu->addAction( - "Delete", [this, selectedItemIndex]() { - treeModel_->deleteObjectAtIndex(selectedItemIndex); + "Delete", [this, selectedItemIndices]() { + treeModel_->deleteObjectsAtIndices(selectedItemIndices); selectionModel()->Q_EMIT selectionChanged({}, {}); }, QKeySequence::Delete); - actionDelete->setEnabled(treeModel_->canDelete(selectedItemIndex)); + actionDelete->setEnabled(canDeleteSelectedIndices); auto actionCopy = treeViewMenu->addAction( - "Copy", [this, selectedItemIndex]() { treeModel_->copyObjectAtIndex(selectedItemIndex, false); }, QKeySequence::Copy); - actionCopy->setEnabled(canCopy(selectedItemIndex)); - - auto actionPaste = treeViewMenu->addAction( - (resourceModel) ? "Paste on Top Level" : "Paste", [this, selectedItemIndex]() { - treeModel_->pasteObjectAtIndex(selectedItemIndex); }, QKeySequence::Paste); - actionPaste->setEnabled(canPasteInto(selectedItemIndex)); + "Copy", [this, selectedItemIndices]() { + treeModel_->copyObjectsAtIndices(selectedItemIndices, false); + }, QKeySequence::Copy); + actionCopy->setEnabled(canCopySelectedIndices); + + auto [pasteObjects, sourceProjectTopLevelObjectIds] = treeModel_->getObjectsAndRootIdsFromClipboardString(RaCoClipboard::get()); + QAction* actionPaste; + if (treeModel_->canPasteIntoIndex(insertionTargetIndex, pasteObjects, sourceProjectTopLevelObjectIds)) { + actionPaste = treeViewMenu->addAction( + "Paste Here", [this, insertionTargetIndex]() { treeModel_->pasteObjectAtIndex(insertionTargetIndex); }, QKeySequence::Paste); + } else if (treeModel_->canPasteIntoIndex({}, pasteObjects, sourceProjectTopLevelObjectIds)) { + actionPaste = treeViewMenu->addAction( + "Paste Into Project", [this]() { treeModel_->pasteObjectAtIndex(QModelIndex()); }, QKeySequence::Paste); + } else { + actionPaste = treeViewMenu->addAction("Paste", [](){}, QKeySequence::Paste); + actionPaste->setEnabled(false); + } auto actionCut = treeViewMenu->addAction( - "Cut", [this, selectedItemIndex]() { treeModel_->cutObjectAtIndex(selectedItemIndex, false); }, QKeySequence::Cut); - actionCut->setEnabled(treeModel_->canDelete(selectedItemIndex)); + "Cut", [this, selectedItemIndices]() { + treeModel_->cutObjectsAtIndices(selectedItemIndices, false); + }, QKeySequence::Cut); + actionCut->setEnabled(canDeleteSelectedIndices && canCopySelectedIndices); treeViewMenu->addSeparator(); - auto actionCopyDeep = treeViewMenu->addAction("Copy (Deep)", [this, selectedItemIndex]() { treeModel_->copyObjectAtIndex(selectedItemIndex, true); }); - actionCopyDeep->setEnabled(canCopy(selectedItemIndex)); + auto actionCopyDeep = treeViewMenu->addAction("Copy (Deep)", [this, selectedItemIndices]() { + treeModel_->copyObjectsAtIndices(selectedItemIndices, true); + }); + actionCopyDeep->setEnabled(canCopySelectedIndices); - auto actionCutDeep = treeViewMenu->addAction("Cut (Deep)", [this, selectedItemIndex]() { treeModel_->cutObjectAtIndex(selectedItemIndex, true); }); - actionCutDeep->setEnabled(treeModel_->canDelete(selectedItemIndex)); + auto actionCutDeep = treeViewMenu->addAction("Cut (Deep)", [this, selectedItemIndices]() { + treeModel_->cutObjectsAtIndices(selectedItemIndices, true); + }); + actionCutDeep->setEnabled(canDeleteSelectedIndices && canCopySelectedIndices); if (!externalProjectModel) { auto actionDeleteUnrefResources = treeViewMenu->addAction("Delete Unused Resources", [this] { treeModel_->deleteUnusedResources(); }); @@ -284,14 +300,16 @@ QMenu* ObjectTreeView::createCustomContextMenu(const QPoint &p) { treeViewMenu->addSeparator(); auto extrefPasteAction = treeViewMenu->addAction( - "Paste As External Reference", [this, selectedItemIndex]() { + "Paste As External Reference", [this]() { std::string error; - if (!treeModel_->pasteObjectAtIndex(selectedItemIndex, true, &error)) { + if (!treeModel_->pasteObjectAtIndex({}, true, &error)) { QMessageBox::warning(this, "Paste As External Reference", fmt::format("Update of pasted external references failed!\n\n{}", error).c_str()); } }); - extrefPasteAction->setEnabled(canPasteInto(selectedItemIndex, true)); + + // Pasting extrefs will ignore the current selection and always paste on top level. + extrefPasteAction->setEnabled(treeModel_->canPasteIntoIndex({}, pasteObjects, sourceProjectTopLevelObjectIds, true)); } if (externalProjectModel) { @@ -310,8 +328,8 @@ QMenu* ObjectTreeView::createCustomContextMenu(const QPoint &p) { externalProjectModel->addProject(projectFile); }); - auto actionCloseImportedProject = treeViewMenu->addAction("Remove Project", [this, selectedItemIndex, externalProjectModel]() { externalProjectModel->removeProject(selectedItemIndex); }); - actionCloseImportedProject->setEnabled(selectedItemIndex.isValid() && externalProjectModel->canRemoveProject(selectedItemIndex)); + auto actionCloseImportedProject = treeViewMenu->addAction("Remove Project", [this, selectedItemIndices, externalProjectModel]() { externalProjectModel->removeProjectsAtIndices(selectedItemIndices); }); + actionCloseImportedProject->setEnabled(externalProjectModel->canRemoveProjectsAtIndices(selectedItemIndices)); } return treeViewMenu; @@ -356,6 +374,49 @@ QModelIndex ObjectTreeView::indexFromObjectID(const std::string &id) const { return index; } + +QModelIndexList ObjectTreeView::getSelectedIndices(bool sorted) const { + auto selectedIndices = selectionModel()->selectedRows(); + + if (proxyModel_) { + for (auto& index : selectedIndices) { + index = proxyModel_->mapToSource(index); + } + } + + // For operation like copy, cut and move, the order of selection matters + if (sorted) { + auto sortedList = selectedIndices.toVector(); + std::sort(sortedList.begin(), sortedList.end(), ObjectTreeViewDefaultModel::isIndexAboveInHierachyOrPosition); + return QModelIndexList(sortedList.begin(), sortedList.end()); + } + + return selectedIndices; +} + +QModelIndex ObjectTreeView::getSelectedInsertionTargetIndex() const { + auto selectedIndices = getSelectedIndices(); + + // For single selection, just insert unter the selection. + if (selectedIndices.count() == 0) { + return {}; + } else if (selectedIndices.count() == 1) { + return selectedIndices.front(); + } + + // For multiselection, look for the index on the highest hierachy level which is topmost in its hierachy level. + // Partially sort selectedIndices so that the first index of the list is the highest level topmost index of the tree. + std::nth_element(selectedIndices.begin(), selectedIndices.begin(), selectedIndices.end(), ObjectTreeViewDefaultModel::isIndexAboveInHierachyOrPosition); + + // Now take this highest level topmost index from the list and insert into its parent, if it exists. + auto canidate = selectedIndices.front(); + if (canidate.isValid()) { + return canidate.parent(); + } else { + return canidate; + } +} + void ObjectTreeView::restoreItemExpansionStates() { for (const auto &expandedObjectID : expandedItemIDs_) { auto expandedObjectIndex = indexFromObjectID(expandedObjectID); diff --git a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewDefaultModel.cpp b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewDefaultModel.cpp index 6be87d66..f5bb6ba7 100644 --- a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewDefaultModel.cpp +++ b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewDefaultModel.cpp @@ -144,7 +144,7 @@ QModelIndex ObjectTreeViewDefaultModel::index(int row, int column, const QModelI auto* parentNode = indexToTreeNode(parent); if (!parentNode) { - return getInvisibleRootIndex(); + return {}; } else { return createIndex(row, column, parentNode->getChild(row)); } @@ -173,23 +173,23 @@ bool ObjectTreeViewDefaultModel::canDropMimeData(const QMimeData* data, Qt::Drop } std::vector objectsFromId; + std::set sourceProjectTopLevelObjectIds; if (idList.empty()) { return false; } for (const auto& id : idList) { auto objectFromId = Queries::findById((droppingFromOtherProject) ? *externalProjectStore_->getExternalProjectCommandInterface(originPath)->project() : *project(), id.toStdString()); - auto tryingToDropNonExtRefAsExtRef = droppingFromOtherProject && droppingAsExternalReference && !raco::core::Queries::canPasteObjectAsExternalReference(objectFromId, objectFromId->getParent() == nullptr); - if (tryingToDropNonExtRefAsExtRef) { - return false; + if (objectFromId->getParent() == nullptr) { + sourceProjectTopLevelObjectIds.emplace(id.toStdString()); } objectsFromId.emplace_back(objectFromId); } - return objectsAreAllowedInModel(objectsFromId, parent); + return canPasteIntoIndex(parent, objectsFromId, sourceProjectTopLevelObjectIds, droppingAsExternalReference); } QModelIndex ObjectTreeViewDefaultModel::parent(const QModelIndex& child) const { if (!child.isValid()) { - return QModelIndex(); + return {}; } auto* childNode = indexToTreeNode(child); @@ -197,12 +197,12 @@ QModelIndex ObjectTreeViewDefaultModel::parent(const QModelIndex& child) const { if (parentNode) { if (parentNode == invisibleRootNode_.get()) { - return getInvisibleRootIndex(); + return {}; } return createIndex(parentNode->row(), COLUMNINDEX_NAME, parentNode); } - return QModelIndex(); + return {}; } Qt::DropActions ObjectTreeViewDefaultModel::supportedDropActions() const { @@ -272,17 +272,19 @@ bool ObjectTreeViewDefaultModel::dropMimeData(const QMimeData* data, Qt::DropAct auto mimeDataContainsLocalInstances = originPath == project()->currentPath(); auto movedItemIDs = decodeMimeData(data); - if (mimeDataContainsLocalInstances) { SEditorObject parentObj; if (parent.isValid()) { parentObj = indexToSEditorObject(parent); } - for (const auto& itemID : movedItemIDs) { - if (auto childObj = project()->getInstanceByID(itemID.toStdString())) { - moveScenegraphChild(childObj, parentObj, row); + std::vector objs; + for (const auto& movedItemID : movedItemIDs) { + if (auto childObj = project()->getInstanceByID(movedItemID.toStdString())) { + objs.emplace_back(childObj); } } + + moveScenegraphChildren(objs, parentObj, row); } else { auto originCommandInterface = externalProjectStore_->getExternalProjectCommandInterface(originPath); std::vector objs; @@ -310,8 +312,13 @@ Qt::ItemFlags ObjectTreeViewDefaultModel::flags(const QModelIndex& index) const } } -QMimeData* ObjectTreeViewDefaultModel::mimeData(const QModelIndexList& indexes) const { - return generateMimeData(indexes, project()->currentPath()); +QMimeData* ObjectTreeViewDefaultModel::mimeData(const QModelIndexList& indices) const { + + // Sort the list to make sure order after dropping remains consistent, regardless of what selectiong order is. + auto sortedList = indices.toVector(); + std::sort(sortedList.begin(), sortedList.end(), ObjectTreeViewDefaultModel::isIndexAboveInHierachyOrPosition); + + return generateMimeData(QModelIndexList(sortedList.begin(), sortedList.end()), project()->currentPath()); } QStringList ObjectTreeViewDefaultModel::mimeTypes() const { @@ -377,7 +384,7 @@ void ObjectTreeViewDefaultModel::setUpTreeModificationFunctions() { SEditorObject ObjectTreeViewDefaultModel::createNewObject(const EditorObject::TypeDescriptor& typeDesc, const std::string& nodeName, const QModelIndex& parent) { std::vector nodes; - if (parent == getInvisibleRootIndex()) { + if (!parent.isValid()) { std::copy_if(project()->instances().begin(), project()->instances().end(), std::back_inserter(nodes), [](const SEditorObject& obj) { return obj->getParent() == nullptr; }); } else { std::copy_if(project()->instances().begin(), project()->instances().end(), std::back_inserter(nodes), [this, parent](const SEditorObject& obj) { @@ -395,50 +402,67 @@ SEditorObject ObjectTreeViewDefaultModel::createNewObject(const EditorObject::Ty return newObj; } -bool ObjectTreeViewDefaultModel::canCopy(const QModelIndex& index) const { - return index.isValid(); -} - -bool ObjectTreeViewDefaultModel::canDelete(const QModelIndex& index) const { - if (index.isValid()) { - auto obj = indexToSEditorObject(index); - return obj && core::Queries::canDeleteObjects(*commandInterface_->project(), {obj}); +bool ObjectTreeViewDefaultModel::canCopyAtIndices(const QModelIndexList& indices) const { + for (const auto& index : indices) { + if (index.isValid()) { + return true; + } } return false; } -bool ObjectTreeViewDefaultModel::canInsertMeshAssets(const QModelIndex& index) const { - if (index.isValid()) { - const auto sceneRootNode = indexToSEditorObject(index); - return Queries::canPasteIntoObject(*project(), sceneRootNode); - } +bool ObjectTreeViewDefaultModel::canDeleteAtIndices(const QModelIndexList& indices) const { + return !Queries::filterForDeleteableObjects(*commandInterface_->project(), indicesToSEditorObjects(indices)).empty(); +} - return false; +bool ObjectTreeViewDefaultModel::isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const { + if (index.isValid() && !core::Queries::canPasteIntoObject(*commandInterface_->project(), indexToSEditorObject(index))) { + return false; + } else { + auto types = typesAllowedIntoIndex(index); + + return std::find(types.begin(), types.end(), obj->getTypeDescription().typeName) != types.end(); + } } -bool ObjectTreeViewDefaultModel::canPasteInto(const QModelIndex& index, const std::string& serializedObjs, bool asExtRef) const { +std::pair, std::set> ObjectTreeViewDefaultModel::getObjectsAndRootIdsFromClipboardString(const std::string& serializedObjs) const { auto deserialization{raco::serialization::deserializeObjects(serializedObjs, raco::user_types::UserObjectFactoryInterface::deserializationFactory(commandInterface_->objectFactory()))}; - auto topLevelObjects = BaseContext::getTopLevelObjectsFromDeserializedObjects(deserialization, commandInterface_->objectFactory(), project()); + auto objects = BaseContext::getTopLevelObjectsFromDeserializedObjects(deserialization, commandInterface_->objectFactory(), project()); + return {objects, deserialization.rootObjectIDs}; +} + +bool ObjectTreeViewDefaultModel::canPasteIntoIndex(const QModelIndex& index, const std::vector& objects, const std::set& sourceProjectTopLevelObjectIds, bool asExtRef) const { if (asExtRef) { if (index.isValid()) { return false; } - for (const auto& topLevelObj : topLevelObjects) { - if (!Queries::canPasteObjectAsExternalReference(topLevelObj, deserialization.rootObjectIDs.find(topLevelObj->objectID()) != deserialization.rootObjectIDs.end())) { - return false; + + // Allow pasting objects if any objects fits the location. + for (const auto& obj : objects) { + if (Queries::canPasteObjectAsExternalReference(obj, sourceProjectTopLevelObjectIds.find(obj->objectID()) != sourceProjectTopLevelObjectIds.end()) && + isObjectAllowedIntoIndex(index, obj)) { + return true; + } + } + } + else { + // Allow pasting objects if any objects fits the location. + for (const auto& obj : objects) { + if (isObjectAllowedIntoIndex(index, obj)) { + return true; } } } - return objectsAreAllowedInModel(topLevelObjects, index); + return false; } -size_t ObjectTreeViewDefaultModel::deleteObjectAtIndex(const QModelIndex& index) { - auto obj = indexToSEditorObject(index); - return commandInterface_->deleteObjects({obj}); +size_t ObjectTreeViewDefaultModel::deleteObjectsAtIndices(const QModelIndexList& indices) { + + return commandInterface_->deleteObjects(indicesToSEditorObjects(indices)); } bool ObjectTreeViewDefaultModel::canDeleteUnusedResources() const { @@ -449,8 +473,9 @@ void ObjectTreeViewDefaultModel::deleteUnusedResources() { commandInterface_->deleteUnreferencedResources(); } -void ObjectTreeViewDefaultModel::copyObjectAtIndex(const QModelIndex& index, bool deepCopy) { - RaCoClipboard::set(commandInterface_->copyObjects({indexToSEditorObject(index)}, deepCopy)); +void ObjectTreeViewDefaultModel::copyObjectsAtIndices(const QModelIndexList& indices, bool deepCopy) { + auto objects = indicesToSEditorObjects(indices); + RaCoClipboard::set(commandInterface_->copyObjects(objects, deepCopy)); } bool ObjectTreeViewDefaultModel::pasteObjectAtIndex(const QModelIndex& index, bool pasteAsExtref, std::string* outError, const std::string& serializedObjects) { @@ -459,16 +484,16 @@ bool ObjectTreeViewDefaultModel::pasteObjectAtIndex(const QModelIndex& index, bo return success; } -void ObjectTreeViewDefaultModel::cutObjectAtIndex(const QModelIndex& index, bool deepCut) { - auto obj = indexToSEditorObject(index); - auto text = commandInterface_->cutObjects({obj}, deepCut); +void ObjectTreeViewDefaultModel::cutObjectsAtIndices(const QModelIndexList& indices, bool deepCut) { + auto objects = indicesToSEditorObjects(indices); + auto text = commandInterface_->cutObjects(objects, deepCut); if (!text.empty()) { RaCoClipboard::set(text); } } -void ObjectTreeViewDefaultModel::moveScenegraphChild(SEditorObject child, SEditorObject parent, int row) { - commandInterface_->moveScenegraphChild(child, parent, row); +void ObjectTreeViewDefaultModel::moveScenegraphChildren(const std::vector& objects, SEditorObject parent, int row) { + commandInterface_->moveScenegraphChildren(objects, parent, row); } void ObjectTreeViewDefaultModel::importMeshScenegraph(const QString& filePath, const QModelIndex& selectedIndex) { @@ -496,10 +521,6 @@ int ObjectTreeViewDefaultModel::rowCount(const QModelIndex& parent) const { return 0; } -QModelIndex ObjectTreeViewDefaultModel::getInvisibleRootIndex() const { - return invisibleRootIndex_; -} - void ObjectTreeViewDefaultModel::iterateThroughTree(std::function nodeFunc, QModelIndex& currentIndex) { if (currentIndex.row() != -1) { nodeFunc(currentIndex); @@ -523,6 +544,19 @@ SEditorObject ObjectTreeViewDefaultModel::indexToSEditorObject(const QModelIndex return indexToTreeNode(index)->getRepresentedObject(); } +std::vector ObjectTreeViewDefaultModel::indicesToSEditorObjects(const QModelIndexList& indices) const { + std::vector objects; + for (const auto& index : indices) { + if (index.isValid()) { + auto obj = indexToSEditorObject(index); + if (obj) { + objects.push_back(indexToSEditorObject(index)); + } + } + } + return objects; +} + QModelIndex ObjectTreeViewDefaultModel::indexFromObjectID(const std::string& id) const { auto cachedID = indexes_.find(id); if (cachedID != indexes_.end()) { @@ -552,35 +586,6 @@ void raco::object_tree::model::ObjectTreeViewDefaultModel::updateTreeIndexes() { invisibleRootIndex_); } -bool ObjectTreeViewDefaultModel::objectsAreAllowedInModel(const std::vector& objs, const QModelIndex& parentIndex) const { - if (!core::Queries::canPasteIntoObject(*commandInterface_->project(), indexToSEditorObject(parentIndex))) { - return false; - } - - auto objsContainSceneGraphObjs = false; - auto objsContainPrefabs = false; - auto objsContainPrefabInstances = false; - - for (const auto& obj : objs) { - if (obj->as()) { - objsContainPrefabs = true; - } else if (obj->as()) { - objsContainPrefabInstances = true; - } - - if (!obj->getTypeDescription().isResource) { - objsContainSceneGraphObjs = true; - } - } - - if (objsContainPrefabs && !objsContainPrefabInstances) { - return false; - } - - return objsContainSceneGraphObjs; -} - - UserObjectFactoryInterface* ObjectTreeViewDefaultModel::objectFactory() { return commandInterface_->objectFactory(); } @@ -589,12 +594,31 @@ Project* ObjectTreeViewDefaultModel::project() const { return commandInterface_->project(); } -Qt::TextElideMode raco::object_tree::model::ObjectTreeViewDefaultModel::textElideMode() const { +Qt::TextElideMode ObjectTreeViewDefaultModel::textElideMode() const { return Qt::TextElideMode::ElideRight; } -std::vector raco::object_tree::model::ObjectTreeViewDefaultModel::allowedCreatableUserTypes(const QModelIndexList& selectedIndexes) const { - return allowedUserCreatableUserTypes_; +bool ObjectTreeViewDefaultModel::isIndexAboveInHierachyOrPosition(QModelIndex left, QModelIndex right) { + while (left.parent() != right.parent()) { + left = left.parent(); + right = right.parent(); + + if (!left.isValid()) { + return true; + } else if (!right.isValid()) { + return false; + } + } + + return left.row() < right.row(); +} + +std::vector raco::object_tree::model::ObjectTreeViewDefaultModel::typesAllowedIntoIndex(const QModelIndex& index) const { + if (index.isValid() && !core::Queries::canPasteIntoObject(*commandInterface_->project(), indexToSEditorObject(index))) { + return {}; + } else { + return allowedUserCreatableUserTypes_; + } } } // namespace raco::object_tree::model \ No newline at end of file diff --git a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewExternalProjectModel.cpp b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewExternalProjectModel.cpp index 7aa90e64..056a483e 100644 --- a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewExternalProjectModel.cpp +++ b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewExternalProjectModel.cpp @@ -77,20 +77,59 @@ void raco::object_tree::model::ObjectTreeViewExternalProjectModel::addProject(co } } -void raco::object_tree::model::ObjectTreeViewExternalProjectModel::removeProject(const QModelIndex& itemIndex) { - auto projectPath = getOriginProjectPathOfSelectedIndex(itemIndex); - externalProjectStore_->removeExternalProject(projectPath); +void raco::object_tree::model::ObjectTreeViewExternalProjectModel::removeProjectsAtIndices(const QModelIndexList& indices) { + std::set projectsToRemove; + for (const auto& index : indices) { + projectsToRemove.emplace(getOriginProjectPathOfSelectedIndex(index)); + } + + for (const auto& projectPath : projectsToRemove) { + externalProjectStore_->removeExternalProject(projectPath); + } } -bool raco::object_tree::model::ObjectTreeViewExternalProjectModel::canRemoveProject(const QModelIndex& itemIndex) { - auto projectPath = getOriginProjectPathOfSelectedIndex(itemIndex); - return externalProjectStore_->canRemoveExternalProject(projectPath); +bool raco::object_tree::model::ObjectTreeViewExternalProjectModel::canRemoveProjectsAtIndices(const QModelIndexList& indices) { + if (indices.isEmpty()) { + return false; + } + + for (const auto& index : indices) { + if (!index.isValid()) { + return false; + } + + auto projectPath = getOriginProjectPathOfSelectedIndex(index); + if (!externalProjectStore_->canRemoveExternalProject(projectPath)) { + return false; + } + } + return true; } -void ObjectTreeViewExternalProjectModel::copyObjectAtIndex(const QModelIndex& index, bool deepCopy) { - auto originPath = getOriginProjectPathOfSelectedIndex(index); - auto* commandInterface = externalProjectStore_->getExternalProjectCommandInterface(originPath); - RaCoClipboard::set(commandInterface->copyObjects({indexToSEditorObject(index)}, deepCopy)); +void ObjectTreeViewExternalProjectModel::copyObjectsAtIndices(const QModelIndexList& indices, bool deepCopy) { + + raco::core::CommandInterface* commandInterface = nullptr; + + std::vector objects; + for (const auto& index : indices) { + + auto object = indexToSEditorObject(index); + if (&object->getTypeDescription() != &ProjectNode::typeDescription) { + + objects.push_back(indexToSEditorObject(index)); + + // canCopyAtIndices already enforces that we only copy objects from the same project, so we can just take the project path from the first index + if (!commandInterface) { + const auto& index = indices.front(); + auto originPath = getOriginProjectPathOfSelectedIndex(index); + commandInterface = externalProjectStore_->getExternalProjectCommandInterface(originPath); + } + } + } + + if (commandInterface) { + RaCoClipboard::set(commandInterface->copyObjects(objects, deepCopy)); + } } void raco::object_tree::model::ObjectTreeViewExternalProjectModel::buildObjectTree() { @@ -139,24 +178,46 @@ Qt::TextElideMode ObjectTreeViewExternalProjectModel::textElideMode() const { return Qt::TextElideMode::ElideLeft; } -bool ObjectTreeViewExternalProjectModel::canCopy(const QModelIndex& index) const { - return index.isValid() && indexToSEditorObject(index)->as() == nullptr; +bool ObjectTreeViewExternalProjectModel::canCopyAtIndices(const QModelIndexList& indices) const { + + bool atLeastOneCopyableItem = false; + std::string originPath; + for (const auto& index : indices) { + if (index.isValid() && indexToSEditorObject(index)->as() == nullptr) { + atLeastOneCopyableItem = true; + + // Make sure all items belong to the same external project + auto indexOriginPath = getOriginProjectPathOfSelectedIndex(index); + if (originPath == "") { + originPath = indexOriginPath; + } else if (indexOriginPath != originPath) { + return false; + } + } + + } + + return atLeastOneCopyableItem; +} + +bool ObjectTreeViewExternalProjectModel::canDeleteAtIndices(const QModelIndexList& indices) const { + return false; } -bool ObjectTreeViewExternalProjectModel::canDelete(const QModelIndex& index) const { +bool ObjectTreeViewExternalProjectModel::canPasteIntoIndex(const QModelIndex& index, const std::vector& objects, const std::set& sourceProjectTopLevelObjectIds, bool asExtRef) const { return false; } -bool ObjectTreeViewExternalProjectModel::canPasteInto(const QModelIndex& index, const std::string& serializedObjs, bool asExtRef) const { +bool ObjectTreeViewExternalProjectModel::isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const { return false; } -size_t ObjectTreeViewExternalProjectModel::deleteObjectAtIndex(const QModelIndex& index) { +size_t ObjectTreeViewExternalProjectModel::deleteObjectsAtIndices(const QModelIndexList& index) { // Don't modify external project structure. return 0; } -void ObjectTreeViewExternalProjectModel::cutObjectAtIndex(const QModelIndex& index, bool deepCut) { +void ObjectTreeViewExternalProjectModel::cutObjectsAtIndices(const QModelIndexList& indices, bool deepCut) { // Don't modify external project structure. } @@ -165,8 +226,4 @@ bool ObjectTreeViewExternalProjectModel::pasteObjectAtIndex(const QModelIndex& i return true; } -bool ObjectTreeViewExternalProjectModel::canInsertMeshAssets(const QModelIndex& index) const { - return false; -} - } // namespace raco::object_tree::model \ No newline at end of file diff --git a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewPrefabModel.cpp b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewPrefabModel.cpp index 567900ff..8c35e3e5 100644 --- a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewPrefabModel.cpp +++ b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewPrefabModel.cpp @@ -21,51 +21,42 @@ ObjectTreeViewPrefabModel::ObjectTreeViewPrefabModel(raco::core::CommandInterfac : ObjectTreeViewDefaultModel(commandInterface, dispatcher, externalProjectsStore, allowedCreatableUserTypes) { } -std::vector ObjectTreeViewPrefabModel::allowedCreatableUserTypes(const QModelIndexList& selectedIndexes) const { - if (!selectedIndexes.isEmpty()) { - auto selectedIndex = selectedIndexes.front(); - if (selectedIndex.isValid()) { - return allowedUserCreatableUserTypes_; +std::vector ObjectTreeViewPrefabModel::typesAllowedIntoIndex(const QModelIndex& index) const { + + auto prefabType = raco::user_types::Prefab::typeDescription.typeName; + if (index.isValid()) { + auto types = ObjectTreeViewDefaultModel::typesAllowedIntoIndex(index); + + auto prefabIndex = std::find(types.begin(), types.end(), prefabType); + if (prefabIndex != types.end()) { + types.erase(std::find(types.begin(), types.end(), prefabType)); } - } - return {raco::user_types::Prefab::typeDescription.typeName}; + return types; + } else { + return {prefabType}; + } } -bool ObjectTreeViewPrefabModel::canInsertMeshAssets(const QModelIndex& index) const { +bool ObjectTreeViewPrefabModel::isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const { if (index.isValid()) { - return ObjectTreeViewDefaultModel::canInsertMeshAssets(index); - } - return false; -} - -bool ObjectTreeViewPrefabModel::objectsAreAllowedInModel(const std::vector& objs, const QModelIndex& parentIndex) const { - if (parentIndex.isValid()) { - if (auto parentObj = indexToSEditorObject(parentIndex)) { + if (auto parentObj = indexToSEditorObject(index)) { if (parentObj->query() || raco::core::PrefabOperations::findContainingPrefabInstance(parentObj)) { return false; } } - return ObjectTreeViewDefaultModel::objectsAreAllowedInModel(objs, parentIndex); - } else { - // pasting in blank space: Clipboard should contain one Prefab - // Any other top-level objects would then be part of a Prefab deep-copy - // If there is a PrefabInstance top-level object: PrefabInstance deep-copy - auto objsContainPrefab = false; - for (const auto& obj : objs) { - if (obj->as()) { - objsContainPrefab = true; - } else if (obj->as()) { - return false; - } + return ObjectTreeViewDefaultModel::isObjectAllowedIntoIndex(index, obj); + } else { + if (obj->as()) { + return true; + } else { + return false; } - return objsContainPrefab; - } - return true; } + } // namespace raco::object_tree::model \ No newline at end of file diff --git a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewResourceModel.cpp b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewResourceModel.cpp index 9a306bf7..0b43a7e6 100644 --- a/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewResourceModel.cpp +++ b/gui/libObjectTree/src/object_tree_view_model/ObjectTreeViewResourceModel.cpp @@ -22,36 +22,20 @@ ObjectTreeViewResourceModel::ObjectTreeViewResourceModel(raco::core::CommandInte bool ObjectTreeViewResourceModel::pasteObjectAtIndex(const QModelIndex& index, bool pasteAsExtref, std::string* outError, const std::string& serializedObjects) { // ignore index: resources always get pasted at top level. - return ObjectTreeViewDefaultModel::pasteObjectAtIndex(getInvisibleRootIndex(), pasteAsExtref, outError, serializedObjects); + return ObjectTreeViewDefaultModel::pasteObjectAtIndex({}, pasteAsExtref, outError, serializedObjects); } -bool ObjectTreeViewResourceModel::objectsAreAllowedInModel(const std::vector& objs, const QModelIndex& parentIndex) const { - for (const auto& obj : objs) { - if (!obj || std::find(allowedUserCreatableUserTypes_.begin(), allowedUserCreatableUserTypes_.end(), obj->getTypeDescription().typeName) == allowedUserCreatableUserTypes_.end()) { - return false; - } - } - return true; +bool ObjectTreeViewResourceModel::isObjectAllowedIntoIndex(const QModelIndex& index, const core::SEditorObject& obj) const { + // Only allow root level pasting here, thus only invalid indices are ok. + return !index.isValid() && ObjectTreeViewDefaultModel::isObjectAllowedIntoIndex(index, obj); } -bool ObjectTreeViewResourceModel::canInsertMeshAssets(const QModelIndex& index) const { - return false; +std::vector ObjectTreeViewResourceModel::typesAllowedIntoIndex(const QModelIndex& index) const { + auto topLevel = QModelIndex(); + // Always assume user wants to create item on top level. + return ObjectTreeViewDefaultModel::typesAllowedIntoIndex(topLevel); } -bool ObjectTreeViewResourceModel::canPasteInto(const QModelIndex& index, const std::string& serializedObjs, bool asExtRef) const { - auto deserialization{raco::serialization::deserializeObjects(serializedObjs, - raco::user_types::UserObjectFactoryInterface::deserializationFactory(commandInterface_->objectFactory()))}; - auto topLevelObjects = core::BaseContext::getTopLevelObjectsFromDeserializedObjects(deserialization, commandInterface_->objectFactory(), project()); - if (asExtRef) { - for (const auto& topLevelObj : topLevelObjects) { - if (!core::Queries::canPasteObjectAsExternalReference(topLevelObj, deserialization.rootObjectIDs.find(topLevelObj->objectID()) != deserialization.rootObjectIDs.end())) { - return false; - } - } - } - - return objectsAreAllowedInModel(topLevelObjects, index); -} } // namespace raco::object_tree::model \ No newline at end of file diff --git a/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.cpp b/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.cpp index bc060467..2ec2e5ab 100644 --- a/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.cpp +++ b/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.cpp @@ -25,11 +25,22 @@ #include +#include using namespace raco::core; using namespace raco::object_tree::model; using namespace raco::user_types; +ObjectTreeViewDefaultModelTest::ObjectTreeViewDefaultModelTest() : viewModel_{new raco::object_tree::model::ObjectTreeViewDefaultModel(&commandInterface, dataChangeDispatcher_, nullptr, + {raco::user_types::Animation::typeDescription.typeName, + raco::user_types::Node::typeDescription.typeName, + raco::user_types::MeshNode::typeDescription.typeName, + raco::user_types::PrefabInstance::typeDescription.typeName, + raco::user_types::OrthographicCamera::typeDescription.typeName, + raco::user_types::PerspectiveCamera::typeDescription.typeName, + raco::user_types::LuaScript::typeDescription.typeName})} {} + + void ObjectTreeViewDefaultModelTest::compareValuesInTree(const SEditorObject &obj, const QModelIndex &objIndex, const ObjectTreeViewDefaultModel &viewModel_) { auto *objTreeNode = viewModel_.indexToTreeNode(objIndex); std::string treeValue; @@ -92,7 +103,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, TreeBuildingSimple) { auto singleNode = createNodes(MeshNode::typeDescription.typeName, nodeNames_).front(); auto singleNodeIndex = viewModel_->index(0, 0); - ASSERT_EQ(viewModel_->parent(singleNodeIndex), viewModel_->getInvisibleRootIndex()); + ASSERT_FALSE(viewModel_->parent(singleNodeIndex).isValid()); compareValuesInTree(singleNode, singleNodeIndex, *viewModel_); } @@ -105,7 +116,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, TreeBuildingThreeRootNodes) { for (size_t i = 0; i < nodeNames_.size(); ++i) { auto currentNodeModel = viewModel_->index(i, 0); - ASSERT_EQ(viewModel_->parent(currentNodeModel).row(), viewModel_->getInvisibleRootIndex().row()); + ASSERT_FALSE(viewModel_->parent(currentNodeModel).isValid()); compareValuesInTree(createdNodes[i], currentNodeModel, *viewModel_); } } @@ -120,7 +131,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, TreeBuildingOneParentOneChild) { auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); auto rootNode = createdNodes.front(); auto childNode = createdNodes.back(); - moveScenegraphChild(childNode, rootNode); + moveScenegraphChildren({childNode}, rootNode); auto rootNodeModelIndex = viewModel_->index(0, ObjectTreeViewDefaultModel::COLUMNINDEX_NAME); auto childNodeModelIndex = viewModel_->index(0, ObjectTreeViewDefaultModel::COLUMNINDEX_NAME, rootNodeModelIndex); @@ -128,7 +139,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, TreeBuildingOneParentOneChild) { auto compareParentRelationshipPerColumn = [this, &rootNodeModelIndex, &childNodeModelIndex](auto column) { auto parent = viewModel_->indexToTreeNode(rootNodeModelIndex); auto child = viewModel_->indexToTreeNode(childNodeModelIndex); - auto invisibleRootNode = viewModel_->indexToTreeNode(viewModel_->getInvisibleRootIndex()); + auto invisibleRootNode = viewModel_->indexToTreeNode({}); ASSERT_EQ(parent->getParent()->getRepresentedObject(), invisibleRootNode->getRepresentedObject()); ASSERT_EQ(parent->getChildren().size(), 1); @@ -156,17 +167,16 @@ TEST_F(ObjectTreeViewDefaultModelTest, TreeBuildingOneRootHasTwoChildrenOtherRoo nodeNames_ = {"childNode1"}; auto childNode1 = createNodes(MeshNode::typeDescription.typeName, nodeNames_).front(); - moveScenegraphChild(childNode1, rootNode1); + moveScenegraphChildren({childNode1}, rootNode1); nodeNames_ = {"childNode2"}; auto childNode2 = createNodes(MeshNode::typeDescription.typeName, nodeNames_).front(); - moveScenegraphChild(childNode2, rootNode1); + moveScenegraphChildren({childNode2}, rootNode1); nodeNames_ = {"rootNode2"}; auto rootNode2 = createNodes(Node::typeDescription.typeName, nodeNames_).front(); - auto invisibleRoot = viewModel_->getInvisibleRootIndex(); - auto invisibleRootNode = viewModel_->indexToTreeNode(viewModel_->getInvisibleRootIndex()); + auto invisibleRootNode = viewModel_->indexToTreeNode({}); ASSERT_EQ(invisibleRootNode->childCount(), 2); auto firstRootLeaf = invisibleRootNode->getChild(0); @@ -207,10 +217,10 @@ TEST_F(ObjectTreeViewDefaultModelTest, TreeBuildingNastyNesting) { auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); for (size_t i = 0; i < createdNodes.size() - 1; ++i) { - moveScenegraphChild(createdNodes[i + 1], createdNodes[i]); + moveScenegraphChildren({createdNodes[i + 1]}, createdNodes[i]); } - QModelIndex parent = viewModel_->getInvisibleRootIndex(); + QModelIndex parent; for (int i = 0; i < NODE_AMOUNT - 1; ++i) { auto currentIndex = viewModel_->index(0, ObjectTreeViewDefaultModel::COLUMNINDEX_NAME, parent); auto actualParent = viewModel_->parent(currentIndex); @@ -231,9 +241,9 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveCreateTwoNodes) { auto childIndex = viewModel_->indexFromObjectID(createdNodes.back()->objectID()); ASSERT_EQ(rootIndex.row(), 0); - ASSERT_EQ(rootIndex.parent(), viewModel_->getInvisibleRootIndex()); + ASSERT_FALSE(rootIndex.parent().isValid()); ASSERT_EQ(childIndex.row(), 1); - ASSERT_EQ(childIndex.parent(), viewModel_->getInvisibleRootIndex()); + ASSERT_FALSE(childIndex.parent().isValid()); } @@ -244,14 +254,14 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveCreateParentAndChild) { auto rootNode = createdNodes.front(); auto childNode = createdNodes.back(); - moveScenegraphChild(childNode, rootNode); + moveScenegraphChildren({childNode}, rootNode); ASSERT_EQ(childNode->getParent(), rootNode); auto rootIndex = viewModel_->indexFromObjectID(rootNode->objectID()); auto childIndex = viewModel_->indexFromObjectID(childNode->objectID()); ASSERT_EQ(rootIndex.row(), 0); - ASSERT_EQ(rootIndex.parent(), viewModel_->getInvisibleRootIndex()); + ASSERT_FALSE(rootIndex.parent().isValid()); ASSERT_EQ(childIndex.row(), 0); ASSERT_EQ(childIndex.parent(), rootIndex); @@ -268,12 +278,12 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveDontAllowMovingObjectIntoIt auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); auto rootNode = createdNodes.front(); - moveScenegraphChild(rootNode, rootNode); + moveScenegraphChildren({rootNode}, rootNode); auto rootIndex = viewModel_->indexFromObjectID(createdNodes.front()->objectID()); ASSERT_EQ(rootNode->getParent(), nullptr); ASSERT_EQ(rootIndex.row(), 0); - ASSERT_EQ(rootIndex.parent(), viewModel_->getInvisibleRootIndex()); + ASSERT_FALSE(rootIndex.parent().isValid()); } @@ -284,17 +294,17 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveDontAllowMovingParentIntoCh auto rootNode = createdNodes.front(); auto childNode = createdNodes.back(); - moveScenegraphChild(childNode, rootNode); + moveScenegraphChildren({childNode}, rootNode); ASSERT_EQ(childNode->getParent(), rootNode); - moveScenegraphChild(rootNode, childNode); + moveScenegraphChildren({rootNode}, childNode); auto rootIndex = viewModel_->indexFromObjectID(createdNodes.front()->objectID()); auto childIndex = viewModel_->indexFromObjectID(createdNodes.back()->objectID()); ASSERT_EQ(childNode->getParent(), rootNode); ASSERT_EQ(rootNode->getParent(), nullptr); ASSERT_EQ(rootIndex.row(), 0); - ASSERT_EQ(rootIndex.parent(), viewModel_->getInvisibleRootIndex()); + ASSERT_FALSE(rootIndex.parent().isValid()); ASSERT_EQ(childIndex.row(), 0); ASSERT_EQ(childIndex.parent(), rootIndex); } @@ -308,7 +318,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveParentMidAndChildProperHier auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); for (size_t i = 0; i < createdNodes.size() - 1; ++i) { - moveScenegraphChild(createdNodes[i + 1], createdNodes[i]); + moveScenegraphChildren({createdNodes[i + 1]}, createdNodes[i]); } for (size_t i = 1; i < createdNodes.size(); ++i) { @@ -329,12 +339,12 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveParentMidAndChildDontMovePa auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); for (size_t i = 0; i < createdNodes.size() - 1; ++i) { - moveScenegraphChild(createdNodes[i + 1], createdNodes[i]); + moveScenegraphChildren({createdNodes[i + 1]}, createdNodes[i]); } - moveScenegraphChild(createdNodes[0], createdNodes[1]); - moveScenegraphChild(createdNodes[1], createdNodes[2]); - moveScenegraphChild(createdNodes[0], createdNodes[2]); + moveScenegraphChildren({createdNodes[0]}, createdNodes[1]); + moveScenegraphChildren({createdNodes[1]}, createdNodes[2]); + moveScenegraphChildren({createdNodes[0]}, createdNodes[2]); for (size_t i = 1; i < createdNodes.size(); ++i) { ASSERT_EQ(createdNodes[i]->getParent(), createdNodes[i - 1]); @@ -354,15 +364,15 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveParentMidAndChildFlattenHie auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); for (size_t i = 0; i < createdNodes.size() - 1; ++i) { - moveScenegraphChild(createdNodes[i + 1], createdNodes[i]); + moveScenegraphChildren({createdNodes[i + 1]}, createdNodes[i]); } auto rootNode = createdNodes.front(); auto midNode = createdNodes[1]; auto childNode = createdNodes.back(); - moveScenegraphChild(childNode, rootNode); - moveScenegraphChild(midNode, rootNode); + moveScenegraphChildren({childNode}, rootNode); + moveScenegraphChildren({midNode}, rootNode); auto rootNodeChildren = rootNode->children_->asVector(); ASSERT_EQ(rootNodeChildren.size(), 2); @@ -385,7 +395,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveParentChildrenDifferentMovi auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); for (size_t i = 1; i < createdNodes.size(); ++i) { - moveScenegraphChild(createdNodes[i], createdNodes[0], 0); + moveScenegraphChildren({createdNodes[i]}, createdNodes[0], 0); } auto rootNode = createdNodes.front(); @@ -414,7 +424,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveInitiatedFromContext) { auto rootNodeChildren = rootNode->children_->asVector(); ASSERT_TRUE(rootNodeChildren.empty()); - commandInterface.moveScenegraphChild({childNode}, {rootNode}); + commandInterface.moveScenegraphChildren({childNode}, {rootNode}); dataChangeDispatcher_->dispatch(recorder); rootNodeChildren = rootNode->children_->asVector(); @@ -430,7 +440,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveWrongIndex) { auto rootNode = createdNodes.front(); auto childNode = createdNodes.back(); - ASSERT_DEATH(commandInterface.moveScenegraphChild({childNode}, {rootNode}, 1), ""); + ASSERT_DEATH(commandInterface.moveScenegraphChildren({childNode}, {rootNode}, 1), ""); } @@ -442,7 +452,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveMovingObjectsAround) { auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); for (size_t i = 1; i < createdNodes.size(); ++i) { - moveScenegraphChild(createdNodes[i], createdNodes[0]); + moveScenegraphChildren({createdNodes[i]}, createdNodes[0]); } auto rootNode = createdNodes.front(); @@ -457,28 +467,28 @@ TEST_F(ObjectTreeViewDefaultModelTest, SceneGraphMoveMovingObjectsAround) { } }; - moveScenegraphChild(movedChild, rootNode, 0); + moveScenegraphChildren({movedChild}, rootNode, 0); checkRootChildenOrder({1, 2, 3}); - moveScenegraphChild(movedChild, rootNode, 1); + moveScenegraphChildren({movedChild}, rootNode, 1); checkRootChildenOrder({1, 2, 3}); - moveScenegraphChild(movedChild, rootNode, 2); + moveScenegraphChildren({movedChild}, rootNode, 2); checkRootChildenOrder({2, 1, 3}); - moveScenegraphChild(movedChild, rootNode, 3); + moveScenegraphChildren({movedChild}, rootNode, 3); checkRootChildenOrder({2, 3, 1}); - moveScenegraphChild(movedChild, rootNode, 2); + moveScenegraphChildren({movedChild}, rootNode, 2); checkRootChildenOrder({2, 3, 1}); - moveScenegraphChild(movedChild, rootNode, 1); + moveScenegraphChildren({movedChild}, rootNode, 1); checkRootChildenOrder({2, 1, 3}); - moveScenegraphChild(movedChild, rootNode, 0); + moveScenegraphChildren({movedChild}, rootNode, 0); checkRootChildenOrder({1, 2, 3}); - moveScenegraphChild(movedChild, {}); + moveScenegraphChildren({movedChild}, {}); checkRootChildenOrder({2, 3}); } @@ -489,9 +499,8 @@ TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionJustOneObject) { ASSERT_EQ(project.instances().size(), 1); auto justAnIndex = viewModel_->indexFromObjectID(justANode->objectID()); - auto delObjAmount = deleteObjectAtIndex(justAnIndex); - ASSERT_EQ(delObjAmount, 1); + ASSERT_EQ(deleteObjectsAtIndices({justAnIndex}), 1); ASSERT_TRUE(project.instances().empty()); justAnIndex = viewModel_->indexFromObjectID(justANode->objectID()); @@ -505,11 +514,10 @@ TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionChildNode) { auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); auto parentNode = createdNodes.front(); auto childNode = createdNodes.back(); - moveScenegraphChild(childNode, parentNode); + moveScenegraphChildren({childNode}, parentNode); auto childIndex = viewModel_->indexFromObjectID(childNode->objectID()); - auto delObjAmount = deleteObjectAtIndex(childIndex); - ASSERT_EQ(delObjAmount, 1); + ASSERT_EQ(deleteObjectsAtIndices({childIndex}), 1); ASSERT_EQ(project.instances().size(), 1); auto parentIndex = viewModel_->indexFromObjectID(parentNode->objectID()); @@ -527,19 +535,18 @@ TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionParentNode) { auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); auto parentNode = createdNodes.front(); - auto child1Node = createdNodes[1]; - auto child2Node = createdNodes.back(); - moveScenegraphChild(child1Node, parentNode); - moveScenegraphChild(child2Node, parentNode); + auto childNode1 = createdNodes[1]; + auto childNode2 = createdNodes.back(); + moveScenegraphChildren({childNode1}, parentNode); + moveScenegraphChildren({childNode2}, parentNode); auto parentIndex = viewModel_->indexFromObjectID(parentNode->objectID()); - auto delObjAmount = deleteObjectAtIndex(parentIndex); - ASSERT_EQ(delObjAmount, 3); + ASSERT_EQ(deleteObjectsAtIndices({parentIndex}), 3); ASSERT_TRUE(project.instances().empty()); ASSERT_FALSE(viewModel_->indexFromObjectID(parentNode->objectID()).isValid()); - ASSERT_FALSE(viewModel_->indexFromObjectID(child1Node->objectID()).isValid()); - ASSERT_FALSE(viewModel_->indexFromObjectID(child2Node->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode1->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode2->objectID()).isValid()); } TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionMidNode) { @@ -551,30 +558,161 @@ TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionMidNode) { auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); auto parentNode = createdNodes.front(); auto midNode = createdNodes[1]; - auto child1Node = createdNodes[2]; - auto child2Node = createdNodes.back(); - moveScenegraphChild(midNode, parentNode); - moveScenegraphChild(child1Node, midNode); - moveScenegraphChild(child2Node, midNode); + auto childNode1 = createdNodes[2]; + auto childNode2 = createdNodes.back(); + moveScenegraphChildren({midNode}, parentNode); + moveScenegraphChildren({childNode1}, midNode); + moveScenegraphChildren({childNode2}, midNode); auto midIndex = viewModel_->indexFromObjectID(midNode->objectID()); - auto delObjAmount = deleteObjectAtIndex(midIndex); - ASSERT_EQ(delObjAmount, 3); + ASSERT_EQ(deleteObjectsAtIndices({midIndex}), 3); ASSERT_EQ(project.instances().size(), 1); ASSERT_TRUE(viewModel_->indexFromObjectID(parentNode->objectID()).isValid()); ASSERT_FALSE(viewModel_->indexFromObjectID(midNode->objectID()).isValid()); - ASSERT_FALSE(viewModel_->indexFromObjectID(child1Node->objectID()).isValid()); - ASSERT_FALSE(viewModel_->indexFromObjectID(child2Node->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode1->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode2->objectID()).isValid()); } +TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionMultiSelectionIncludingParent) { + nodeNames_ = { + "root", + "child1", "child2"}; + + auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); + auto parentNode = createdNodes.front(); + auto childNode1 = createdNodes[1]; + auto childNode2 = createdNodes.back(); + moveScenegraphChildren({childNode1}, parentNode); + moveScenegraphChildren({childNode2}, parentNode); + + auto parentIndex = viewModel_->indexFromObjectID(parentNode->objectID()); + auto child1Index = viewModel_->indexFromObjectID(childNode1->objectID()); + ASSERT_EQ(deleteObjectsAtIndices({parentIndex, child1Index}), 3); + ASSERT_TRUE(project.instances().empty()); + + ASSERT_FALSE(viewModel_->indexFromObjectID(parentNode->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode1->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode2->objectID()).isValid()); +} + +TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionMultiSelectionExcludingParent) { + nodeNames_ = { + "root", + "child1", "child2"}; + + auto createdNodes = createNodes(Node::typeDescription.typeName, nodeNames_); + auto parentNode = createdNodes.front(); + auto childNode1 = createdNodes[1]; + auto childNode2 = createdNodes.back(); + moveScenegraphChildren({childNode1}, parentNode); + moveScenegraphChildren({childNode2}, parentNode); + + auto child1Index = viewModel_->indexFromObjectID(childNode1->objectID()); + auto child2Index = viewModel_->indexFromObjectID(childNode2->objectID()); + ASSERT_EQ(deleteObjectsAtIndices({child1Index, child2Index}), 2); + ASSERT_TRUE(project.instances().size() == 1); + + ASSERT_TRUE(viewModel_->indexFromObjectID(parentNode->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode1->objectID()).isValid()); + ASSERT_FALSE(viewModel_->indexFromObjectID(childNode2->objectID()).isValid()); +} + +TEST_F(ObjectTreeViewDefaultModelTest, ObjectDeletionMultiSelectionIncludingPrefabInstanceChild) { + auto prefab = create("prefab"); + auto prefabInstance = create("prefabInstance"); + auto prefabChild = create("prefabChild"); + auto node = create("node"); + auto nodeChild = create("nodeChild"); + commandInterface.set({prefabInstance, {"template"}}, prefab); + commandInterface.moveScenegraphChildren({nodeChild}, node); + commandInterface.moveScenegraphChildren({prefabChild}, prefab); + viewModel_->buildObjectTree(); + auto prefabInstanceChild = prefabInstance->children_->asVector()[0]->as(); + + auto nodeIndex = viewModel_->indexFromObjectID(node->objectID()); + auto nodeChildIndex = viewModel_->indexFromObjectID(nodeChild->objectID()); + auto prefabInstanceIndex = viewModel_->indexFromObjectID(prefabInstance->objectID()); + auto prefabInstanceChildIndex = viewModel_->indexFromObjectID(prefabInstanceChild->objectID()); + + ASSERT_EQ(deleteObjectsAtIndices({prefabInstanceChildIndex}), 0); + nodeIndex = viewModel_->indexFromObjectID(node->objectID()); + nodeChildIndex = viewModel_->indexFromObjectID(nodeChild->objectID()); + prefabInstanceIndex = viewModel_->indexFromObjectID(prefabInstance->objectID()); + prefabInstanceChildIndex = viewModel_->indexFromObjectID(prefabInstanceChild->objectID()); + ASSERT_TRUE(nodeIndex.isValid()); + ASSERT_TRUE(nodeChildIndex.isValid()); + ASSERT_TRUE(prefabInstanceIndex.isValid()); + ASSERT_TRUE(prefabInstanceChildIndex.isValid()); + + ASSERT_EQ(deleteObjectsAtIndices({prefabInstanceChildIndex, nodeIndex}), 2); + nodeIndex = viewModel_->indexFromObjectID(node->objectID()); + nodeChildIndex = viewModel_->indexFromObjectID(nodeChild->objectID()); + prefabInstanceIndex = viewModel_->indexFromObjectID(prefabInstance->objectID()); + prefabInstanceChildIndex = viewModel_->indexFromObjectID(prefabInstanceChild->objectID()); + ASSERT_FALSE(nodeIndex.isValid()); + ASSERT_FALSE(nodeChildIndex.isValid()); + ASSERT_TRUE(prefabInstanceIndex.isValid()); + ASSERT_TRUE(prefabInstanceChildIndex.isValid()); + + ASSERT_EQ(deleteObjectsAtIndices({prefabInstanceIndex, prefabInstanceChildIndex}), 2); + prefabInstanceIndex = viewModel_->indexFromObjectID(prefabInstance->objectID()); + prefabInstanceChildIndex = viewModel_->indexFromObjectID(prefabInstanceChild->objectID()); + ASSERT_FALSE(prefabInstanceIndex.isValid()); + ASSERT_FALSE(prefabInstanceChildIndex.isValid()); +} + + +TEST_F(ObjectTreeViewDefaultModelTest, TypesAllowedIntoIndexEmptyIndex) { + auto allowedTypes = viewModel_->typesAllowedIntoIndex({}); + std::vector allowedTypesAssert {Animation::typeDescription.typeName, + Node::typeDescription.typeName, + MeshNode::typeDescription.typeName, + PrefabInstance::typeDescription.typeName, + OrthographicCamera::typeDescription.typeName, + PerspectiveCamera::typeDescription.typeName, + LuaScript::typeDescription.typeName}; + + ASSERT_EQ(allowedTypes.size(), allowedTypesAssert.size()); + for (int i = 0; i < allowedTypes.size(); ++i) { + ASSERT_EQ(allowedTypes[i], allowedTypesAssert[i]); + } +} + +TEST_F(ObjectTreeViewDefaultModelTest, TypesAllowedIntoIndexInvalidParent) { + auto prefabInstance = createNodes(PrefabInstance::typeDescription.typeName, {"prefabInstance"}).front(); + auto luaScript = createNodes(LuaScript::typeDescription.typeName, {"luaScript"}).front(); + auto animation = createNodes(Animation::typeDescription.typeName, {"animation"}).front(); + + ASSERT_TRUE(viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(prefabInstance->objectID())).empty()); + ASSERT_TRUE(viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(luaScript->objectID())).empty()); + ASSERT_TRUE(viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(animation->objectID())).empty()); +} + +TEST_F(ObjectTreeViewDefaultModelTest, TypesAllowedIntoIndexNode) { + auto node = createNodes(Node::typeDescription.typeName, {"node"}).front(); + + auto allowedTypes = viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(node->objectID())); + std::vector allowedTypesAssert {Animation::typeDescription.typeName, + Node::typeDescription.typeName, + MeshNode::typeDescription.typeName, + PrefabInstance::typeDescription.typeName, + OrthographicCamera::typeDescription.typeName, + PerspectiveCamera::typeDescription.typeName, + LuaScript::typeDescription.typeName}; + + ASSERT_EQ(allowedTypes.size(), allowedTypesAssert.size()); + for (int i = 0; i < allowedTypes.size(); ++i) { + ASSERT_EQ(allowedTypes[i], allowedTypesAssert[i]); + } +} TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsSceneGraphObjectsAreAllowedWithEmptyIndex) { for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (Queries::isNotResource(newObj) && typeName != Prefab::typeDescription.typeName) { - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->getInvisibleRootIndex())); + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex({}, newObj)); } } } @@ -592,9 +730,9 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsCheckAllSceneGraphObjectCombin auto pastingSomethingUnderAnimaton = sceneGraphNodeInScene->as(); if (pastingSomethingUnderPrefabInstance || pastingSomethingUnderLuaScript || pastingSomethingUnderAnimaton) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, sceneObjIndex)); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(sceneObjIndex, newObj)); } else { - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({newObj}, sceneObjIndex)); + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex(sceneObjIndex, newObj)); } } } @@ -605,7 +743,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsResourcesAreNotAllowedOnTopLev for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (Queries::isResource(newObj)) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->getInvisibleRootIndex())); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex({}, newObj)); } } } @@ -618,7 +756,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsResourcesAreNotAllowedUnderSce if (Queries::isResource(newObj)) { for (const auto &sceneGraphNodeInScene : allSceneGraphNodes) { auto sceneObjIndex = viewModel_->indexFromObjectID(sceneGraphNodeInScene->objectID()); - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, sceneObjIndex)); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(sceneObjIndex, newObj)); } } } @@ -629,10 +767,10 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsPrefabsAreNotAllowed) { auto allSceneGraphNodes = createAllSceneGraphObjects(); - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({prefab}, viewModel_->getInvisibleRootIndex())); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex({}, prefab)); for (const auto &sceneGraphNodeInScene : allSceneGraphNodes) { auto sceneObjIndex = viewModel_->indexFromObjectID(sceneGraphNodeInScene->objectID()); - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({prefab}, sceneObjIndex)); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(sceneObjIndex, prefab)); } } @@ -641,7 +779,7 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsCheckPrefabInstanceCombination auto allSceneGraphNodes = createAllSceneGraphObjects(); - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({prefabInstance}, viewModel_->getInvisibleRootIndex())); + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex({}, prefabInstance)); for (const auto &sceneGraphNodeInScene : allSceneGraphNodes) { auto sceneObjIndex = viewModel_->indexFromObjectID(sceneGraphNodeInScene->objectID()); auto pastingPrefabInstanceUnderPrefabInstance = sceneGraphNodeInScene->as(); @@ -649,16 +787,64 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsCheckPrefabInstanceCombination auto pastingPrefabInstanceUnderAnimation = sceneGraphNodeInScene->as(); if (pastingPrefabInstanceUnderPrefabInstance || pastingPrefabInstanceUnderLuaScript || pastingPrefabInstanceUnderAnimation) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({prefabInstance}, sceneObjIndex)); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(sceneObjIndex, prefabInstance)); } else { - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({prefabInstance}, sceneObjIndex)); + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex(sceneObjIndex, prefabInstance)); } } } +TEST_F(ObjectTreeViewDefaultModelTest, CanCopyAtIndicesMultiSelection) { + auto createdNodes = createNodes(Node::typeDescription.typeName, {"parent1", "child1", "parent2"}); + auto parentNode1 = createdNodes[0]; + auto childNode1 = createdNodes[1]; + auto parentNode2 = createdNodes[2]; + moveScenegraphChildren({childNode1}, parentNode1); + auto parent1Index = viewModel_->indexFromObjectID(parentNode1->objectID()); + auto child1Index = viewModel_->indexFromObjectID(childNode1->objectID()); + auto parent2Index = viewModel_->indexFromObjectID(parentNode2->objectID()); + + ASSERT_FALSE(viewModel_->canCopyAtIndices({})); + ASSERT_FALSE(viewModel_->canCopyAtIndices({{}})); + ASSERT_TRUE(viewModel_->canCopyAtIndices({parent1Index})); + ASSERT_TRUE(viewModel_->canCopyAtIndices({parent1Index, {}})); + ASSERT_TRUE(viewModel_->canCopyAtIndices({child1Index, parent2Index})); + ASSERT_TRUE(viewModel_->canCopyAtIndices({parent1Index, child1Index, parent2Index})); + ASSERT_TRUE(viewModel_->canCopyAtIndices({{}, parent1Index, child1Index, parent2Index, {}})); +} + + +TEST_F(ObjectTreeViewDefaultModelTest, CanDeleteAtIndicesMultiSelection) { + auto prefab = create("prefab"); + auto prefabInstance = create("prefabInstance"); + auto prefabChild = create("prefabChild"); + auto node = create("node"); + auto nodeChild = create("nodeChild"); + commandInterface.set({prefabInstance, {"template"}}, prefab); + commandInterface.moveScenegraphChildren({nodeChild}, node); + commandInterface.moveScenegraphChildren({prefabChild}, prefab); + viewModel_->buildObjectTree(); + auto prefabInstanceChild = prefabInstance->children_->asVector()[0]->as(); + + auto nodeIndex = viewModel_->indexFromObjectID(node->objectID()); + auto nodeChildIndex = viewModel_->indexFromObjectID(nodeChild->objectID()); + auto prefabInstanceIndex = viewModel_->indexFromObjectID(prefabInstance->objectID()); + auto prefabInstanceChildIndex = viewModel_->indexFromObjectID(prefabInstanceChild->objectID()); + + ASSERT_FALSE(viewModel_->canDeleteAtIndices({})); + ASSERT_FALSE(viewModel_->canDeleteAtIndices({{}})); + ASSERT_FALSE(viewModel_->canDeleteAtIndices({prefabInstanceChildIndex})); + ASSERT_TRUE(viewModel_->canDeleteAtIndices({nodeIndex})); + ASSERT_TRUE(viewModel_->canDeleteAtIndices({nodeIndex, {}})); + ASSERT_TRUE(viewModel_->canDeleteAtIndices({nodeChildIndex, prefabInstanceIndex})); + ASSERT_TRUE(viewModel_->canDeleteAtIndices({nodeIndex, nodeChildIndex, prefabInstanceIndex})); + ASSERT_TRUE(viewModel_->canDeleteAtIndices({{}, nodeIndex, nodeChildIndex, prefabInstanceIndex, {}})); + ASSERT_TRUE(viewModel_->canDeleteAtIndices({prefabInstanceIndex, prefabInstanceChildIndex})); +} + TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsDeepCopiedSceneGraphWithResourcesIsAllowed) { - auto meshNode = createNodes(raco::user_types::MeshNode::typeDescription.typeName, {raco::user_types::MeshNode::typeDescription.typeName}).front(); - auto mesh = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); + auto meshNode = createNodes(MeshNode::typeDescription.typeName, {MeshNode::typeDescription.typeName}).front(); + auto mesh = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); dataChangeDispatcher_->dispatch(recorder.release()); @@ -666,12 +852,13 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsDeepCopiedSceneGraphWithResour auto cutObjs = commandInterface.cutObjects({meshNode}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_TRUE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsDeepCopiedPrefabInstanceWithPrefabIsAllowed) { - auto prefabInstance = createNodes(raco::user_types::PrefabInstance::typeDescription.typeName, {raco::user_types::PrefabInstance::typeDescription.typeName}).front(); - auto prefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}).front(); + auto prefabInstance = createNodes(PrefabInstance::typeDescription.typeName, {PrefabInstance::typeDescription.typeName}).front(); + auto prefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{prefabInstance, {"template"}}, prefab); dataChangeDispatcher_->dispatch(recorder.release()); @@ -679,47 +866,67 @@ TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsDeepCopiedPrefabInstanceWithPr auto cutObjs = commandInterface.cutObjects({prefabInstance}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_TRUE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsLuaScriptIsAllowedAsExtRefOnTopLevel) { - auto luaScript = createNodes(raco::user_types::LuaScript::typeDescription.typeName, {raco::user_types::LuaScript::typeDescription.typeName}).front(); + auto luaScript = createNodes(LuaScript::typeDescription.typeName, {LuaScript::typeDescription.typeName}).front(); luaScript->addAnnotation(std::make_shared("differentProject")); dataChangeDispatcher_->dispatch(recorder.release()); auto cutObjs = commandInterface.cutObjects({luaScript}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_TRUE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs, true)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds, true)); } TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsChildLuaScriptIsNotAllowedAsExtRef) { - auto luaScript = createNodes(raco::user_types::LuaScript::typeDescription.typeName, {raco::user_types::LuaScript::typeDescription.typeName}).front(); - auto externalParentNode = createNodes(raco::user_types::Node::typeDescription.typeName, {Node::typeDescription.typeName}).front(); - auto localParentNode = createNodes(raco::user_types::Node::typeDescription.typeName, {"localParent"}); - moveScenegraphChild(luaScript, externalParentNode); + auto luaScript = createNodes(LuaScript::typeDescription.typeName, {LuaScript::typeDescription.typeName}).front(); + auto externalParentNode = createNodes(Node::typeDescription.typeName, {Node::typeDescription.typeName}).front(); + auto localParentNode = createNodes(Node::typeDescription.typeName, {"localParent"}); + moveScenegraphChildren({luaScript}, externalParentNode); externalParentNode->addAnnotation(std::make_shared("differentProject")); luaScript->addAnnotation(std::make_shared("differentProject")); dataChangeDispatcher_->dispatch(recorder.release()); auto cutObjs = commandInterface.cutObjects({luaScript}, true); + ASSERT_EQ(cutObjs, ""); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_FALSE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs, true)); - ASSERT_FALSE(viewModel_->canPasteInto(viewModel_->indexFromObjectID("NodelocalParent"), cutObjs, true)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_FALSE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds, true)); + ASSERT_FALSE(viewModel_->canPasteIntoIndex(viewModel_->indexFromObjectID("NodelocalParent"), parsedObjs, sourceProjectTopLevelObjectIds, true)); } - -TEST_F(ObjectTreeViewDefaultModelTest, AllowedObjsLuaScriptIsNotAllowedAsExtRefUnderChild) { - auto luaScript = createNodes(raco::user_types::LuaScript::typeDescription.typeName, {raco::user_types::LuaScript::typeDescription.typeName}).front(); - auto localParentNode = createNodes(raco::user_types::Node::typeDescription.typeName, {"localParent"}); +TEST_F(ObjectTreeViewDefaultModelTest, PasteLuaScriptAsExtRefNotAllowedAsChild) { + auto luaScript = createNodes(LuaScript::typeDescription.typeName, {LuaScript::typeDescription.typeName}).front(); + auto localParentNode = createNodes(Node::typeDescription.typeName, {"localParent"}); luaScript->addAnnotation(std::make_shared("differentProject")); dataChangeDispatcher_->dispatch(recorder.release()); auto cutObjs = commandInterface.cutObjects({luaScript}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_FALSE(viewModel_->canPasteInto(viewModel_->indexFromObjectID("NodelocalParent"), cutObjs, true)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_FALSE(viewModel_->canPasteIntoIndex(viewModel_->indexFromObjectID("NodelocalParent"), parsedObjs, sourceProjectTopLevelObjectIds, true)); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds, true)); } + +TEST_F(ObjectTreeViewDefaultModelTest, PasteLuaScriptAsExtRefNotAllowedWhenChildInExternalProject) { + auto luaScript = createNodes(LuaScript::typeDescription.typeName, {LuaScript::typeDescription.typeName}).front(); + auto externalParentNode = createNodes(Node::typeDescription.typeName, {"externalParentNode"}).front(); + auto localParentNode = createNodes(Node::typeDescription.typeName, {"localParent"}); + moveScenegraphChildren({luaScript}, externalParentNode); + luaScript->addAnnotation(std::make_shared("differentProject")); + dataChangeDispatcher_->dispatch(recorder.release()); + + auto cutObjs = commandInterface.cutObjects({luaScript}, true); + dataChangeDispatcher_->dispatch(recorder.release()); + + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_FALSE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds, true)); +} \ No newline at end of file diff --git a/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.h b/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.h index b85d2406..a1d85503 100644 --- a/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.h +++ b/gui/libObjectTree/tests/ObjectTreeViewDefaultModel_test.h @@ -22,6 +22,7 @@ #include "user_types/PrefabInstance.h" #include "user_types/UserObjectFactory.h" #include "user_types/Texture.h" +#include "user_types/Animation.h" class ObjectTreeViewDefaultModelTest : public TestEnvironmentCore { @@ -37,13 +38,13 @@ class ObjectTreeViewDefaultModelTest : public TestEnvironmentCore { return createdNodes; } - void moveScenegraphChild(raco::core::SEditorObject child, raco::core::SEditorObject parent, int row = -1) { - viewModel_->moveScenegraphChild(child, parent, row); + void moveScenegraphChildren(std::vector const &objects, raco::core::SEditorObject parent, int row = -1) { + viewModel_->moveScenegraphChildren(objects, parent, row); dataChangeDispatcher_->dispatch(recorder.release()); } - size_t deleteObjectAtIndex(const QModelIndex& index) { - auto delObjAmount = viewModel_->deleteObjectAtIndex(index); + size_t deleteObjectsAtIndices(const QModelIndexList& index) { + auto delObjAmount = viewModel_->deleteObjectsAtIndices({index}); dataChangeDispatcher_->dispatch(recorder.release()); return delObjAmount; } @@ -57,13 +58,7 @@ class ObjectTreeViewDefaultModelTest : public TestEnvironmentCore { std::vector nodeNames_; std::unique_ptr viewModel_; - ObjectTreeViewDefaultModelTest() : viewModel_{new raco::object_tree::model::ObjectTreeViewDefaultModel(&commandInterface, dataChangeDispatcher_, nullptr, - {raco::user_types::Node::typeDescription.typeName, - raco::user_types::MeshNode::typeDescription.typeName, - raco::user_types::PrefabInstance::typeDescription.typeName, - raco::user_types::OrthographicCamera::typeDescription.typeName, - raco::user_types::PerspectiveCamera::typeDescription.typeName, - raco::user_types::LuaScript::typeDescription.typeName})} {} + ObjectTreeViewDefaultModelTest(); void compareValuesInTree(const raco::core::SEditorObject &obj, const QModelIndex &objIndex, const raco::object_tree::model::ObjectTreeViewDefaultModel &viewModel); diff --git a/gui/libObjectTree/tests/ObjectTreeViewExternalProjectModel_test.cpp b/gui/libObjectTree/tests/ObjectTreeViewExternalProjectModel_test.cpp index 71507f35..4cad88b8 100644 --- a/gui/libObjectTree/tests/ObjectTreeViewExternalProjectModel_test.cpp +++ b/gui/libObjectTree/tests/ObjectTreeViewExternalProjectModel_test.cpp @@ -45,8 +45,6 @@ class ExposedObjectTreeViewExternalProjectModel : public model::ObjectTreeViewEx class ObjectTreeViewExternalProjectModelTest : public ObjectTreeViewDefaultModelTest { protected: - static inline const char* PROJECT_PATH = "projectPath.rca"; - static inline int NEW_PROJECT_NODE_AMOUNT{0}; raco::ramses_base::HeadlessEngineBackend backend{}; @@ -55,12 +53,13 @@ class ObjectTreeViewExternalProjectModelTest : public ObjectTreeViewDefaultModel raco::application::RaCoApplication otherApplication{otherBackend}; ExposedObjectTreeViewExternalProjectModel externalProjectModel{application}; - void generateExternalProject(const std::vector &instances) { - application.externalProjectsStore_.externalProjects_[PROJECT_PATH] = raco::application::RaCoProject::createNew(&otherApplication); - NEW_PROJECT_NODE_AMOUNT = raco::core::Queries::filterForVisibleObjects(application.externalProjectsStore_.externalProjects_[PROJECT_PATH]->project()->instances()).size(); + void generateExternalProject(const std::vector &instances, std::string projectPath = "projectPath.rca") { + application.externalProjectsStore_.externalProjects_[projectPath] = raco::application::RaCoProject::createNew(&otherApplication); + NEW_PROJECT_NODE_AMOUNT = raco::core::Queries::filterForVisibleObjects(application.externalProjectsStore_.externalProjects_[projectPath]->project()->instances()).size(); + auto project = application.externalProjectsStore_.externalProjects_[projectPath]->project(); for (const auto &instance : instances) { - application.externalProjectsStore_.externalProjects_[PROJECT_PATH]->project()->addInstance(instance); + project->addInstance(instance); } } }; @@ -73,13 +72,14 @@ TEST_F(ObjectTreeViewExternalProjectModelTest, InstantiationNoLocalInstancesInMo } TEST_F(ObjectTreeViewExternalProjectModelTest, LoadingProjectEmpty) { - generateExternalProject({}); + auto projectPath = "projectPath.rca"; + generateExternalProject({}, projectPath); externalProjectModel.triggerObjectTreeRebuilding(); auto rootNode = externalProjectModel.getInvisibleRootNode(); ASSERT_EQ(rootNode->childCount(), 1); - ASSERT_EQ(rootNode->getChildren().front()->getRepresentedObject()->objectName(), PROJECT_PATH); + ASSERT_EQ(rootNode->getChildren().front()->getRepresentedObject()->objectName(), projectPath); ASSERT_EQ(rootNode->getChildren().front()->childCount(), NEW_PROJECT_NODE_AMOUNT); } @@ -108,7 +108,7 @@ TEST_F(ObjectTreeViewExternalProjectModelTest, LoadingProjectHierarchyParentsCre } for (size_t i{0}; i < NODE_AMOUNT - 1; ++i) { - otherApplication.activeRaCoProject().commandInterface()->moveScenegraphChild(instances[i + 1], instances[i]); + otherApplication.activeRaCoProject().commandInterface()->moveScenegraphChildren({instances[i + 1]}, instances[i]); } generateExternalProject(instances); @@ -133,7 +133,7 @@ TEST_F(ObjectTreeViewExternalProjectModelTest, LoadingProjectHierarchyChildrenCr } for (size_t i{1}; i < NODE_AMOUNT; ++i) { - otherApplication.activeRaCoProject().commandInterface()->moveScenegraphChild(instances[i - 1], instances[i]); + otherApplication.activeRaCoProject().commandInterface()->moveScenegraphChildren({instances[i - 1]}, instances[i]); } generateExternalProject(instances); @@ -157,7 +157,7 @@ TEST_F(ObjectTreeViewExternalProjectModelTest, LoadingProjectHierarchyRetainChil for (size_t i{0}; i < CHILD_NODE_AMOUNT; ++i) { auto childNode = instances.emplace_back(otherApplication.activeRaCoProject().commandInterface()->createObject(raco::user_types::Node::typeDescription.typeName, "node_" + std::to_string(i))); - otherApplication.activeRaCoProject().commandInterface()->moveScenegraphChild(childNode, rootNode, 0); + otherApplication.activeRaCoProject().commandInterface()->moveScenegraphChildren({childNode}, rootNode, 0); } generateExternalProject(instances); @@ -169,4 +169,57 @@ TEST_F(ObjectTreeViewExternalProjectModelTest, LoadingProjectHierarchyRetainChil ASSERT_EQ(ourRootNode->getChild(0)->getRepresentedObject()->objectName(), "node_2"); ASSERT_EQ(ourRootNode->getChild(1)->getRepresentedObject()->objectName(), "node_1"); ASSERT_EQ(ourRootNode->getChild(2)->getRepresentedObject()->objectName(), "node_0"); -} \ No newline at end of file +} + +TEST_F(ObjectTreeViewExternalProjectModelTest, CanCopyAtIndicesMultiSelection) { + std::string project1Path = "project1Path.rca"; + auto project1Node = std::make_shared(); + generateExternalProject({project1Node}, project1Path); + + auto project2Node = std::make_shared(); + std::string project2Path = "project2Path.rca"; + generateExternalProject({project2Node}, project2Path); + + externalProjectModel.triggerObjectTreeRebuilding(); + + auto project1Index = externalProjectModel.indexFromObjectID(project1Path); + auto project1NodeIndex = externalProjectModel.indexFromObjectID(project1Node->objectID()); + auto project2Index = externalProjectModel.indexFromObjectID(project2Path); + auto project2NodeIndex = externalProjectModel.indexFromObjectID(project2Node->objectID()); + + ASSERT_TRUE(project1Index.isValid()); + ASSERT_TRUE(project1NodeIndex.isValid()); + ASSERT_TRUE(project2Index.isValid()); + ASSERT_TRUE(project2NodeIndex.isValid()); + + ASSERT_FALSE(externalProjectModel.canCopyAtIndices({})); + ASSERT_FALSE(externalProjectModel.canCopyAtIndices({project1Index})); + ASSERT_FALSE(externalProjectModel.canCopyAtIndices({project2Index})); + ASSERT_FALSE(externalProjectModel.canCopyAtIndices({project1Index, project2Index})); + + ASSERT_TRUE(externalProjectModel.canCopyAtIndices({project1NodeIndex})); + ASSERT_TRUE(externalProjectModel.canCopyAtIndices({project1Index, project1NodeIndex})); + ASSERT_TRUE(externalProjectModel.canCopyAtIndices({project1NodeIndex, {}})); + ASSERT_TRUE(externalProjectModel.canCopyAtIndices({project2NodeIndex})); + ASSERT_TRUE(externalProjectModel.canCopyAtIndices({project2Index, project2NodeIndex})); + ASSERT_TRUE(externalProjectModel.canCopyAtIndices({project2NodeIndex, {}})); + + ASSERT_FALSE(externalProjectModel.canCopyAtIndices({project1NodeIndex, project2NodeIndex})); +} + +TEST_F(ObjectTreeViewExternalProjectModelTest, CanDeleteAtIndicesNever) { + std::string project1Path = "project1Path.rca"; + auto project1Node = std::make_shared(); + generateExternalProject({project1Node}, project1Path); + + externalProjectModel.triggerObjectTreeRebuilding(); + + auto project1Index = externalProjectModel.indexFromObjectID(project1Path); + auto project1NodeIndex = externalProjectModel.indexFromObjectID(project1Node->objectID()); + + ASSERT_FALSE(externalProjectModel.canDeleteAtIndices({})); + ASSERT_FALSE(externalProjectModel.canDeleteAtIndices({{}})); + ASSERT_FALSE(externalProjectModel.canDeleteAtIndices({project1Index})); + ASSERT_FALSE(externalProjectModel.canDeleteAtIndices({project1NodeIndex})); + ASSERT_FALSE(externalProjectModel.canDeleteAtIndices({project1Index, project1NodeIndex, {}})); +} diff --git a/gui/libObjectTree/tests/ObjectTreeViewMultipleModels_test.cpp b/gui/libObjectTree/tests/ObjectTreeViewMultipleModels_test.cpp index dae8ea79..8cad85c9 100644 --- a/gui/libObjectTree/tests/ObjectTreeViewMultipleModels_test.cpp +++ b/gui/libObjectTree/tests/ObjectTreeViewMultipleModels_test.cpp @@ -17,10 +17,10 @@ using namespace raco::user_types; TEST_F(ObjectTreeViewMultipleModelsTest, MoveTopLevelNodeUnderPrefab) { auto node = createNodes(raco::user_types::Node::typeDescription.typeName, {"Node"}).front(); - moveScenegraphChild(node, prefab_); + moveScenegraphChildren({node}, prefab_); - auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, prefabModel_.getInvisibleRootIndex())); - auto *viewModelRootNode = viewModel_->indexToTreeNode(viewModel_->getInvisibleRootIndex()); + auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, {})); + auto *viewModelRootNode = viewModel_->indexToTreeNode({}); ASSERT_TRUE(viewModelRootNode->getChildren().empty()); ASSERT_EQ(prefabNode->getChildren().size(), 1); @@ -29,11 +29,11 @@ TEST_F(ObjectTreeViewMultipleModelsTest, MoveTopLevelNodeUnderPrefab) { TEST_F(ObjectTreeViewMultipleModelsTest, MovePrefabNodeToTopLevel) { auto node = createNodes(raco::user_types::Node::typeDescription.typeName, {"Node"}).front(); - moveScenegraphChild(node, prefab_); - moveScenegraphChild(node, nullptr); + moveScenegraphChildren({node}, prefab_); + moveScenegraphChildren({node}, nullptr); - auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, prefabModel_.getInvisibleRootIndex())); - auto *viewModelRootNode = viewModel_->indexToTreeNode(viewModel_->getInvisibleRootIndex()); + auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, {})); + auto *viewModelRootNode = viewModel_->indexToTreeNode({}); ASSERT_TRUE(prefabNode->getChildren().empty()); ASSERT_EQ(viewModelRootNode->getChildren().size(), 1); @@ -44,11 +44,11 @@ TEST_F(ObjectTreeViewMultipleModelsTest, MovePrefabNodeToTopLevel) { TEST_F(ObjectTreeViewMultipleModelsTest, MoveChildNodeUnderPrefab) { auto nodeParent = createNodes(raco::user_types::Node::typeDescription.typeName, {"NodeParent"}).front(); auto node = createNodes(raco::user_types::Node::typeDescription.typeName, {"Node"}).front(); - moveScenegraphChild(node, nodeParent); - moveScenegraphChild(node, prefab_); + moveScenegraphChildren({node}, nodeParent); + moveScenegraphChildren({node}, prefab_); - auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, prefabModel_.getInvisibleRootIndex())); - auto *viewModelRootNode = viewModel_->indexToTreeNode(viewModel_->getInvisibleRootIndex()); + auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, {})); + auto *viewModelRootNode = viewModel_->indexToTreeNode({}); ASSERT_EQ(viewModelRootNode->getChildren().size(), 1); ASSERT_EQ(viewModelRootNode->getChild(0)->getRepresentedObject()->objectName(), "NodeParent"); @@ -61,11 +61,11 @@ TEST_F(ObjectTreeViewMultipleModelsTest, MoveChildNodeUnderPrefab) { TEST_F(ObjectTreeViewMultipleModelsTest, MovePrefabNodeToNodeParent) { auto nodeParent = createNodes(raco::user_types::Node::typeDescription.typeName, {"NodeParent"}).front(); auto node = createNodes(raco::user_types::Node::typeDescription.typeName, {"Node"}).front(); - moveScenegraphChild(node, prefab_); - moveScenegraphChild(node, nodeParent); + moveScenegraphChildren({node}, prefab_); + moveScenegraphChildren({node}, nodeParent); - auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, prefabModel_.getInvisibleRootIndex())); - auto *viewModelRootNode = viewModel_->indexToTreeNode(viewModel_->getInvisibleRootIndex()); + auto *prefabNode = prefabModel_.indexToTreeNode(prefabModel_.index(0, 0, {})); + auto *viewModelRootNode = viewModel_->indexToTreeNode({}); ASSERT_EQ(viewModelRootNode->getChildren().size(), 1); ASSERT_EQ(viewModelRootNode->getChild(0)->getRepresentedObject()->objectName(), "NodeParent"); diff --git a/gui/libObjectTree/tests/ObjectTreeViewPrefabModel_test.cpp b/gui/libObjectTree/tests/ObjectTreeViewPrefabModel_test.cpp index 01d8eb3d..8ec88141 100644 --- a/gui/libObjectTree/tests/ObjectTreeViewPrefabModel_test.cpp +++ b/gui/libObjectTree/tests/ObjectTreeViewPrefabModel_test.cpp @@ -14,38 +14,85 @@ #include "core/Queries.h" #include "object_tree_view_model/ObjectTreeViewPrefabModel.h" +using namespace raco::user_types; + class ObjectTreeViewPrefabModelTest : public ObjectTreeViewDefaultModelTest { public: ObjectTreeViewPrefabModelTest() : ObjectTreeViewDefaultModelTest() { viewModel_.reset(new raco::object_tree::model::ObjectTreeViewPrefabModel(&commandInterface, dataChangeDispatcher_, nullptr, - {raco::user_types::Node::typeDescription.typeName, - raco::user_types::MeshNode::typeDescription.typeName, - raco::user_types::Prefab::typeDescription.typeName, - raco::user_types::PrefabInstance::typeDescription.typeName, - raco::user_types::OrthographicCamera::typeDescription.typeName, - raco::user_types::PerspectiveCamera::typeDescription.typeName, - raco::user_types::LuaScript::typeDescription.typeName})); + { Animation::typeDescription.typeName, + Node::typeDescription.typeName, + MeshNode::typeDescription.typeName, + Prefab::typeDescription.typeName, + PrefabInstance::typeDescription.typeName, + OrthographicCamera::typeDescription.typeName, + PerspectiveCamera::typeDescription.typeName, + LuaScript::typeDescription.typeName})); } }; +TEST_F(ObjectTreeViewPrefabModelTest, TypesAllowedIntoIndexEmptyIndex) { + auto allowedTypes = viewModel_->typesAllowedIntoIndex({}); + std::vector allowedTypesAssert {Prefab::typeDescription.typeName}; + + ASSERT_EQ(allowedTypes.size(), allowedTypesAssert.size()); + for (int i = 0; i < allowedTypes.size(); ++i) { + ASSERT_EQ(allowedTypes[i], allowedTypesAssert[i]); + } +} + +TEST_F(ObjectTreeViewPrefabModelTest, TypesAllowedIntoIndexInvalidParent) { + auto prefab = createNodes(Prefab::typeDescription.typeName, {"prefab"}).front(); + auto prefabInstance = createNodes(PrefabInstance::typeDescription.typeName, {"prefabInstance"}).front(); + auto luaScript = createNodes(LuaScript::typeDescription.typeName, {"luaScript"}).front(); + auto animation = createNodes(Animation::typeDescription.typeName, {"animation"}).front(); + moveScenegraphChildren({prefabInstance}, prefab); + moveScenegraphChildren({luaScript}, prefab); + moveScenegraphChildren({animation}, prefab); + + ASSERT_TRUE(viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(prefabInstance->objectID())).empty()); + ASSERT_TRUE(viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(luaScript->objectID())).empty()); + ASSERT_TRUE(viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(animation->objectID())).empty()); +} + +TEST_F(ObjectTreeViewPrefabModelTest, TypesAllowedIntoIndexNode) { + auto prefab = createNodes(Prefab::typeDescription.typeName, {"prefab"}).front(); + auto node = createNodes(Node::typeDescription.typeName, {"node"}).front(); + moveScenegraphChildren({node}, prefab); + + auto allowedTypes = viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(node->objectID())); + std::vector allowedTypesAssert {Animation::typeDescription.typeName, + Node::typeDescription.typeName, + MeshNode::typeDescription.typeName, + PrefabInstance::typeDescription.typeName, + OrthographicCamera::typeDescription.typeName, + PerspectiveCamera::typeDescription.typeName, + LuaScript::typeDescription.typeName}; + + ASSERT_EQ(allowedTypes.size(), allowedTypesAssert.size()); + for (int i = 0; i < allowedTypes.size(); ++i) { + ASSERT_EQ(allowedTypes[i], allowedTypesAssert[i]); + } +} + TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsResourcesAreNotAllowedOnTopLevel) { for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (raco::core::Queries::isResource(newObj)) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->getInvisibleRootIndex())); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex({} , newObj)); } } } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsResourcesAreNotAllowedUnderPrefab) { - auto prefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}); + auto prefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}); for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (raco::core::Queries::isResource(newObj)) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->indexFromObjectID(raco::user_types::Prefab::typeDescription.typeName + raco::user_types::Prefab::typeDescription.typeName))); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(viewModel_->indexFromObjectID(Prefab::typeDescription.typeName + Prefab::typeDescription.typeName), newObj)); } } } @@ -54,41 +101,41 @@ TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsCheckSceneGraphObjectsOnTopLeve for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (!raco::core::Queries::isResource(newObj)) { - if (typeName == raco::user_types::Prefab::typeDescription.typeName) { - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->getInvisibleRootIndex())); + if (typeName == Prefab::typeDescription.typeName) { + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex({}, newObj)); } else { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->getInvisibleRootIndex())); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex({}, newObj)); } } } } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsCheckExternalSceneGraphObjectsUnderPrefab) { - auto prefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}); - auto prefabIndex = viewModel_->indexFromObjectID(raco::user_types::Prefab::typeDescription.typeName + raco::user_types::Prefab::typeDescription.typeName); + auto prefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}); + auto prefabIndex = viewModel_->indexFromObjectID(Prefab::typeDescription.typeName + Prefab::typeDescription.typeName); for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (!raco::core::Queries::isResource(newObj) && !raco::core::Queries::isProjectSettings(newObj)) { - if (newObj->as()) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, prefabIndex)); + if (newObj->as()) { + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(prefabIndex, newObj)); } else { - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({newObj}, prefabIndex)); + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex(prefabIndex, newObj)); } } } } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsPrefabInTreeViewIsNotMovable) { - auto prefabs = createNodes(raco::user_types::Prefab::typeDescription.typeName, {"prefab1", "prefab2"}); - auto prefabIndex = viewModel_->indexFromObjectID(raco::user_types::Prefab::typeDescription.typeName + "prefab1"); + auto prefabs = createNodes(Prefab::typeDescription.typeName, {"prefab1", "prefab2"}); + auto prefabIndex = viewModel_->indexFromObjectID(Prefab::typeDescription.typeName + "prefab1"); - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({prefabs[1]}, prefabIndex)); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(prefabIndex, prefabs[1])); } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedSceneGraphWithResourcesIsNotAllowed) { - auto meshNode = createNodes(raco::user_types::MeshNode::typeDescription.typeName, {raco::user_types::MeshNode::typeDescription.typeName}).front(); - auto mesh = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); + auto meshNode = createNodes(MeshNode::typeDescription.typeName, {MeshNode::typeDescription.typeName}).front(); + auto mesh = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); dataChangeDispatcher_->dispatch(recorder.release()); @@ -96,13 +143,14 @@ TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedSceneGraphWithResourc auto cutObjs = commandInterface.cutObjects({meshNode}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_FALSE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_FALSE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedSceneGraphWithResourcesAllowedUnderPrefab) { - auto meshNode = createNodes(raco::user_types::MeshNode::typeDescription.typeName, {raco::user_types::MeshNode::typeDescription.typeName}).front(); - auto mesh = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); - auto prefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}).front(); + auto meshNode = createNodes(MeshNode::typeDescription.typeName, {MeshNode::typeDescription.typeName}).front(); + auto mesh = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); + auto prefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); dataChangeDispatcher_->dispatch(recorder.release()); @@ -110,12 +158,13 @@ TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedSceneGraphWithResourc auto copiedObjs = commandInterface.copyObjects({meshNode}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_TRUE(viewModel_->canPasteInto(viewModel_->indexFromObjectID(prefab->objectID()), copiedObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(copiedObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex(viewModel_->indexFromObjectID(prefab->objectID()), parsedObjs, sourceProjectTopLevelObjectIds)); } -TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedPrefabInstanceWithPrefabIsNotAllowed) { - auto prefabInstance = createNodes(raco::user_types::PrefabInstance::typeDescription.typeName, {raco::user_types::PrefabInstance::typeDescription.typeName}).front(); - auto prefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}).front(); +TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedPrefabInstanceWithPrefabIsAllowed) { + auto prefabInstance = createNodes(PrefabInstance::typeDescription.typeName, {PrefabInstance::typeDescription.typeName}).front(); + auto prefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{prefabInstance, {"template"}}, prefab); dataChangeDispatcher_->dispatch(recorder.release()); @@ -123,50 +172,53 @@ TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedPrefabInstanceWithPre auto cutObjs = commandInterface.cutObjects({prefabInstance}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_FALSE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedPrefabIsAllowedInEmptySpace) { - auto meshNode = createNodes(raco::user_types::MeshNode::typeDescription.typeName, {raco::user_types::MeshNode::typeDescription.typeName}).front(); - auto mesh = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); - auto prefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}).front(); + auto meshNode = createNodes(MeshNode::typeDescription.typeName, {MeshNode::typeDescription.typeName}).front(); + auto mesh = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); + auto prefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); dataChangeDispatcher_->dispatch(recorder.release()); - commandInterface.moveScenegraphChild(meshNode, prefab); + commandInterface.moveScenegraphChildren({meshNode}, prefab); dataChangeDispatcher_->dispatch(recorder.release()); auto copiedObjs = commandInterface.copyObjects({prefab}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_TRUE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), copiedObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(copiedObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsDeepCopiedPrefabIsAllowedUnderPrefab) { - auto meshNode = createNodes(raco::user_types::MeshNode::typeDescription.typeName, {raco::user_types::MeshNode::typeDescription.typeName}).front(); - auto mesh = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); - auto prefabs = createNodes(raco::user_types::Prefab::typeDescription.typeName, {"prefab", "prefab2"}); + auto meshNode = createNodes(MeshNode::typeDescription.typeName, {MeshNode::typeDescription.typeName}).front(); + auto mesh = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); + auto prefabs = createNodes(Prefab::typeDescription.typeName, {"prefab", "prefab2"}); commandInterface.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); dataChangeDispatcher_->dispatch(recorder.release()); - commandInterface.moveScenegraphChild(meshNode, prefabs.front()); + commandInterface.moveScenegraphChildren({meshNode}, prefabs.front()); dataChangeDispatcher_->dispatch(recorder.release()); auto copiedObjs = commandInterface.copyObjects({prefabs.front()}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_TRUE(viewModel_->canPasteInto(viewModel_->indexFromObjectID("prefab2"), copiedObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(copiedObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex(viewModel_->indexFromObjectID("prefab2"), parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewPrefabModelTest, AllowedObjsNothingIsAllowedUnderExtRef) { - auto extRefPrefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}).front(); + auto extRefPrefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}).front(); extRefPrefab->addAnnotation(std::make_shared("differentProject")); - auto extRefPrefabIndex = viewModel_->indexFromObjectID(raco::user_types::Prefab::typeDescription.typeName + raco::user_types::Prefab::typeDescription.typeName); + auto extRefPrefabIndex = viewModel_->indexFromObjectID(Prefab::typeDescription.typeName + Prefab::typeDescription.typeName); for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, extRefPrefabIndex)); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(extRefPrefabIndex, newObj)); } } diff --git a/gui/libObjectTree/tests/ObjectTreeViewResourceModel_test.cpp b/gui/libObjectTree/tests/ObjectTreeViewResourceModel_test.cpp index 3d0f3b82..7f5c97ae 100644 --- a/gui/libObjectTree/tests/ObjectTreeViewResourceModel_test.cpp +++ b/gui/libObjectTree/tests/ObjectTreeViewResourceModel_test.cpp @@ -15,34 +15,71 @@ #include "object_tree_view_model/ObjectTreeViewResourceModel.h" #include "core/SerializationFunctions.h" #include "user_types/AnimationChannel.h" +#include "user_types/LuaScriptModule.h" #include "user_types/RenderBuffer.h" #include "user_types/RenderLayer.h" #include "user_types/RenderTarget.h" #include "user_types/RenderPass.h" +using namespace raco::user_types; + class ObjectTreeViewResourceModelTest : public ObjectTreeViewDefaultModelTest { public: ObjectTreeViewResourceModelTest() : ObjectTreeViewDefaultModelTest() { viewModel_.reset(new raco::object_tree::model::ObjectTreeViewResourceModel(&commandInterface, dataChangeDispatcher_, nullptr, { - raco::user_types::AnimationChannel::typeDescription.typeName, - raco::user_types::CubeMap::typeDescription.typeName, - raco::user_types::Material::typeDescription.typeName, - raco::user_types::Mesh::typeDescription.typeName, - raco::user_types::Texture::typeDescription.typeName, - raco::user_types::RenderBuffer::typeDescription.typeName, - raco::user_types::RenderTarget::typeDescription.typeName, - raco::user_types::RenderLayer::typeDescription.typeName, - raco::user_types::RenderPass::typeDescription.typeName})); + AnimationChannel::typeDescription.typeName, + CubeMap::typeDescription.typeName, + Material::typeDescription.typeName, + Mesh::typeDescription.typeName, + LuaScriptModule::typeDescription.typeName, + Texture::typeDescription.typeName, + RenderBuffer::typeDescription.typeName, + RenderTarget::typeDescription.typeName, + RenderLayer::typeDescription.typeName, + RenderPass::typeDescription.typeName})); } }; +TEST_F(ObjectTreeViewResourceModelTest, TypesAllowedIntoIndexEmptyIndex) { + auto allowedTypes = viewModel_->typesAllowedIntoIndex({}); + std::vector allowedTypesAssert { + AnimationChannel::typeDescription.typeName, + CubeMap::typeDescription.typeName, + Material::typeDescription.typeName, + Mesh::typeDescription.typeName, + LuaScriptModule::typeDescription.typeName, + Texture::typeDescription.typeName, + RenderBuffer::typeDescription.typeName, + RenderTarget::typeDescription.typeName, + RenderLayer::typeDescription.typeName, + RenderPass::typeDescription.typeName}; + + ASSERT_EQ(allowedTypes.size(), allowedTypesAssert.size()); + for (int i = 0; i < allowedTypes.size(); ++i) { + ASSERT_EQ(allowedTypes[i], allowedTypesAssert[i]); + } +} + +TEST_F(ObjectTreeViewResourceModelTest, TypesAllowedIntoIndexAnyTypeBehavesLikeEmptyIndex) { + auto allowedTypesEmptyIndex = viewModel_->typesAllowedIntoIndex({}); + + for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { + auto item = createNodes(typeName, {typeName}).front(); + auto allowedTypes = viewModel_->typesAllowedIntoIndex(viewModel_->indexFromObjectID(item->objectID())); + + ASSERT_EQ(allowedTypes.size(), allowedTypesEmptyIndex.size()); + for (int i = 0; i < allowedTypes.size(); ++i) { + ASSERT_EQ(allowedTypes[i], allowedTypesEmptyIndex[i]); + } + } +} TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsResourcesAreAllowedOnTopLevel) { for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (raco::core::Queries::isResource(newObj)) { - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->getInvisibleRootIndex())); + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex({}, newObj)); } } } @@ -55,8 +92,9 @@ TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsResourcesAreAllowedOnTopLevel auto copiedObjs = commandInterface.copyObjects({newObj}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_EQ(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), copiedObjs, true), - &newObj->getTypeDescription() != &raco::user_types::RenderPass::typeDescription); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(copiedObjs); + ASSERT_EQ(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds, true), + &newObj->getTypeDescription() != &RenderPass::typeDescription); } } } @@ -67,13 +105,14 @@ TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsResourcesAreAllowedWithResour auto newObj = viewModel_->objectFactory()->createObject(typeName); if (raco::core::Queries::isResource(newObj)) { for (const auto &resourceInScene : allResources) { - ASSERT_TRUE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->indexFromObjectID(resourceInScene->objectID()))); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(viewModel_->indexFromObjectID(resourceInScene->objectID()), newObj)); + ASSERT_TRUE(viewModel_->isObjectAllowedIntoIndex({}, newObj)); } } } } -TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsResourcesAreAllowedAsExtRefWithResourceParentSelected) { +TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsResourcesAreAllowedAsExtRef) { auto allResources = createAllResources(); for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); @@ -82,10 +121,8 @@ TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsResourcesAreAllowedAsExtRefWi auto copyObjs = commandInterface.copyObjects({newObj}, true); dataChangeDispatcher_->dispatch(recorder.release()); - for (const auto &resourceInScene : allResources) { - ASSERT_EQ(viewModel_->canPasteInto(viewModel_->indexFromObjectID(resourceInScene->objectID()), copyObjs, true), - &newObj->getTypeDescription() != &raco::user_types::RenderPass::typeDescription); - } + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(copyObjs); + ASSERT_EQ(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds, true), &newObj->getTypeDescription() != &RenderPass::typeDescription); } } } @@ -95,33 +132,35 @@ TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsSceneGraphObjectsAreNotAllowe for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (!raco::core::Queries::isResource(newObj)) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, viewModel_->getInvisibleRootIndex())); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex({}, newObj)); } } } TEST_F(ObjectTreeViewResourceModelTest, PastePastingMaterialsUnderMaterialCreatesMaterialOnTopLevel) { - createNodes(raco::user_types::Material::typeDescription.typeName, {raco::user_types::Material::typeDescription.typeName}).front(); - auto materialIndex = viewModel_->indexFromObjectID(raco::user_types::Material::typeDescription.typeName + raco::user_types::Material::typeDescription.typeName); + createNodes(Material::typeDescription.typeName, {Material::typeDescription.typeName}).front(); + auto materialIndex = viewModel_->indexFromObjectID(Material::typeDescription.typeName + Material::typeDescription.typeName); - auto resourceChild = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); + auto resourceChild = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); auto cutObjs = commandInterface.cutObjects({resourceChild}, false); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_TRUE(viewModel_->canPasteInto(materialIndex, cutObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_FALSE(viewModel_->canPasteIntoIndex(materialIndex, parsedObjs, sourceProjectTopLevelObjectIds)); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); viewModel_->pasteObjectAtIndex(materialIndex, false, nullptr, cutObjs); dataChangeDispatcher_->dispatch(recorder.release()); - materialIndex = viewModel_->indexFromObjectID(raco::user_types::Material::typeDescription.typeName + raco::user_types::Material::typeDescription.typeName); + materialIndex = viewModel_->indexFromObjectID(Material::typeDescription.typeName + Material::typeDescription.typeName); ASSERT_TRUE(materialIndex.isValid()); ASSERT_EQ(viewModel_->indexToTreeNode(materialIndex)->childCount(), 0); ASSERT_EQ(viewModel_->project()->instances().size(), 2); } -TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsDeepCopiedSceneGraphWithResourcesIsNotAllowed) { - auto meshNode = createNodes(raco::user_types::MeshNode::typeDescription.typeName, {raco::user_types::MeshNode::typeDescription.typeName}).front(); - auto mesh = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); +TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsDeepCopiedSceneGraphWithResourcesIsAllowed) { + auto meshNode = createNodes(MeshNode::typeDescription.typeName, {MeshNode::typeDescription.typeName}).front(); + auto mesh = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{meshNode, {"mesh"}}, mesh); dataChangeDispatcher_->dispatch(recorder.release()); @@ -129,12 +168,13 @@ TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsDeepCopiedSceneGraphWithResou auto cutObjs = commandInterface.cutObjects({meshNode}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_FALSE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_TRUE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsDeepCopiedPrefabInstanceWithPrefabIsNotAllowed) { - auto prefabInstance = createNodes(raco::user_types::PrefabInstance::typeDescription.typeName, {raco::user_types::PrefabInstance::typeDescription.typeName}).front(); - auto prefab = createNodes(raco::user_types::Prefab::typeDescription.typeName, {raco::user_types::Prefab::typeDescription.typeName}).front(); + auto prefabInstance = createNodes(PrefabInstance::typeDescription.typeName, {PrefabInstance::typeDescription.typeName}).front(); + auto prefab = createNodes(Prefab::typeDescription.typeName, {Prefab::typeDescription.typeName}).front(); commandInterface.set(raco::core::ValueHandle{prefabInstance, {"template"}}, prefab); dataChangeDispatcher_->dispatch(recorder.release()); @@ -142,18 +182,19 @@ TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsDeepCopiedPrefabInstanceWithP auto cutObjs = commandInterface.cutObjects({prefabInstance}, true); dataChangeDispatcher_->dispatch(recorder.release()); - ASSERT_FALSE(viewModel_->canPasteInto(viewModel_->getInvisibleRootIndex(), cutObjs)); + auto [parsedObjs, sourceProjectTopLevelObjectIds] = viewModel_->getObjectsAndRootIdsFromClipboardString(cutObjs); + ASSERT_FALSE(viewModel_->canPasteIntoIndex({}, parsedObjs, sourceProjectTopLevelObjectIds)); } TEST_F(ObjectTreeViewResourceModelTest, AllowedObjsNoSceneGraphObjectsAreAllowedUnderExtRef) { - auto extRefMesh = createNodes(raco::user_types::Mesh::typeDescription.typeName, {raco::user_types::Mesh::typeDescription.typeName}).front(); + auto extRefMesh = createNodes(Mesh::typeDescription.typeName, {Mesh::typeDescription.typeName}).front(); extRefMesh->addAnnotation(std::make_shared("differentProject")); - auto extRefMeshIndex = viewModel_->indexFromObjectID(raco::user_types::Mesh::typeDescription.typeName + raco::user_types::Mesh::typeDescription.typeName); + auto extRefMeshIndex = viewModel_->indexFromObjectID(Mesh::typeDescription.typeName + Mesh::typeDescription.typeName); for (const auto &[typeName, typeInfo] : viewModel_->objectFactory()->getTypes()) { auto newObj = viewModel_->objectFactory()->createObject(typeName); if (!raco::core::Queries::isResource(newObj)) { - ASSERT_FALSE(viewModel_->objectsAreAllowedInModel({newObj}, extRefMeshIndex)); + ASSERT_FALSE(viewModel_->isObjectAllowedIntoIndex(extRefMeshIndex, newObj)); } } } \ No newline at end of file diff --git a/gui/libPropertyBrowser/include/property_browser/PropertyBrowserWidget.h b/gui/libPropertyBrowser/include/property_browser/PropertyBrowserWidget.h index 51f91c1d..85de230d 100644 --- a/gui/libPropertyBrowser/include/property_browser/PropertyBrowserWidget.h +++ b/gui/libPropertyBrowser/include/property_browser/PropertyBrowserWidget.h @@ -26,9 +26,12 @@ class PropertyBrowserView final : public QWidget { public: explicit PropertyBrowserView(PropertyBrowserItem* item, PropertyBrowserModel* model, QWidget* parent = nullptr); + std::string getCurrentObjectID() const; + private: QPoint verticalPivot_{0, 0}; QWidget* verticalPivotWidget_{nullptr}; + std::string currentObjectID_; }; class PropertyBrowserWidget final : public QWidget { diff --git a/gui/libPropertyBrowser/include/property_browser/editors/VecNTEditor.h b/gui/libPropertyBrowser/include/property_browser/editors/VecNTEditor.h index 2c362c3f..dd349835 100644 --- a/gui/libPropertyBrowser/include/property_browser/editors/VecNTEditor.h +++ b/gui/libPropertyBrowser/include/property_browser/editors/VecNTEditor.h @@ -56,7 +56,7 @@ class VecNTEditor final : public QWidget { QObject::connect(spinboxes_[i].get(), &SpinBoxType::valueChanged, item, [item, i](T value) { item->children().at(i)->set(value); }); QObject::connect(item->children().at(i), &PropertyBrowserItem::valueChanged, spinboxes_[i].get(), [this, i](raco::core::ValueHandle& handle) { spinboxes_[i]->setValue(handle.as()); - }); + }); if (i < N - 1) { int nextSpinboxIndex = i + 1; @@ -67,6 +67,12 @@ class VecNTEditor final : public QWidget { layout->addWidget(spinboxes_[i].get(), 0, i); } + QObject::connect(item, &PropertyBrowserItem::childrenChanged, item, [this](const QList& children) { + for (int i = 0; i < N; i++) { + spinboxes_[i]->setValue(children.at(i)->valueHandle().as()); + } + }); + } public Q_SLOTS: void setEnabled(bool val) { diff --git a/gui/libPropertyBrowser/src/PropertyBrowserItem.cpp b/gui/libPropertyBrowser/src/PropertyBrowserItem.cpp index 606494ef..88d8019e 100644 --- a/gui/libPropertyBrowser/src/PropertyBrowserItem.cpp +++ b/gui/libPropertyBrowser/src/PropertyBrowserItem.cpp @@ -370,9 +370,10 @@ bool PropertyBrowserItem::getDefaultExpandedFromValueHandleType() const { auto parent = valueHandle_.parent(); - bool isInputOutputGroup = parent.isObject() && - (valueHandle_.isRefToProp(&raco::user_types::LuaScript::luaInputs_) || - valueHandle_.isRefToProp(&raco::user_types::LuaScript::luaOutputs_)); + bool isTopLevelLuaValueGroup = parent.isObject() && + (valueHandle_.isRefToProp(&raco::user_types::LuaScript::luaInputs_) + || valueHandle_.isRefToProp(&raco::user_types::LuaScript::luaOutputs_) + || valueHandle_.isRefToProp(&raco::user_types::LuaScript::luaModules_)); bool isFirstLevelChildOfInputOutputGroup = parent.isProperty() && parent.parent().isObject() && @@ -382,7 +383,7 @@ bool PropertyBrowserItem::getDefaultExpandedFromValueHandleType() const { bool isTableOrStruct = valueHandle_.type() == PrimitiveType::Table || valueHandle_.type() == PrimitiveType::Struct; - return isInputOutputGroup || (isFirstLevelChildOfInputOutputGroup && isTableOrStruct); + return isTopLevelLuaValueGroup || (isFirstLevelChildOfInputOutputGroup && isTableOrStruct); } return true; diff --git a/gui/libPropertyBrowser/src/PropertyBrowserWidget.cpp b/gui/libPropertyBrowser/src/PropertyBrowserWidget.cpp index 70100db0..23b8bb7d 100644 --- a/gui/libPropertyBrowser/src/PropertyBrowserWidget.cpp +++ b/gui/libPropertyBrowser/src/PropertyBrowserWidget.cpp @@ -68,7 +68,7 @@ constexpr QLabel* createNotificationWidget(PropertyBrowserModel* model, QWidget* } PropertyBrowserView::PropertyBrowserView(PropertyBrowserItem* item, PropertyBrowserModel* model, QWidget* parent) - : QWidget{parent} { + : currentObjectID_(item->valueHandle().rootObject()->objectID()), QWidget{parent} { item->setParent(this); auto* layout = new PropertyBrowserGridLayout{this}; layout->setColumnStretch(0, 1); @@ -139,6 +139,10 @@ PropertyBrowserView::PropertyBrowserView(PropertyBrowserItem* item, PropertyBrow contentLayout->addWidget(new PropertySubtreeView{model, item, this}); } +std::string PropertyBrowserView::getCurrentObjectID() const { + return currentObjectID_; +} + PropertyBrowserWidget::PropertyBrowserWidget( SDataChangeDispatcher dispatcher, raco::core::CommandInterface* commandInterface, @@ -169,9 +173,7 @@ void PropertyBrowserWidget::setLockable(bool lockable) { } void PropertyBrowserWidget::clear() { - if (!locked_) { - clearValueHandle(false); - } + clearValueHandle(false); } void PropertyBrowserWidget::setLocked(bool locked) { @@ -180,29 +182,38 @@ void PropertyBrowserWidget::setLocked(bool locked) { } void PropertyBrowserWidget::clearValueHandle(bool restorable) { - if (restorable && propertyBrowser_) { - subscription_ = dispatcher_->registerOnObjectsLifeCycle([this](core::SEditorObject obj) { - if (restorableObjectId_ == obj->objectID()) - setValueHandle({ obj }); - }, [](auto){}); - } else { - restorableObjectId_ = ""; - subscription_ = raco::components::Subscription{}; + if (!locked_) { + if (restorable && propertyBrowser_) { + subscription_ = dispatcher_->registerOnObjectsLifeCycle([this](core::SEditorObject obj) { + if (restorableObjectId_ == obj->objectID()) { + setValueHandle({ obj }); + }}, [](auto) {}); + + } else { + restorableObjectId_ = ""; + subscription_ = raco::components::Subscription{}; + } + propertyBrowser_.reset(); + emptyLabel_->setVisible(true); } - propertyBrowser_.reset(); - emptyLabel_->setVisible(true); } void PropertyBrowserWidget::setValueHandle(core::ValueHandle valueHandle) { + if (propertyBrowser_ && propertyBrowser_->getCurrentObjectID() == valueHandle.rootObject()->objectID()) { + // No need to update the Value Handle if we still are referencing to the same object. + // This happens for example when the display name changes, thus the tree view will update and then restore the selected item in the property browser. + return; + } + if (!locked_) { emptyLabel_->setVisible(false); restorableObjectId_ = valueHandle.rootObject()->objectID(); subscription_ = dispatcher_->registerOnObjectsLifeCycle([](auto) {}, [this, valueHandle](core::SEditorObject obj) { if (valueHandle.rootObject() == obj) { - clearValueHandle(true); if (locked_) { setLocked(false); } + clearValueHandle(true); } }); propertyBrowser_.reset(new PropertyBrowserView{new PropertyBrowserItem{valueHandle, dispatcher_, commandInterface_, model_}, model_, this}); @@ -220,7 +231,7 @@ void PropertyBrowserWidget::setValueHandles(const std::set 1) { - // TODO: handle multi-selection here + clearValueHandle(false); } else { setValueHandle(*valueHandles.begin()); } diff --git a/gui/libPropertyBrowser/src/PropertySubtreeView.cpp b/gui/libPropertyBrowser/src/PropertySubtreeView.cpp index 7f155a32..ecf2f415 100644 --- a/gui/libPropertyBrowser/src/PropertySubtreeView.cpp +++ b/gui/libPropertyBrowser/src/PropertySubtreeView.cpp @@ -187,7 +187,7 @@ void PropertySubtreeView::updateChildrenContainer() { layout_.addWidget(childrenContainer_, 2, 0); } } else if (!item_->showChildren() && childrenContainer_) { - childrenContainer_->deleteLater(); + delete childrenContainer_; childrenContainer_ = nullptr; } diff --git a/gui/libPropertyBrowser/src/editors/DoubleEditor.cpp b/gui/libPropertyBrowser/src/editors/DoubleEditor.cpp index 9ef8ea88..6a796ceb 100644 --- a/gui/libPropertyBrowser/src/editors/DoubleEditor.cpp +++ b/gui/libPropertyBrowser/src/editors/DoubleEditor.cpp @@ -56,7 +56,10 @@ DoubleEditor::DoubleEditor( // State change: Show spinbox or slider QObject::connect(slider, &DoubleSlider::singleClicked, this, [this, spinBox]() { stack_->setCurrentWidget(spinBox); }); - QObject::connect(spinBox, &DoubleSpinBox::editingFinished, this, [this, slider]() { stack_->setCurrentWidget(slider); }); + QObject::connect(spinBox, &DoubleSpinBox::editingFinished, this, [this, slider]() { + stack_->setCurrentWidget(slider); + slider->clearFocus(); + }); QObject::connect(spinBox, &DoubleSpinBox::focusNextRequested, this, [this, item]() { item->requestNextSiblingFocus(); }); QObject::connect(item, &PropertyBrowserItem::widgetRequestFocus, this, [this, spinBox]() { stack_->setCurrentWidget(spinBox); diff --git a/gui/libPropertyBrowser/src/editors/IntEditor.cpp b/gui/libPropertyBrowser/src/editors/IntEditor.cpp index fbaee520..ddc7bdb3 100644 --- a/gui/libPropertyBrowser/src/editors/IntEditor.cpp +++ b/gui/libPropertyBrowser/src/editors/IntEditor.cpp @@ -53,7 +53,10 @@ IntEditor::IntEditor( // State change: Show spinbox or slider QObject::connect(slider, &IntSlider::singleClicked, this, [this, spinBox]() { stack_->setCurrentWidget(spinBox); }); - QObject::connect(spinBox, &IntSpinBox::editingFinished, this, [this, slider]() { stack_->setCurrentWidget(slider); }); + QObject::connect(spinBox, &IntSpinBox::editingFinished, this, [this, slider]() { + stack_->setCurrentWidget(slider); + slider->clearFocus(); + }); QObject::connect(spinBox, &IntSpinBox::focusNextRequested, this, [this, item]() { item->requestNextSiblingFocus(); }); QObject::connect(item, &PropertyBrowserItem::widgetRequestFocus, this, [this, spinBox]() { stack_->setCurrentWidget(spinBox); diff --git a/gui/libPropertyBrowser/src/editors/RefEditor.cpp b/gui/libPropertyBrowser/src/editors/RefEditor.cpp index a95c287b..9dd97c37 100644 --- a/gui/libPropertyBrowser/src/editors/RefEditor.cpp +++ b/gui/libPropertyBrowser/src/editors/RefEditor.cpp @@ -53,7 +53,7 @@ RefEditor::RefEditor( QObject::connect(comboBox_, qOverload(&QComboBox::activated), ref_, &PropertyBrowserRef::setIndex); QObject::connect(comboBox_, qOverload(&QComboBox::currentIndexChanged), [this](auto index) { emptyReference_ = (index == PropertyBrowserRef::EMPTY_REF_INDEX); - goToRefObjectButton_->setDisabled(index == PropertyBrowserRef::EMPTY_REF_INDEX); + goToRefObjectButton_->setDisabled(emptyReference_); }); QObject::connect(item, &PropertyBrowserItem::widgetRequestFocus, this, [this]() { comboBox_->setFocus(); diff --git a/gui/libStyle/include/style/Icons.h b/gui/libStyle/include/style/Icons.h index e75c0073..77c2bb22 100644 --- a/gui/libStyle/include/style/Icons.h +++ b/gui/libStyle/include/style/Icons.h @@ -52,6 +52,7 @@ enum class Pixmap { typePrefabInternal, typePrefabExternal, typePrefabInstance, + typeLuaScriptModule, typeAnimationChannel, typeAnimation }; diff --git a/gui/libStyle/src/Icons.cpp b/gui/libStyle/src/Icons.cpp index 8dc6eb7c..1acc8a41 100644 --- a/gui/libStyle/src/Icons.cpp +++ b/gui/libStyle/src/Icons.cpp @@ -46,6 +46,7 @@ Icons& Icons::instance() { {Pixmap::typePrefabInternal, QPixmap{":typePrefabInternalIcon"}}, {Pixmap::typePrefabExternal, QPixmap{":typePrefabExternalIcon"}}, {Pixmap::typePrefabInstance, QPixmap{":typePrefabInstanceIcon"}}, + {Pixmap::typeLuaScriptModule, QPixmap{":typeLuaScriptModuleIcon"}}, {Pixmap::typeAnimationChannel, QPixmap{":typeAnimationChannelIcon"}}, {Pixmap::typeAnimation, QPixmap{":typeAnimationIcon"}} }; diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index 5ce7c14a..4192be4a 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -31,6 +31,8 @@ set(RESOURCE_FILES scripts/array.lua scripts/compile-error.lua scripts/Heavy.lua + scripts/moduleDefinition.lua + scripts/moduleDependency.lua scripts/order.lua scripts/runtime-error.lua scripts/SimpleScript.lua diff --git a/resources/scripts/moduleDefinition.lua b/resources/scripts/moduleDefinition.lua new file mode 100644 index 00000000..4f8c7ad9 --- /dev/null +++ b/resources/scripts/moduleDefinition.lua @@ -0,0 +1,14 @@ +local coalaModule = {} + +coalaModule.coalaChief = "Alfred" + +coalaModule.coalaStruct = { + preferredFood = STRING, + weight = INT +} + +function coalaModule.bark() + print("Coalas don't bark...") +end + +return coalaModule \ No newline at end of file diff --git a/resources/scripts/moduleDependency.lua b/resources/scripts/moduleDependency.lua new file mode 100644 index 00000000..aadc5913 --- /dev/null +++ b/resources/scripts/moduleDependency.lua @@ -0,0 +1,17 @@ +modules("coalas") + +function interface() + IN.zz = INT + IN.aa = FLOAT + IN.abc = INT + IN.za = STRING + IN.ff = BOOL + + OUT.zz = INT + OUT.aa = FLOAT + OUT.zzz = FLOAT + OUT.e = STRING +end + +function run() +end \ No newline at end of file diff --git a/styles/_Default/icons/baseline_view_module_white_18dp.png b/styles/_Default/icons/baseline_view_module_white_18dp.png new file mode 100644 index 00000000..5495f987 --- /dev/null +++ b/styles/_Default/icons/baseline_view_module_white_18dp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:765064d0698fa2587e9610fa9206fb2fbceb61e5dc6f3cab56478665d06abc4c +size 106 diff --git a/styles/icons.qrc b/styles/icons.qrc index 080eedde..c9106120 100644 --- a/styles/icons.qrc +++ b/styles/icons.qrc @@ -37,5 +37,6 @@ _Default/icons/baseline_center_focus_strong_white_18dp.png _Default/icons/baseline_center_focus_weak_white_18dp.png _Default/icons/baseline_crop_free_white_18dp.png + _Default/icons/baseline_view_module_white_18dp.png diff --git a/third_party/ramses-logic b/third_party/ramses-logic index 0e3eb067..c4f05291 160000 --- a/third_party/ramses-logic +++ b/third_party/ramses-logic @@ -1 +1 @@ -Subproject commit 0e3eb067337f972d385115c8bf06cf3a4743fb5c +Subproject commit c4f05291c50719d0b56c919af4cf0d5b0bfdaf37