From d5eb55bef132e5d0b1ad2028298d18539d2c2cbb Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 23 Apr 2026 18:13:43 +0200 Subject: [PATCH 01/23] feat(editor): add dedicated .ovprefab file type and icon support --- Resources/Editor/Textures/Prefab.png | Bin 0 -> 437 bytes Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp | 1 + Sources/OvEditor/src/OvEditor/Core/Editor.cpp | 2 +- .../src/OvEditor/Core/EditorResources.cpp | 1 + .../OvTools/include/OvTools/Utils/PathParser.h | 1 + Sources/OvTools/src/OvTools/Utils/PathParser.cpp | 3 +++ 6 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Resources/Editor/Textures/Prefab.png diff --git a/Resources/Editor/Textures/Prefab.png b/Resources/Editor/Textures/Prefab.png new file mode 100644 index 0000000000000000000000000000000000000000..8a328d1c06e7e581b5560c2a5ab450eb22cf09e0 GIT binary patch literal 437 zcmV;m0ZRUfP)@O}kWw$v^Hw0>0A@&iz&i4ymsIs%^b?5&jP#k0s~eyk1?VZBNL%Db2FOhG@(z^2Qodn f^_K3c^ym5l;hI!o-z1H;00000NkvXXu0mjf+my60 literal 0 HcmV?d00001 diff --git a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp index e3d47948..cd4840c0 100644 --- a/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp +++ b/Sources/OvCore/src/OvCore/Helpers/GUIHelpers.cpp @@ -32,6 +32,7 @@ namespace case EFileType::SOUND: return "Pick Sound"; case EFileType::SCRIPT: return "Pick Script"; case EFileType::SCENE: return "Pick Scene"; + case EFileType::PREFAB: return "Pick Prefab"; default: return "Pick Asset"; } } diff --git a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp index eb4b36bb..f33b8766 100644 --- a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp @@ -131,7 +131,7 @@ void OvEditor::Core::Editor::SetupUI() { EDITOR_EXEC(LoadSceneFromDisk(path)); } - else if (fileType == EFileType::SCRIPT || fileType == EFileType::SHADER || fileType == EFileType::SHADER_PART) + else if (fileType == EFileType::SCRIPT || fileType == EFileType::SHADER || fileType == EFileType::SHADER_PART || fileType == EFileType::PREFAB) { EDITOR_EXEC(OpenInCodeEditor(m_editorActions.GetRealPath(path))); } diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp index 712c6529..40e77827 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorResources.cpp @@ -123,6 +123,7 @@ OvEditor::Core::EditorResources::EditorResources(const std::string& p_editorAsse {"Component", CreateTexture(texturesFolder / "Component.png")}, {"Material", CreateTexture(texturesFolder / "Material.png")}, {"Scene", CreateTexture(texturesFolder / "Scene.png")}, + {"Prefab", CreateTexture(texturesFolder / "Prefab.png")}, {"Sound", CreateTexture(texturesFolder / "Sound.png")}, {"Script", CreateTexture(texturesFolder / "Script.png")}, {"Add_Script", CreateTexture(texturesFolder / "Add_Script.png")}, diff --git a/Sources/OvTools/include/OvTools/Utils/PathParser.h b/Sources/OvTools/include/OvTools/Utils/PathParser.h index 7b5af46b..e41450a1 100644 --- a/Sources/OvTools/include/OvTools/Utils/PathParser.h +++ b/Sources/OvTools/include/OvTools/Utils/PathParser.h @@ -28,6 +28,7 @@ namespace OvTools::Utils MATERIAL, SOUND, SCENE, + PREFAB, SCRIPT, FONT }; diff --git a/Sources/OvTools/src/OvTools/Utils/PathParser.cpp b/Sources/OvTools/src/OvTools/Utils/PathParser.cpp index 07453886..3195eb67 100644 --- a/Sources/OvTools/src/OvTools/Utils/PathParser.cpp +++ b/Sources/OvTools/src/OvTools/Utils/PathParser.cpp @@ -102,6 +102,7 @@ std::string OvTools::Utils::PathParser::FileTypeToString(EFileType p_fileType) case OvTools::Utils::PathParser::EFileType::MATERIAL: return "Material"; case OvTools::Utils::PathParser::EFileType::SOUND: return "Sound"; case OvTools::Utils::PathParser::EFileType::SCENE: return "Scene"; + case OvTools::Utils::PathParser::EFileType::PREFAB: return "Prefab"; case OvTools::Utils::PathParser::EFileType::SCRIPT: return "Script"; case OvTools::Utils::PathParser::EFileType::FONT: return "Font"; default: return "Unknown"; @@ -115,6 +116,7 @@ OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::StringToFileTy if (p_type == "Shader") return EFileType::SHADER; if (p_type == "Material") return EFileType::MATERIAL; if (p_type == "Sound") return EFileType::SOUND; + if (p_type == "Prefab") return EFileType::PREFAB; return EFileType::UNKNOWN; } @@ -130,6 +132,7 @@ OvTools::Utils::PathParser::EFileType OvTools::Utils::PathParser::GetFileType(co else if (ext == "ovmat") return EFileType::MATERIAL; else if (ext == "wav" || ext == "mp3" || ext == "ogg") return EFileType::SOUND; else if (ext == "ovscene") return EFileType::SCENE; + else if (ext == "ovprefab") return EFileType::PREFAB; else if (ext == "lua" || ext == "ovscript") return EFileType::SCRIPT; else if (ext == "ttf") return EFileType::FONT; From dd27e041fb1197d1934e99094355e868db623565 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 23 Apr 2026 18:13:54 +0200 Subject: [PATCH 02/23] feat(editor): add prefab save/instantiate workflow and retargeting --- .../include/OvEditor/Core/EditorActions.h | 13 ++ .../include/OvEditor/Panels/SceneView.h | 3 +- .../include/OvEditor/Utils/PrefabOperations.h | 48 +++++++ .../src/OvEditor/Core/EditorActions.cpp | 47 +++++++ .../src/OvEditor/Panels/Hierarchy.cpp | 35 +++++ .../src/OvEditor/Panels/SceneView.cpp | 9 ++ .../src/OvEditor/Utils/PrefabOperations.cpp | 122 ++++++++++++++++++ 7 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h create mode 100644 Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp diff --git a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h index 5d43ef7e..d9239ac9 100644 --- a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h +++ b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h @@ -225,6 +225,19 @@ namespace OvEditor::Core * @param bool */ void DuplicateActor(OvCore::ECS::Actor& p_toDuplicate, OvCore::ECS::Actor* p_forcedParent = nullptr, bool p_focus = true); + + /** + * Save an actor hierarchy to a prefab file + * @param p_actor + * @param p_path + */ + void SaveActorAsPrefab(OvCore::ECS::Actor& p_actor, const std::string& p_path); + + /** + * Instantiate a prefab file in the current scene + * @param p_path + */ + OvCore::ECS::Actor* InstantiatePrefab(const std::string& p_path); #pragma endregion #pragma region ACTOR_MANIPULATION diff --git a/Sources/OvEditor/include/OvEditor/Panels/SceneView.h b/Sources/OvEditor/include/OvEditor/Panels/SceneView.h index edaafdf6..dcb5eb14 100644 --- a/Sources/OvEditor/include/OvEditor/Panels/SceneView.h +++ b/Sources/OvEditor/include/OvEditor/Panels/SceneView.h @@ -64,6 +64,7 @@ namespace OvEditor::Panels void OnSceneDropped(const std::string& p_path); void OnModelDropped(const std::string& p_path); void OnMaterialDropped(const std::string& p_path); + void OnPrefabDropped(const std::string& p_path); private: OvCore::SceneSystem::SceneManager& m_sceneManager; @@ -74,4 +75,4 @@ namespace OvEditor::Panels OvTools::Utils::OptRef m_highlightedActor; std::optional m_highlightedGizmoDirection; }; -} \ No newline at end of file +} diff --git a/Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h b/Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h new file mode 100644 index 00000000..fb39655e --- /dev/null +++ b/Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h @@ -0,0 +1,48 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#pragma once + +#include +#include + +namespace OvCore::ECS +{ + class Actor; +} + +namespace OvEditor::Utils +{ + /** + * Utility functions to serialize and instantiate prefab files. + */ + class PrefabOperations + { + public: + /** + * Disabled constructor + */ + PrefabOperations() = delete; + + /** + * Save an actor hierarchy to a prefab file. + * @param p_rootActor + * @param p_outputPath + */ + static bool SaveToFile(OvCore::ECS::Actor& p_rootActor, const std::filesystem::path& p_outputPath); + + /** + * Instantiate a prefab file using the provided actor factory. + * @param p_prefabPath + * @param p_createActor + */ + static OvCore::ECS::Actor* InstantiateFromFile( + const std::filesystem::path& p_prefabPath, + const std::function& p_createActor + ); + }; +} + diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 8d2a725d..1e4a8d62 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include @@ -143,6 +144,7 @@ namespace } ); } + } std::string OvEditor::Core::GetBuildTypeName(OvEditor::Core::EBuildType p_buildType) @@ -882,6 +884,46 @@ void OvEditor::Core::EditorActions::DuplicateActor(OvCore::ECS::Actor & p_toDupl DuplicateActor(*child, &newActor, false); } +void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_actor, const std::string& p_path) +{ + if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, p_path)) + { + OVLOG_ERROR("Failed to save prefab to: " + p_path); + return; + } + + OVLOG_INFO("Prefab saved to: " + p_path); +} + +OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std::string& p_path) +{ + if (!m_context.sceneManager.GetCurrentScene()) + { + return nullptr; + } + + const std::filesystem::path realPath = GetRealPath(p_path); + + auto* instantiatedRoot = OvEditor::Utils::PrefabOperations::InstantiateFromFile( + realPath, + [this]() -> OvCore::ECS::Actor& + { + return CreateEmptyActor(false); + } + ); + + if (instantiatedRoot) + { + OVLOG_INFO("Prefab instantiated: " + realPath.string()); + } + else + { + OVLOG_ERROR("Failed to instantiate prefab from: " + realPath.string()); + } + + return instantiatedRoot; +} + void OvEditor::Core::EditorActions::CopyActor(OvCore::ECS::Actor& p_actor) { m_context.copyBuffer = Context::ActorCopyBuffer{ @@ -1366,6 +1408,7 @@ void OvEditor::Core::EditorActions::MigrateScripts() const auto newRelPath = (std::filesystem::path("Scripts") / scriptName).generic_string(); PropagateFileRenameThroughSavedFilesOfType(stem, newRelPath, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(stem, newRelPath, OvTools::Utils::PathParser::EFileType::PREFAB); } OVLOG_INFO("Scene files updated with new script paths"); @@ -1525,6 +1568,7 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa if (next != "?") { PropagateFileRenameThroughSavedFilesOfType(prev, next, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(prev, next, OvTools::Utils::PathParser::EFileType::PREFAB); } EDITOR_PANEL(Panels::Inspector, "Inspector").Refresh(); @@ -1532,9 +1576,11 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa } case OvTools::Utils::PathParser::EFileType::MATERIAL: PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); break; case OvTools::Utils::PathParser::EFileType::MODEL: PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::MATERIAL); break; case OvTools::Utils::PathParser::EFileType::SHADER: @@ -1545,6 +1591,7 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa break; case OvTools::Utils::PathParser::EFileType::SOUND: PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); break; default: break; diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index b76fb75d..ad7517b9 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -32,6 +32,8 @@ #include #include +#include +#include #include "OvEditor/Core/EditorResources.h" #include "OvEditor/Utils/ActorCreationMenu.h" @@ -73,6 +75,39 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu { EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, m_target), 0)); }; + + auto& saveAsPrefabButton = CreateWidget("Save as Prefab..."); + saveAsPrefabButton.ClickedEvent += [this] + { + OvWindowing::Dialogs::SaveFileDialog dialog("Save Prefab"); + const auto initialPath = EDITOR_CONTEXT(projectAssetsPath) / m_target->GetName(); + dialog.SetInitialDirectory(initialPath.string()); + dialog.DefineExtension("Overload Prefab", ".ovprefab"); + dialog.Show(); + + if (!dialog.HasSucceeded()) + { + return; + } + + if (dialog.IsFileExisting()) + { + OvWindowing::Dialogs::MessageBox message( + "File already exists!", + "The file \"" + dialog.GetSelectedFileName() + "\" already exists.\n\nOverwriting this file will replace the previous prefab content.\n\nAre you ok with that?", + OvWindowing::Dialogs::MessageBox::EMessageType::WARNING, + OvWindowing::Dialogs::MessageBox::EButtonLayout::YES_NO, + true + ); + + if (message.GetUserAction() != OvWindowing::Dialogs::MessageBox::EUserAction::YES) + { + return; + } + } + + EDITOR_EXEC(SaveActorAsPrefab(*m_target, dialog.GetSelectedFilePath())); + }; auto& deleteButton = CreateWidget("Delete"); deleteButton.ClickedEvent += [this] diff --git a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp index a030d224..3cb964f5 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/SceneView.cpp @@ -61,6 +61,7 @@ OvEditor::Panels::SceneView::SceneView case SCENE: OnSceneDropped(path); break; case MODEL: OnModelDropped(path); break; case MATERIAL: OnMaterialDropped(path); break; + case PREFAB: OnPrefabDropped(path); break; default: break; } }; @@ -300,3 +301,11 @@ void OvEditor::Panels::SceneView::OnMaterialDropped(const std::string& p_path) } } } + +void OvEditor::Panels::SceneView::OnPrefabDropped(const std::string& p_path) +{ + if (auto* actor = EDITOR_EXEC(InstantiatePrefab(p_path)); actor) + { + EDITOR_EXEC(SelectActor(*actor)); + } +} diff --git a/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp b/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp new file mode 100644 index 00000000..a1aaa267 --- /dev/null +++ b/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp @@ -0,0 +1,122 @@ +/** +* @project: Overload +* @author: Overload Tech. +* @licence: MIT +*/ + +#include +#include +#include + +#include + +#include + +#include + +namespace +{ + void SerializeActorHierarchy( + OvCore::ECS::Actor& p_actor, + tinyxml2::XMLDocument& p_doc, + tinyxml2::XMLNode& p_actorsRoot) + { + p_actor.OnSerialize(p_doc, &p_actorsRoot); + + for (auto* child : p_actor.GetChildren()) + { + SerializeActorHierarchy(*child, p_doc, p_actorsRoot); + } + } +} + +bool OvEditor::Utils::PrefabOperations::SaveToFile(OvCore::ECS::Actor& p_rootActor, const std::filesystem::path& p_outputPath) +{ + tinyxml2::XMLDocument doc; + + auto* rootNode = doc.NewElement("root"); + doc.InsertFirstChild(rootNode); + + auto* prefabNode = doc.NewElement("prefab"); + rootNode->InsertEndChild(prefabNode); + + auto* actorsNode = doc.NewElement("actors"); + prefabNode->InsertEndChild(actorsNode); + + SerializeActorHierarchy(p_rootActor, doc, *actorsNode); + + return doc.SaveFile(p_outputPath.string().c_str()) == tinyxml2::XML_SUCCESS; +} + +OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( + const std::filesystem::path& p_prefabPath, + const std::function& p_createActor) +{ + if (!p_createActor) + { + return nullptr; + } + + tinyxml2::XMLDocument doc; + doc.LoadFile(p_prefabPath.string().c_str()); + + if (doc.Error()) + { + return nullptr; + } + + auto* rootNode = doc.FirstChildElement("root"); + auto* prefabNode = rootNode ? rootNode->FirstChildElement("prefab") : nullptr; + auto* actorsNode = prefabNode ? prefabNode->FirstChildElement("actors") : nullptr; + + if (!actorsNode) + { + return nullptr; + } + + struct PendingAttachment + { + OvCore::ECS::Actor* actor = nullptr; + int64_t sourceParentID = 0; + }; + + std::vector pendingAttachments; + std::unordered_map sourceToInstance; + + for (auto* currentActor = actorsNode->FirstChildElement("actor"); + currentActor; + currentActor = currentActor->NextSiblingElement("actor")) + { + auto& newActor = p_createActor(); + const int64_t generatedID = newActor.GetID(); + const uint64_t generatedGUID = newActor.GetGUID(); + + newActor.OnDeserialize(doc, currentActor); + + pendingAttachments.push_back({ + &newActor, + newActor.GetParentID() + }); + + sourceToInstance[newActor.GetID()] = &newActor; + + newActor.SetID(generatedID); + newActor.SetGUID(generatedGUID); + } + + OvCore::ECS::Actor* instantiatedRoot = nullptr; + + for (auto& pending : pendingAttachments) + { + if (auto found = sourceToInstance.find(pending.sourceParentID); found != sourceToInstance.end()) + { + pending.actor->SetParent(*found->second); + } + else if (!instantiatedRoot) + { + instantiatedRoot = pending.actor; + } + } + + return instantiatedRoot; +} From 64e7e4ef21e9e05538190bb43ec804ab4892a4ac Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 01:47:50 +0200 Subject: [PATCH 03/23] feat(prefab): track source instances and add apply/revert actions --- Sources/OvCore/include/OvCore/ECS/Actor.h | 18 +++++ Sources/OvCore/src/OvCore/ECS/Actor.cpp | 26 +++++++ .../include/OvEditor/Core/EditorActions.h | 15 ++++ .../src/OvEditor/Core/EditorActions.cpp | 78 +++++++++++++++++++ 4 files changed, 137 insertions(+) diff --git a/Sources/OvCore/include/OvCore/ECS/Actor.h b/Sources/OvCore/include/OvCore/ECS/Actor.h index 80fe9031..77562beb 100644 --- a/Sources/OvCore/include/OvCore/ECS/Actor.h +++ b/Sources/OvCore/include/OvCore/ECS/Actor.h @@ -103,6 +103,23 @@ namespace OvCore::ECS */ uint64_t GetGUID() const; + /** + * Defines the prefab source path for this actor. + * An empty path means this actor is not linked to a prefab source. + * @param p_prefabSource + */ + void SetPrefabSource(const std::string& p_prefabSource); + + /** + * Returns the prefab source path for this actor. + */ + const std::string& GetPrefabSource() const; + + /** + * Returns true if this actor is linked to a prefab source. + */ + bool HasPrefabSource() const; + /** * Set an actor as the parent of this actor * @param p_parent @@ -369,6 +386,7 @@ namespace OvCore::ECS /* Internal settings */ int64_t m_actorID; uint64_t m_guid; + std::string m_prefabSource; bool m_destroyed = false; bool m_sleeping = true; bool m_awaked = false; diff --git a/Sources/OvCore/src/OvCore/ECS/Actor.cpp b/Sources/OvCore/src/OvCore/ECS/Actor.cpp index 0455099c..5d1ee1f5 100644 --- a/Sources/OvCore/src/OvCore/ECS/Actor.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Actor.cpp @@ -140,6 +140,28 @@ uint64_t OvCore::ECS::Actor::GetGUID() const return m_guid; } +void OvCore::ECS::Actor::SetPrefabSource(const std::string& p_prefabSource) +{ + if (p_prefabSource == "?") + { + m_prefabSource.clear(); + } + else + { + m_prefabSource = p_prefabSource; + } +} + +const std::string& OvCore::ECS::Actor::GetPrefabSource() const +{ + return m_prefabSource; +} + +bool OvCore::ECS::Actor::HasPrefabSource() const +{ + return !m_prefabSource.empty(); +} + void OvCore::ECS::Actor::SetParent(Actor& p_parent) { DetachFromParent(); @@ -451,6 +473,7 @@ void OvCore::ECS::Actor::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XM OvCore::Helpers::Serializer::SerializeBoolean(p_doc, actorNode, "active", m_active); OvCore::Helpers::Serializer::SerializeInt64(p_doc, actorNode, "id", m_actorID); OvCore::Helpers::Serializer::SerializeUInt64(p_doc, actorNode, "guid", m_guid); + OvCore::Helpers::Serializer::SerializeString(p_doc, actorNode, "prefab_source", m_prefabSource); OvCore::Helpers::Serializer::SerializeInt64(p_doc, actorNode, "parent", m_parentID); tinyxml2::XMLNode* componentsNode = p_doc.NewElement("components"); @@ -505,6 +528,9 @@ void OvCore::ECS::Actor::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2:: OvCore::Helpers::Serializer::DeserializeBoolean(p_doc, p_actorsRoot, "active", m_active); OvCore::Helpers::Serializer::DeserializeInt64(p_doc, p_actorsRoot, "id", m_actorID); OvCore::Helpers::Serializer::DeserializeUInt64(p_doc, p_actorsRoot, "guid", m_guid); + std::string prefabSource; + OvCore::Helpers::Serializer::DeserializeString(p_doc, p_actorsRoot, "prefab_source", prefabSource); + SetPrefabSource(prefabSource); OvCore::Helpers::Serializer::DeserializeInt64(p_doc, p_actorsRoot, "parent", m_parentID); { diff --git a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h index d9239ac9..a0b2c462 100644 --- a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h +++ b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h @@ -238,6 +238,21 @@ namespace OvEditor::Core * @param p_path */ OvCore::ECS::Actor* InstantiatePrefab(const std::string& p_path); + + /** + * Apply the current actor hierarchy state to its prefab source. + * Returns true on success. + * @param p_actor + */ + bool ApplyActorToPrefab(OvCore::ECS::Actor& p_actor); + + /** + * Revert an actor hierarchy from its prefab source. + * The actor instance is replaced by a fresh prefab instantiation. + * Returns the new root actor on success. + * @param p_actor + */ + OvCore::ECS::Actor* RevertActorToPrefab(OvCore::ECS::Actor& p_actor); #pragma endregion #pragma region ACTOR_MANIPULATION diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 1e4a8d62..8206aa90 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -56,6 +56,16 @@ namespace { constexpr std::string_view kDefaultMaterialPath = ":Materials\\Default.ovmat"; + void SetPrefabSourceRecursively(OvCore::ECS::Actor& p_rootActor, const std::string& p_prefabSource) + { + p_rootActor.SetPrefabSource(p_prefabSource); + + for (auto* child : p_rootActor.GetChildren()) + { + SetPrefabSourceRecursively(*child, p_prefabSource); + } + } + void RefreshMaterialsUsingShader( OvCore::ResourceManagement::MaterialManager& p_materialManager, OvRendering::Resources::Shader& p_shader @@ -892,6 +902,9 @@ void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_acto return; } + const std::string prefabSourcePath = GetResourcePath(p_path); + SetPrefabSourceRecursively(p_actor, prefabSourcePath); + OVLOG_INFO("Prefab saved to: " + p_path); } @@ -903,6 +916,7 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: } const std::filesystem::path realPath = GetRealPath(p_path); + const std::string prefabSourcePath = GetResourcePath(realPath.string()); auto* instantiatedRoot = OvEditor::Utils::PrefabOperations::InstantiateFromFile( realPath, @@ -914,6 +928,7 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: if (instantiatedRoot) { + SetPrefabSourceRecursively(*instantiatedRoot, prefabSourcePath); OVLOG_INFO("Prefab instantiated: " + realPath.string()); } else @@ -924,6 +939,54 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: return instantiatedRoot; } +bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_actor) +{ + if (!p_actor.HasPrefabSource()) + { + OVLOG_WARNING("Cannot apply actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); + return false; + } + + const std::string realPath = GetRealPath(p_actor.GetPrefabSource()); + + if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, realPath)) + { + OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath); + return false; + } + + OVLOG_INFO("Prefab updated from actor \"" + p_actor.GetName() + "\": " + realPath); + return true; +} + +OvCore::ECS::Actor* OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_actor) +{ + if (!p_actor.HasPrefabSource()) + { + OVLOG_WARNING("Cannot revert actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); + return nullptr; + } + + OvCore::ECS::Actor* parent = p_actor.GetParent(); + const auto prefabSourcePath = p_actor.GetPrefabSource(); + + auto* instantiatedRoot = InstantiatePrefab(prefabSourcePath); + if (!instantiatedRoot) + { + return nullptr; + } + + if (parent) + { + instantiatedRoot->SetParent(*parent); + } + + DestroyActor(p_actor); + SelectActor(*instantiatedRoot); + + return instantiatedRoot; +} + void OvEditor::Core::EditorActions::CopyActor(OvCore::ECS::Actor& p_actor) { m_context.copyBuffer = Context::ActorCopyBuffer{ @@ -1590,6 +1653,21 @@ void OvEditor::Core::EditorActions::PropagateFileRename(std::string p_previousNa PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::MATERIAL); break; case OvTools::Utils::PathParser::EFileType::SOUND: + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); + break; + case OvTools::Utils::PathParser::EFileType::PREFAB: + if (auto currentScene = m_context.sceneManager.GetCurrentScene()) + { + for (auto actor : currentScene->GetActors()) + { + if (actor->GetPrefabSource() == p_previousName) + { + actor->SetPrefabSource(p_newName); + } + } + } + PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::SCENE); PropagateFileRenameThroughSavedFilesOfType(p_previousName, p_newName, OvTools::Utils::PathParser::EFileType::PREFAB); break; From c813f1530ff793491b0520ceb23162df7003b1a6 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 01:48:04 +0200 Subject: [PATCH 04/23] feat(editor): add prefab creation entry and hierarchy prefab controls --- .../src/OvEditor/Panels/Hierarchy.cpp | 48 +++++++++++++++++-- .../src/OvEditor/Utils/ActorCreationMenu.cpp | 36 ++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index ad7517b9..4a18d48b 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -35,9 +36,31 @@ #include #include +#include + #include "OvEditor/Core/EditorResources.h" #include "OvEditor/Utils/ActorCreationMenu.h" +namespace +{ + bool IsPartOfPrefabInstance(const OvCore::ECS::Actor& p_actor) + { + const OvCore::ECS::Actor* current = &p_actor; + + while (current) + { + if (current->HasPrefabSource()) + { + return true; + } + + current = current->GetParent(); + } + + return false; + } +} + class ActorContextualMenu : public OvUI::Plugins::ContextualMenu { public: @@ -109,6 +132,21 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu EDITOR_EXEC(SaveActorAsPrefab(*m_target, dialog.GetSelectedFilePath())); }; + if (m_target->HasPrefabSource()) + { + auto& applyToPrefabButton = CreateWidget("Apply to Prefab"); + applyToPrefabButton.ClickedEvent += [this] + { + EDITOR_EXEC(ApplyActorToPrefab(*m_target)); + }; + + auto& revertToPrefabButton = CreateWidget("Revert to Prefab"); + revertToPrefabButton.ClickedEvent += [this] + { + EDITOR_EXEC(RevertActorToPrefab(*m_target)); + }; + } + auto& deleteButton = CreateWidget("Delete"); deleteButton.ClickedEvent += [this] { @@ -399,8 +437,10 @@ void OvEditor::Panels::Hierarchy::AddActorByInstance(OvCore::ECS::Actor & p_acto auto& textSelectable = m_actors.CreateWidget(p_actor.GetName(), true); textSelectable.leaf = true; - if (auto* actorTexture = EDITOR_CONTEXT(editorResources)->GetTexture("Actor")) - textSelectable.iconTextureID = actorTexture->GetTexture().GetID(); + const uint32_t actorIconID = OvCore::Helpers::GUIHelpers::GetActorIconID(); + const uint32_t prefabIconID = OvCore::Helpers::GUIHelpers::GetIconForFileType(OvTools::Utils::PathParser::EFileType::PREFAB); + + textSelectable.iconTextureID = IsPartOfPrefabInstance(p_actor) && prefabIconID != 0 ? prefabIconID : actorIconID; textSelectable.AddPlugin(&p_actor, &textSelectable); textSelectable.AddPlugin>>("Actor", "Attach to...", std::make_pair(&p_actor, &textSelectable)); @@ -422,13 +462,15 @@ void OvEditor::Panels::Hierarchy::AddActorByInstance(OvCore::ECS::Actor & p_acto auto& dispatcher = textSelectable.AddPlugin>(); OvCore::ECS::Actor* targetPtr = &p_actor; - dispatcher.RegisterGatherer([targetPtr, &textSelectable] + dispatcher.RegisterGatherer([targetPtr, &textSelectable, actorIconID, prefabIconID] { const bool isActive = targetPtr->IsActive(); textSelectable.labelColor = isActive ? OVUI_STYLE(Text) : OVUI_STYLE(TextDisabled); + textSelectable.iconTextureID = IsPartOfPrefabInstance(*targetPtr) && prefabIconID != 0 ? prefabIconID : actorIconID; + return targetPtr->GetName(); }); diff --git a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp index 1a695df5..e5d59da7 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -144,6 +145,40 @@ namespace { return Combine(std::bind(CreateCharacter, p_parent), p_onItemClicked); } + + std::function CreateFromPrefabHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + { + return [p_parent, p_onItemClicked]() + { + OvCore::Helpers::GUIHelpers::OpenAssetPicker( + OvTools::Utils::PathParser::EFileType::PREFAB, + [p_parent, p_onItemClicked](std::string p_prefabPath) + { + if (p_prefabPath.empty()) + { + return; + } + + if (auto* actor = EDITOR_EXEC(InstantiatePrefab(p_prefabPath)); actor) + { + if (p_parent) + { + actor->SetParent(*p_parent); + } + + EDITOR_EXEC(SelectActor(*actor)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + } + }, + true, + false + ); + }; + } } void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) @@ -152,6 +187,7 @@ void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets using namespace OvCore::ECS::Components; p_menuList.CreateWidget("Create Empty").ClickedEvent += Combine(EDITOR_BIND(CreateEmptyActor, true, p_parent, ""), p_onItemClicked); + p_menuList.CreateWidget("From prefab...").ClickedEvent += CreateFromPrefabHandler(p_parent, p_onItemClicked); auto& primitives = p_menuList.CreateWidget("Primitives"); auto& physicals = p_menuList.CreateWidget("Physicals"); From e0458016878af9e0d27d016b9d7d9fe9e69a049e Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 01:49:32 +0200 Subject: [PATCH 05/23] feat(icon) : Improve `Prefab` icon --- Resources/Editor/Textures/Prefab.png | Bin 437 -> 665 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Resources/Editor/Textures/Prefab.png b/Resources/Editor/Textures/Prefab.png index 8a328d1c06e7e581b5560c2a5ab450eb22cf09e0..521d21e01b473ce0f1988e6cde69d763663d7c69 100644 GIT binary patch delta 640 zcmV-`0)PFr1DOSoB!2;OQb$4nuFf3k00004XF*Lt006O%3;baP00009a7bBm000id z000id0mpBsWB>pHE=fc|R5(wKl1)ohQ51%swa@X~t|@6*VCW>2f^?A4C@P314hw1) z^&9#HIq6Rb4Q$XLDG_TJAr3nn9b)YPM(&u zLEwB@y{?d6g(2V;a6{pg>&bB6I`@oxEX~kbZksVlwMT*)v2hbUUp}VD%cp3GSucE`~>v{uoch@T%^P+mbIxm zOJfyE6E(t_m~_*jneNM};9^LBLv0?aUm#ji!cPgy<9}7kFKcWS64E~owR^^z9}3)d zjJO2hd*#=7mGE=C-IyNCc2Hm!1x*zf(=f4_!v+2mBQS3QHW6I&<)Ijy-)1&3%zNE; z9u`Gdz>tb?LX{&Z9)M;_Z7D`G1wq9~=C_-&f6Pn<$7Qso!%Pamxu@#qm3~`eNHgx) zI+r!2)p^z}t8X{4k;NR5DCqP)nw*a2PPV*{j>`AaNPE_>a~CJnw_Eht3WF&3lXW|| zJ{^k5Gl%dv=<*8r_VoLYM_uu5&w-^Kex1R5*>rl08eqP#A@u zo1~SblnN>+IEdDwo%|Raw2PB}z{yoyU7Q5Lt+RhY5J8GKbboOYRGf4WK{^CQ!8lY~ zs Date: Fri, 24 Apr 2026 02:55:13 +0200 Subject: [PATCH 06/23] Fix prefab context actions and nested prefab source remapping --- .../src/OvEditor/Core/EditorActions.cpp | 58 +++++++++++++++---- .../src/OvEditor/Panels/Hierarchy.cpp | 42 ++++++++++---- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 8206aa90..3fc40681 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -56,14 +56,39 @@ namespace { constexpr std::string_view kDefaultMaterialPath = ":Materials\\Default.ovmat"; - void SetPrefabSourceRecursively(OvCore::ECS::Actor& p_rootActor, const std::string& p_prefabSource) - { - p_rootActor.SetPrefabSource(p_prefabSource); + void RemapPrefabSourceRecursively( + OvCore::ECS::Actor& p_rootActor, + const std::string& p_previousRootPrefabSource, + const std::string& p_newRootPrefabSource, + bool p_isRoot = true) + { + const bool shouldRemapCurrent = + p_isRoot || + !p_rootActor.HasPrefabSource() || + p_rootActor.GetPrefabSource() == p_previousRootPrefabSource; + + if (shouldRemapCurrent) + { + p_rootActor.SetPrefabSource(p_newRootPrefabSource); + } for (auto* child : p_rootActor.GetChildren()) { - SetPrefabSourceRecursively(*child, p_prefabSource); + RemapPrefabSourceRecursively(*child, p_previousRootPrefabSource, p_newRootPrefabSource, false); + } + } + + OvCore::ECS::Actor& ResolvePrefabInstanceRoot(OvCore::ECS::Actor& p_actor) + { + auto* resolvedRoot = &p_actor; + const std::string& prefabSource = p_actor.GetPrefabSource(); + + while (resolvedRoot->HasParent() && resolvedRoot->GetParent()->GetPrefabSource() == prefabSource) + { + resolvedRoot = resolvedRoot->GetParent(); } + + return *resolvedRoot; } void RefreshMaterialsUsingShader( @@ -896,6 +921,8 @@ void OvEditor::Core::EditorActions::DuplicateActor(OvCore::ECS::Actor & p_toDupl void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_actor, const std::string& p_path) { + const std::string previousRootPrefabSource = p_actor.GetPrefabSource(); + if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, p_path)) { OVLOG_ERROR("Failed to save prefab to: " + p_path); @@ -903,7 +930,7 @@ void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_acto } const std::string prefabSourcePath = GetResourcePath(p_path); - SetPrefabSourceRecursively(p_actor, prefabSourcePath); + RemapPrefabSourceRecursively(p_actor, previousRootPrefabSource, prefabSourcePath); OVLOG_INFO("Prefab saved to: " + p_path); } @@ -928,7 +955,8 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: if (instantiatedRoot) { - SetPrefabSourceRecursively(*instantiatedRoot, prefabSourcePath); + const std::string previousRootPrefabSource = instantiatedRoot->GetPrefabSource(); + RemapPrefabSourceRecursively(*instantiatedRoot, previousRootPrefabSource, prefabSourcePath); OVLOG_INFO("Prefab instantiated: " + realPath.string()); } else @@ -947,15 +975,20 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act return false; } - const std::string realPath = GetRealPath(p_actor.GetPrefabSource()); + auto& prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); + const std::string realPath = GetRealPath(prefabInstanceRoot.GetPrefabSource()); - if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, realPath)) + if (!OvEditor::Utils::PrefabOperations::SaveToFile(prefabInstanceRoot, realPath)) { OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath); return false; } - OVLOG_INFO("Prefab updated from actor \"" + p_actor.GetName() + "\": " + realPath); + OVLOG_INFO( + "Prefab updated from actor \"" + p_actor.GetName() + + "\" (instance root: \"" + prefabInstanceRoot.GetName() + + "\"): " + realPath + ); return true; } @@ -967,8 +1000,9 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::E return nullptr; } - OvCore::ECS::Actor* parent = p_actor.GetParent(); - const auto prefabSourcePath = p_actor.GetPrefabSource(); + auto& prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); + OvCore::ECS::Actor* parent = prefabInstanceRoot.GetParent(); + const auto prefabSourcePath = prefabInstanceRoot.GetPrefabSource(); auto* instantiatedRoot = InstantiatePrefab(prefabSourcePath); if (!instantiatedRoot) @@ -981,7 +1015,7 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::E instantiatedRoot->SetParent(*parent); } - DestroyActor(p_actor); + DestroyActor(prefabInstanceRoot); SelectActor(*instantiatedRoot); return instantiatedRoot; diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index 4a18d48b..3aa7b069 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -132,20 +132,21 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu EDITOR_EXEC(SaveActorAsPrefab(*m_target, dialog.GetSelectedFilePath())); }; - if (m_target->HasPrefabSource()) + auto& applyToPrefabButton = CreateWidget("Apply to Prefab"); + m_applyToPrefabButton = &applyToPrefabButton; + applyToPrefabButton.enabled = m_target->HasPrefabSource(); + applyToPrefabButton.ClickedEvent += [this] { - auto& applyToPrefabButton = CreateWidget("Apply to Prefab"); - applyToPrefabButton.ClickedEvent += [this] - { - EDITOR_EXEC(ApplyActorToPrefab(*m_target)); - }; + EDITOR_EXEC(ApplyActorToPrefab(*m_target)); + }; - auto& revertToPrefabButton = CreateWidget("Revert to Prefab"); - revertToPrefabButton.ClickedEvent += [this] - { - EDITOR_EXEC(RevertActorToPrefab(*m_target)); - }; - } + auto& revertToPrefabButton = CreateWidget("Revert to Prefab"); + m_revertToPrefabButton = &revertToPrefabButton; + revertToPrefabButton.enabled = m_target->HasPrefabSource(); + revertToPrefabButton.ClickedEvent += [this] + { + EDITOR_EXEC(RevertActorToPrefab(*m_target)); + }; auto& deleteButton = CreateWidget("Delete"); deleteButton.ClickedEvent += [this] @@ -191,6 +192,21 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu virtual void Execute(OvUI::Plugins::EPluginExecutionContext p_context) override { + if (m_target) + { + const bool hasPrefabSource = m_target->HasPrefabSource(); + + if (m_applyToPrefabButton) + { + m_applyToPrefabButton->enabled = hasPrefabSource; + } + + if (m_revertToPrefabButton) + { + m_revertToPrefabButton->enabled = hasPrefabSource; + } + } + if (m_widgets.size() > 0) OvUI::Plugins::ContextualMenu::Execute(p_context); } @@ -198,6 +214,8 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu private: OvCore::ECS::Actor* m_target; OvUI::Widgets::Layout::TreeNode* m_treeNode; + OvUI::Widgets::Menu::MenuItem* m_applyToPrefabButton = nullptr; + OvUI::Widgets::Menu::MenuItem* m_revertToPrefabButton = nullptr; }; void ExpandTreeNode(OvUI::Widgets::Layout::TreeNode& p_toExpand) From 3fc9291c2cf93d0c012547ed37036f7a38fd26da Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 03:20:24 +0200 Subject: [PATCH 07/23] Harden hierarchy prefab menu target resolution --- .../src/OvEditor/Panels/Hierarchy.cpp | 112 ++++++++++++++---- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index 3aa7b069..1e211da3 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -7,6 +7,8 @@ #include "OvEditor/Panels/Hierarchy.h" #include "OvEditor/Core/EditorActions.h" +#include + #include #include #include @@ -43,7 +45,7 @@ namespace { - bool IsPartOfPrefabInstance(const OvCore::ECS::Actor& p_actor) + const OvCore::ECS::Actor* GetPrefabInstanceRoot(const OvCore::ECS::Actor& p_actor) { const OvCore::ECS::Actor* current = &p_actor; @@ -51,13 +53,18 @@ namespace { if (current->HasPrefabSource()) { - return true; + return current; } current = current->GetParent(); } - return false; + return nullptr; + } + + bool IsPartOfPrefabInstance(const OvCore::ECS::Actor& p_actor) + { + return GetPrefabInstanceRoot(p_actor) != nullptr; } } @@ -65,7 +72,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu { public: ActorContextualMenu(OvCore::ECS::Actor* p_target, OvUI::Widgets::Layout::TreeNode* p_treeNode = nullptr, bool p_panelMenu = false) : - m_target(p_target), + m_targetID(p_target ? p_target->GetGUID() : 0), m_treeNode(p_treeNode) { using namespace OvUI::Panels; @@ -73,37 +80,55 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu using namespace OvUI::Widgets::Menu; using namespace OvCore::ECS::Components; - if (m_target) + if (m_targetID != 0) { auto& focusButton = CreateWidget("Focus"); focusButton.ClickedEvent += [this] { - EDITOR_EXEC(MoveToTarget(*m_target)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(MoveToTarget(*target)); + } }; auto& copyButton = CreateWidget("Copy"); copyButton.ClickedEvent += [this] { - EDITOR_EXEC(CopyActor(*m_target)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(CopyActor(*target)); + } }; auto& duplicateButton = CreateWidget("Duplicate"); duplicateButton.ClickedEvent += [this] { - EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*m_target), nullptr, true), 0)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*target), nullptr, true), 0)); + } }; auto& pasteButton = CreateWidget("Paste"); pasteButton.ClickedEvent += [this] { - EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, m_target), 0)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, target), 0)); + } }; auto& saveAsPrefabButton = CreateWidget("Save as Prefab..."); saveAsPrefabButton.ClickedEvent += [this] { + auto* target = GetTargetActor(); + if (!target) + { + return; + } + OvWindowing::Dialogs::SaveFileDialog dialog("Save Prefab"); - const auto initialPath = EDITOR_CONTEXT(projectAssetsPath) / m_target->GetName(); + const auto initialPath = EDITOR_CONTEXT(projectAssetsPath) / target->GetName(); dialog.SetInitialDirectory(initialPath.string()); dialog.DefineExtension("Overload Prefab", ".ovprefab"); dialog.Show(); @@ -129,29 +154,36 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu } } - EDITOR_EXEC(SaveActorAsPrefab(*m_target, dialog.GetSelectedFilePath())); + EDITOR_EXEC(SaveActorAsPrefab(*target, dialog.GetSelectedFilePath())); }; auto& applyToPrefabButton = CreateWidget("Apply to Prefab"); m_applyToPrefabButton = &applyToPrefabButton; - applyToPrefabButton.enabled = m_target->HasPrefabSource(); applyToPrefabButton.ClickedEvent += [this] { - EDITOR_EXEC(ApplyActorToPrefab(*m_target)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(ApplyActorToPrefab(*target)); + } }; auto& revertToPrefabButton = CreateWidget("Revert to Prefab"); m_revertToPrefabButton = &revertToPrefabButton; - revertToPrefabButton.enabled = m_target->HasPrefabSource(); revertToPrefabButton.ClickedEvent += [this] { - EDITOR_EXEC(RevertActorToPrefab(*m_target)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(RevertActorToPrefab(*target)); + } }; auto& deleteButton = CreateWidget("Delete"); deleteButton.ClickedEvent += [this] { - EDITOR_EXEC(DestroyActor(std::ref(*m_target))); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DestroyActor(std::ref(*target))); + } }; auto& renameMenu = CreateWidget("Rename to..."); @@ -161,12 +193,18 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu renameMenu.ClickedEvent += [this, &nameEditor] { - nameEditor.content = m_target->GetName(); + if (auto* target = GetTargetActor()) + { + nameEditor.content = target->GetName(); + } }; nameEditor.EnterPressedEvent += [this](std::string p_newName) { - m_target->SetName(p_newName); + if (auto* target = GetTargetActor()) + { + target->SetName(p_newName); + } }; } else @@ -187,23 +225,23 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu ) : std::nullopt; - OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(createActor, m_target, onItemClicked); + OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(createActor, GetTargetActor(), onItemClicked); } virtual void Execute(OvUI::Plugins::EPluginExecutionContext p_context) override { - if (m_target) + if (m_applyToPrefabButton || m_revertToPrefabButton) { - const bool hasPrefabSource = m_target->HasPrefabSource(); + const bool canEditPrefab = CanEditPrefab(); if (m_applyToPrefabButton) { - m_applyToPrefabButton->enabled = hasPrefabSource; + m_applyToPrefabButton->enabled = canEditPrefab; } if (m_revertToPrefabButton) { - m_revertToPrefabButton->enabled = hasPrefabSource; + m_revertToPrefabButton->enabled = canEditPrefab; } } @@ -212,7 +250,33 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu } private: - OvCore::ECS::Actor* m_target; + OvCore::ECS::Actor* GetTargetActor() const + { + if (m_targetID == 0) + { + return nullptr; + } + + auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); + if (!currentScene) + { + return nullptr; + } + + return currentScene->FindActorByGUID(m_targetID); + } + + bool CanEditPrefab() const + { + if (auto* target = GetTargetActor()) + { + return GetPrefabInstanceRoot(*target) != nullptr; + } + + return false; + } + + uint64_t m_targetID; OvUI::Widgets::Layout::TreeNode* m_treeNode; OvUI::Widgets::Menu::MenuItem* m_applyToPrefabButton = nullptr; OvUI::Widgets::Menu::MenuItem* m_revertToPrefabButton = nullptr; From 06cb33ec3c234e2758194798a98f1953bda69093 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 03:20:31 +0200 Subject: [PATCH 08/23] Fix nested prefab source normalization and root resolution --- .../src/OvEditor/Core/EditorActions.cpp | 94 ++++++++++++------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 3fc40681..f58064fe 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -56,39 +56,71 @@ namespace { constexpr std::string_view kDefaultMaterialPath = ":Materials\\Default.ovmat"; - void RemapPrefabSourceRecursively( - OvCore::ECS::Actor& p_rootActor, - const std::string& p_previousRootPrefabSource, - const std::string& p_newRootPrefabSource, - bool p_isRoot = true) + void NormalizePrefabSourcesRecursively( + OvCore::ECS::Actor& p_actor, + const std::string& p_inheritedPrefabSource, + const std::string& p_previousRootPrefabSource) { - const bool shouldRemapCurrent = - p_isRoot || - !p_rootActor.HasPrefabSource() || - p_rootActor.GetPrefabSource() == p_previousRootPrefabSource; + if (p_actor.HasPrefabSource()) + { + const std::string currentPrefabSource = p_actor.GetPrefabSource(); + const bool hasRedundantSource = + currentPrefabSource == p_inheritedPrefabSource || + (!p_previousRootPrefabSource.empty() && currentPrefabSource == p_previousRootPrefabSource); + + if (hasRedundantSource) + { + p_actor.SetPrefabSource("?"); + } + } + + const std::string nextInheritedPrefabSource = + p_actor.HasPrefabSource() ? p_actor.GetPrefabSource() : p_inheritedPrefabSource; - if (shouldRemapCurrent) + for (auto* child : p_actor.GetChildren()) { - p_rootActor.SetPrefabSource(p_newRootPrefabSource); + NormalizePrefabSourcesRecursively(*child, nextInheritedPrefabSource, p_previousRootPrefabSource); } + } + + void SetRootPrefabSourceAndNormalizeChildren( + OvCore::ECS::Actor& p_rootActor, + const std::string& p_previousRootPrefabSource, + const std::string& p_newRootPrefabSource) + { + p_rootActor.SetPrefabSource(p_newRootPrefabSource); for (auto* child : p_rootActor.GetChildren()) { - RemapPrefabSourceRecursively(*child, p_previousRootPrefabSource, p_newRootPrefabSource, false); + NormalizePrefabSourcesRecursively(*child, p_newRootPrefabSource, p_previousRootPrefabSource); } } - OvCore::ECS::Actor& ResolvePrefabInstanceRoot(OvCore::ECS::Actor& p_actor) + OvCore::ECS::Actor* ResolvePrefabInstanceRoot(OvCore::ECS::Actor& p_actor) { auto* resolvedRoot = &p_actor; - const std::string& prefabSource = p_actor.GetPrefabSource(); - while (resolvedRoot->HasParent() && resolvedRoot->GetParent()->GetPrefabSource() == prefabSource) + while (resolvedRoot && !resolvedRoot->HasPrefabSource()) { resolvedRoot = resolvedRoot->GetParent(); } - return *resolvedRoot; + if (!resolvedRoot) + { + return (OVLOG_ERROR("Failed to resolve prefab instance root for actor \"" + p_actor.GetName() + "\": no prefab source found in actor hierarchy."), nullptr); + } + + const std::string prefabSource = resolvedRoot->GetPrefabSource(); + + // Keep compatibility with legacy scenes where every actor of an instance inherited the same source. + while (resolvedRoot->HasParent() && + resolvedRoot->GetParent()->HasPrefabSource() && + resolvedRoot->GetParent()->GetPrefabSource() == prefabSource) + { + resolvedRoot = resolvedRoot->GetParent(); + } + + return resolvedRoot; } void RefreshMaterialsUsingShader( @@ -930,7 +962,7 @@ void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_acto } const std::string prefabSourcePath = GetResourcePath(p_path); - RemapPrefabSourceRecursively(p_actor, previousRootPrefabSource, prefabSourcePath); + SetRootPrefabSourceAndNormalizeChildren(p_actor, previousRootPrefabSource, prefabSourcePath); OVLOG_INFO("Prefab saved to: " + p_path); } @@ -956,7 +988,7 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: if (instantiatedRoot) { const std::string previousRootPrefabSource = instantiatedRoot->GetPrefabSource(); - RemapPrefabSourceRecursively(*instantiatedRoot, previousRootPrefabSource, prefabSourcePath); + SetRootPrefabSourceAndNormalizeChildren(*instantiatedRoot, previousRootPrefabSource, prefabSourcePath); OVLOG_INFO("Prefab instantiated: " + realPath.string()); } else @@ -969,16 +1001,15 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_actor) { - if (!p_actor.HasPrefabSource()) + auto* prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); + if (!prefabInstanceRoot) { - OVLOG_WARNING("Cannot apply actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); - return false; + return (OVLOG_ERROR("Cannot apply actor \"" + p_actor.GetName() + "\" to prefab: no source instance."), false); } - auto& prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); - const std::string realPath = GetRealPath(prefabInstanceRoot.GetPrefabSource()); + const std::string realPath = GetRealPath(prefabInstanceRoot->GetPrefabSource()); - if (!OvEditor::Utils::PrefabOperations::SaveToFile(prefabInstanceRoot, realPath)) + if (!OvEditor::Utils::PrefabOperations::SaveToFile(*prefabInstanceRoot, realPath)) { OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath); return false; @@ -986,7 +1017,7 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act OVLOG_INFO( "Prefab updated from actor \"" + p_actor.GetName() + - "\" (instance root: \"" + prefabInstanceRoot.GetName() + + "\" (instance root: \"" + prefabInstanceRoot->GetName() + "\"): " + realPath ); return true; @@ -994,15 +1025,14 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act OvCore::ECS::Actor* OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_actor) { - if (!p_actor.HasPrefabSource()) + auto* prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); + if (!prefabInstanceRoot) { - OVLOG_WARNING("Cannot revert actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); - return nullptr; + return (OVLOG_ERROR("Cannot revert actor \"" + p_actor.GetName() + "\" to prefab: no source instance."), nullptr); } - auto& prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); - OvCore::ECS::Actor* parent = prefabInstanceRoot.GetParent(); - const auto prefabSourcePath = prefabInstanceRoot.GetPrefabSource(); + OvCore::ECS::Actor* parent = prefabInstanceRoot->GetParent(); + const auto prefabSourcePath = prefabInstanceRoot->GetPrefabSource(); auto* instantiatedRoot = InstantiatePrefab(prefabSourcePath); if (!instantiatedRoot) @@ -1015,7 +1045,7 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::E instantiatedRoot->SetParent(*parent); } - DestroyActor(prefabInstanceRoot); + DestroyActor(*prefabInstanceRoot); SelectActor(*instantiatedRoot); return instantiatedRoot; From d31b29f054775f6b0718672e99c07ba90d225ade Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 03:20:36 +0200 Subject: [PATCH 09/23] Inline prefab instantiation guard logging on early returns --- .../src/OvEditor/Utils/PrefabOperations.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp b/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp index a1aaa267..668335ef 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp @@ -5,11 +5,13 @@ */ #include +#include #include #include #include +#include #include #include @@ -54,15 +56,14 @@ OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( { if (!p_createActor) { - return nullptr; + return (OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": invalid actor factory callback."), nullptr); } tinyxml2::XMLDocument doc; - doc.LoadFile(p_prefabPath.string().c_str()); - - if (doc.Error()) + const auto loadResult = doc.LoadFile(p_prefabPath.string().c_str()); + if (loadResult != tinyxml2::XML_SUCCESS) { - return nullptr; + return (OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": XML parsing failed (code " + std::to_string(loadResult) + ")."), nullptr); } auto* rootNode = doc.FirstChildElement("root"); @@ -71,7 +72,7 @@ OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( if (!actorsNode) { - return nullptr; + return (OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": missing // node."), nullptr); } struct PendingAttachment From 8823405496b4aa64c27c019f68515a5db7c9da7a Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 03:26:05 +0200 Subject: [PATCH 10/23] Inline prefab guard log-and-return in editor actions --- Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index f58064fe..6804058d 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -957,8 +957,7 @@ void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_acto if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, p_path)) { - OVLOG_ERROR("Failed to save prefab to: " + p_path); - return; + return (OVLOG_ERROR("Failed to save prefab to: " + p_path), void()); } const std::string prefabSourcePath = GetResourcePath(p_path); @@ -1011,8 +1010,7 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act if (!OvEditor::Utils::PrefabOperations::SaveToFile(*prefabInstanceRoot, realPath)) { - OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath); - return false; + return (OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath), false); } OVLOG_INFO( From 655c1fb085a6ef317147b4986006772ac90de547 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 04:23:55 +0200 Subject: [PATCH 11/23] Adjust prefab open behavior and default save filename --- Sources/OvEditor/src/OvEditor/Core/Editor.cpp | 2 +- Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp index f33b8766..eb4b36bb 100644 --- a/Sources/OvEditor/src/OvEditor/Core/Editor.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/Editor.cpp @@ -131,7 +131,7 @@ void OvEditor::Core::Editor::SetupUI() { EDITOR_EXEC(LoadSceneFromDisk(path)); } - else if (fileType == EFileType::SCRIPT || fileType == EFileType::SHADER || fileType == EFileType::SHADER_PART || fileType == EFileType::PREFAB) + else if (fileType == EFileType::SCRIPT || fileType == EFileType::SHADER || fileType == EFileType::SHADER_PART) { EDITOR_EXEC(OpenInCodeEditor(m_editorActions.GetRealPath(path))); } diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index 1e211da3..1a18e673 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -128,8 +128,8 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu } OvWindowing::Dialogs::SaveFileDialog dialog("Save Prefab"); - const auto initialPath = EDITOR_CONTEXT(projectAssetsPath) / target->GetName(); - dialog.SetInitialDirectory(initialPath.string()); + dialog.SetInitialDirectory(EDITOR_CONTEXT(projectAssetsPath).string()); + dialog.SetInitialFilename(target->GetName()); dialog.DefineExtension("Overload Prefab", ".ovprefab"); dialog.Show(); From 65361e3a4bbab6f7d2707928b0ba11f57e373012 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 04:23:58 +0200 Subject: [PATCH 12/23] Improve prefab instance workflows and actor-to-folder prefab drop --- .../src/OvEditor/Core/EditorActions.cpp | 37 ++++++++++++------- .../src/OvEditor/Panels/AssetBrowser.cpp | 24 ++++++++++++ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 6804058d..acd3f832 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -69,9 +69,7 @@ namespace (!p_previousRootPrefabSource.empty() && currentPrefabSource == p_previousRootPrefabSource); if (hasRedundantSource) - { p_actor.SetPrefabSource("?"); - } } const std::string nextInheritedPrefabSource = @@ -107,12 +105,13 @@ namespace if (!resolvedRoot) { - return (OVLOG_ERROR("Failed to resolve prefab instance root for actor \"" + p_actor.GetName() + "\": no prefab source found in actor hierarchy."), nullptr); + OVLOG_ERROR("Failed to resolve prefab instance root for actor \"" + p_actor.GetName() + "\": no prefab source found in actor hierarchy."); + return nullptr; } const std::string prefabSource = resolvedRoot->GetPrefabSource(); - // Keep compatibility with legacy scenes where every actor of an instance inherited the same source. + // Handle scenes where ancestors may still share the same prefab source. while (resolvedRoot->HasParent() && resolvedRoot->GetParent()->HasPrefabSource() && resolvedRoot->GetParent()->GetPrefabSource() == prefabSource) @@ -957,7 +956,8 @@ void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_acto if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, p_path)) { - return (OVLOG_ERROR("Failed to save prefab to: " + p_path), void()); + OVLOG_ERROR("Failed to save prefab to: " + p_path); + return; } const std::string prefabSourcePath = GetResourcePath(p_path); @@ -969,9 +969,7 @@ void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_acto OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std::string& p_path) { if (!m_context.sceneManager.GetCurrentScene()) - { return nullptr; - } const std::filesystem::path realPath = GetRealPath(p_path); const std::string prefabSourcePath = GetResourcePath(realPath.string()); @@ -988,6 +986,11 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: { const std::string previousRootPrefabSource = instantiatedRoot->GetPrefabSource(); SetRootPrefabSourceAndNormalizeChildren(*instantiatedRoot, previousRootPrefabSource, prefabSourcePath); + + const std::string prefabName = realPath.stem().string(); + if (!prefabName.empty()) + instantiatedRoot->SetName(prefabName); + OVLOG_INFO("Prefab instantiated: " + realPath.string()); } else @@ -1003,14 +1006,16 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act auto* prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); if (!prefabInstanceRoot) { - return (OVLOG_ERROR("Cannot apply actor \"" + p_actor.GetName() + "\" to prefab: no source instance."), false); + OVLOG_ERROR("Cannot apply actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); + return false; } const std::string realPath = GetRealPath(prefabInstanceRoot->GetPrefabSource()); if (!OvEditor::Utils::PrefabOperations::SaveToFile(*prefabInstanceRoot, realPath)) { - return (OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath), false); + OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath); + return false; } OVLOG_INFO( @@ -1026,22 +1031,26 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::E auto* prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); if (!prefabInstanceRoot) { - return (OVLOG_ERROR("Cannot revert actor \"" + p_actor.GetName() + "\" to prefab: no source instance."), nullptr); + OVLOG_ERROR("Cannot revert actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); + return nullptr; } OvCore::ECS::Actor* parent = prefabInstanceRoot->GetParent(); + const auto previousLocalPosition = prefabInstanceRoot->transform.GetLocalPosition(); + const auto previousLocalRotation = prefabInstanceRoot->transform.GetLocalRotation(); + const auto previousLocalScale = prefabInstanceRoot->transform.GetLocalScale(); const auto prefabSourcePath = prefabInstanceRoot->GetPrefabSource(); auto* instantiatedRoot = InstantiatePrefab(prefabSourcePath); if (!instantiatedRoot) - { return nullptr; - } if (parent) - { instantiatedRoot->SetParent(*parent); - } + + instantiatedRoot->transform.SetLocalPosition(previousLocalPosition); + instantiatedRoot->transform.SetLocalRotation(previousLocalRotation); + instantiatedRoot->transform.SetLocalScale(previousLocalScale); DestroyActor(*prefabInstanceRoot); SelectActor(*instantiatedRoot); diff --git a/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp b/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp index ad775f7b..b98b1190 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/AssetBrowser.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -1155,6 +1156,29 @@ void OvEditor::Panels::AssetBrowser::ConsiderItem(OvUI::Widgets::Layout::TreeNod } } }; + + treeNode.AddPlugin>>("Actor").DataReceivedEvent += [this, &treeNode, path, p_isEngineItem](std::pair p_data) + { + if (!p_data.first) + { + return; + } + + const auto correctPath = m_pathUpdate.find(&treeNode) != m_pathUpdate.end() ? m_pathUpdate.at(&treeNode) : std::filesystem::path(path); + if (!ValidateFolderPath(correctPath, "Create prefab")) + { + return; + } + + const std::string actorName = p_data.first->GetName().empty() ? "Prefab" : p_data.first->GetName(); + const std::filesystem::path prefabPath = FindAvailableFilePath(correctPath / (actorName + ".ovprefab")); + + EDITOR_EXEC(SaveActorAsPrefab(*p_data.first, prefabPath.string())); + + treeNode.Open(); + treeNode.RemoveAllWidgets(); + ParseFolder(treeNode, std::filesystem::directory_entry(correctPath), p_isEngineItem); + }; } contextMenu.DestroyedEvent += [&itemGroup](const std::filesystem::path& p_deletedPath) { itemGroup.Destroy(); }; From 9b0f50896fd1c8ee888aedc5ad0df1c324c1b394 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 04:24:05 +0200 Subject: [PATCH 13/23] Refine prefab instantiation guard logging and compact single-line ifs --- .../src/OvEditor/Utils/PrefabOperations.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp b/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp index 668335ef..9b11ce74 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp @@ -56,14 +56,16 @@ OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( { if (!p_createActor) { - return (OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": invalid actor factory callback."), nullptr); + OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": invalid actor factory callback."); + return nullptr; } tinyxml2::XMLDocument doc; const auto loadResult = doc.LoadFile(p_prefabPath.string().c_str()); if (loadResult != tinyxml2::XML_SUCCESS) { - return (OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": XML parsing failed (code " + std::to_string(loadResult) + ")."), nullptr); + OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": XML parsing failed (code " + std::to_string(loadResult) + ")."); + return nullptr; } auto* rootNode = doc.FirstChildElement("root"); @@ -72,7 +74,8 @@ OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( if (!actorsNode) { - return (OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": missing // node."), nullptr); + OVLOG_ERROR("Failed to instantiate prefab \"" + p_prefabPath.string() + "\": missing // node."); + return nullptr; } struct PendingAttachment @@ -110,13 +113,9 @@ OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( for (auto& pending : pendingAttachments) { if (auto found = sourceToInstance.find(pending.sourceParentID); found != sourceToInstance.end()) - { pending.actor->SetParent(*found->second); - } else if (!instantiatedRoot) - { instantiatedRoot = pending.actor; - } } return instantiatedRoot; From e8aa20c5b8aaa29cef2c36d54981a2ae3325ceba Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 04:38:12 +0200 Subject: [PATCH 14/23] Fix MSVC PDB contention by enabling /FS in premake --- premake5.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/premake5.lua b/premake5.lua index db46e9c0..99ea0ee9 100644 --- a/premake5.lua +++ b/premake5.lua @@ -34,6 +34,7 @@ workspace "Overload" -- Set toolset based on operating system filter {"system:windows"} toolset("msc") + buildoptions { "/FS" } -- Prevent C1041 PDB write conflicts with parallel compilation filter {"system:linux"} toolset("clang") -- Use Clang on Linux (sol2 doesn't work well with GCC) filter {} From 6e47f8d40b2c928b7da05ba6f765e534298437df Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 19:05:38 +0200 Subject: [PATCH 15/23] fix(editor): keep prefab instance identity when reverting --- .../include/OvEditor/Core/EditorActions.h | 6 +- .../OvEditor/Utils/ActorCreationMenu.h | 7 +- .../src/OvEditor/Core/EditorActions.cpp | 247 +++++++++++++++--- .../src/OvEditor/Panels/Hierarchy.cpp | 15 +- .../src/OvEditor/Panels/Inspector.cpp | 9 +- .../src/OvEditor/Utils/ActorCreationMenu.cpp | 154 +++++++---- 6 files changed, 337 insertions(+), 101 deletions(-) diff --git a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h index a0b2c462..75936143 100644 --- a/Sources/OvEditor/include/OvEditor/Core/EditorActions.h +++ b/Sources/OvEditor/include/OvEditor/Core/EditorActions.h @@ -248,11 +248,11 @@ namespace OvEditor::Core /** * Revert an actor hierarchy from its prefab source. - * The actor instance is replaced by a fresh prefab instantiation. - * Returns the new root actor on success. + * Keeps existing actor GUIDs to preserve scene/script references. + * Returns true on success. * @param p_actor */ - OvCore::ECS::Actor* RevertActorToPrefab(OvCore::ECS::Actor& p_actor); + bool RevertActorToPrefab(OvCore::ECS::Actor& p_actor); #pragma endregion #pragma region ACTOR_MANIPULATION diff --git a/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h b/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h index d7f2702c..23e2d3e4 100644 --- a/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h +++ b/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h @@ -7,6 +7,7 @@ #pragma once #include +#include namespace OvUI::Widgets::Menu { @@ -26,6 +27,8 @@ namespace OvEditor::Utils class ActorCreationMenu { public: + using ActorParentProvider = std::function; + /** * Disabled constructor */ @@ -35,9 +38,9 @@ namespace OvEditor::Utils * Generates an actor creation menu under the given MenuList item. * Also handles custom additionnal OnClick callback * @param p_menuList - * @param p_parent + * @param p_parentProvider * @param p_onItemClicked */ - static void GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, OvCore::ECS::Actor* p_parent = nullptr, std::optional> p_onItemClicked = {}); + static void GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, ActorParentProvider p_parentProvider = {}, std::optional> p_onItemClicked = {}); }; } diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index acd3f832..92fcab7f 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -5,6 +5,7 @@ */ #include +#include #include "OvDebug/Assertion.h" #include "OvTools/Utils/OptRef.h" #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include @@ -58,18 +60,15 @@ namespace void NormalizePrefabSourcesRecursively( OvCore::ECS::Actor& p_actor, - const std::string& p_inheritedPrefabSource, - const std::string& p_previousRootPrefabSource) + const std::string& p_inheritedPrefabSource) { if (p_actor.HasPrefabSource()) { const std::string currentPrefabSource = p_actor.GetPrefabSource(); - const bool hasRedundantSource = - currentPrefabSource == p_inheritedPrefabSource || - (!p_previousRootPrefabSource.empty() && currentPrefabSource == p_previousRootPrefabSource); - - if (hasRedundantSource) + if (currentPrefabSource == p_inheritedPrefabSource) + { p_actor.SetPrefabSource("?"); + } } const std::string nextInheritedPrefabSource = @@ -77,20 +76,19 @@ namespace for (auto* child : p_actor.GetChildren()) { - NormalizePrefabSourcesRecursively(*child, nextInheritedPrefabSource, p_previousRootPrefabSource); + NormalizePrefabSourcesRecursively(*child, nextInheritedPrefabSource); } } void SetRootPrefabSourceAndNormalizeChildren( OvCore::ECS::Actor& p_rootActor, - const std::string& p_previousRootPrefabSource, const std::string& p_newRootPrefabSource) { p_rootActor.SetPrefabSource(p_newRootPrefabSource); for (auto* child : p_rootActor.GetChildren()) { - NormalizePrefabSourcesRecursively(*child, p_newRootPrefabSource, p_previousRootPrefabSource); + NormalizePrefabSourcesRecursively(*child, p_newRootPrefabSource); } } @@ -109,17 +107,163 @@ namespace return nullptr; } - const std::string prefabSource = resolvedRoot->GetPrefabSource(); + return resolvedRoot; + } - // Handle scenes where ancestors may still share the same prefab source. - while (resolvedRoot->HasParent() && - resolvedRoot->GetParent()->HasPrefabSource() && - resolvedRoot->GetParent()->GetPrefabSource() == prefabSource) + bool TryParseUInt64(const char* p_text, uint64_t& p_value) + { + if (!p_text) { - resolvedRoot = resolvedRoot->GetParent(); + return false; } - return resolvedRoot; + const std::string_view text{ p_text }; + const auto [parseEnd, error] = std::from_chars(text.data(), text.data() + text.size(), p_value); + return error == std::errc{} && parseEnd == text.data() + text.size(); + } + + void RemapActorReferenceGuidsInActorNode( + tinyxml2::XMLElement& p_actorNode, + const std::unordered_map& p_guidRemap) + { + auto* behavioursNode = p_actorNode.FirstChildElement("behaviours"); + + if (!behavioursNode) + { + return; + } + + for (auto* behaviourNode = behavioursNode->FirstChildElement("behaviour"); + behaviourNode; + behaviourNode = behaviourNode->NextSiblingElement("behaviour")) + { + auto* dataNode = behaviourNode->FirstChildElement("data"); + auto* scriptPropertiesNode = dataNode ? dataNode->FirstChildElement("script_properties") : nullptr; + + if (!scriptPropertiesNode) + { + continue; + } + + for (auto* propertyNode = scriptPropertiesNode->FirstChildElement(); + propertyNode; + propertyNode = propertyNode->NextSiblingElement()) + { + const char* typeAttribute = propertyNode->Attribute("type"); + + if (!typeAttribute || std::string_view{ typeAttribute } != "actor") + { + continue; + } + + uint64_t sourceGuid = 0; + + if (!TryParseUInt64(propertyNode->GetText(), sourceGuid)) + { + continue; + } + + if (const auto it = p_guidRemap.find(sourceGuid); it != p_guidRemap.end()) + { + propertyNode->SetText(std::to_string(it->second).c_str()); + } + } + } + } + + void BuildGuidRemapFromCommonHierarchy( + OvCore::ECS::Actor& p_targetActor, + OvCore::ECS::Actor& p_templateActor, + std::unordered_map& p_guidRemap) + { + p_guidRemap[p_templateActor.GetGUID()] = p_targetActor.GetGUID(); + + auto& targetChildren = p_targetActor.GetChildren(); + auto& templateChildren = p_templateActor.GetChildren(); + const size_t commonChildrenCount = std::min(targetChildren.size(), templateChildren.size()); + + for (size_t childIndex = 0; childIndex < commonChildrenCount; ++childIndex) + { + BuildGuidRemapFromCommonHierarchy(*targetChildren[childIndex], *templateChildren[childIndex], p_guidRemap); + } + } + + void SetActorNodeValue(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLElement& p_actorNode, const char* p_fieldName, const std::string& p_value) + { + auto* field = p_actorNode.FirstChildElement(p_fieldName); + + if (!field) + { + field = p_doc.NewElement(p_fieldName); + p_actorNode.InsertEndChild(field); + } + + field->SetText(p_value.c_str()); + } + + void RemoveActorComponentsAndBehaviours(OvCore::ECS::Actor& p_actor) + { + const auto components = p_actor.GetComponents(); + + for (const auto& component : components) + { + if (!dynamic_cast(component.get())) + { + p_actor.RemoveComponent(*component); + } + } + + const auto behaviourNames = p_actor.GetBehavioursOrder(); + + for (const auto& behaviourName : behaviourNames) + { + p_actor.RemoveBehaviour(behaviourName); + } + } + + void OverwriteActorFromPrefabTemplate( + OvCore::ECS::Actor& p_targetActor, + OvCore::ECS::Actor& p_templateActor, + const std::unordered_map& p_guidRemap, + bool p_preserveName, + bool p_preserveLocalTransform) + { + tinyxml2::XMLDocument doc; + auto* actorsNode = doc.NewElement("actors"); + doc.InsertFirstChild(actorsNode); + p_templateActor.OnSerialize(doc, actorsNode); + + auto* actorNode = actorsNode->FirstChildElement("actor"); + if (!actorNode) + { + return; + } + + RemapActorReferenceGuidsInActorNode(*actorNode, p_guidRemap); + + SetActorNodeValue(doc, *actorNode, "id", std::to_string(p_targetActor.GetID())); + SetActorNodeValue(doc, *actorNode, "guid", std::to_string(p_targetActor.GetGUID())); + SetActorNodeValue(doc, *actorNode, "parent", std::to_string(p_targetActor.GetParentID())); + + const std::string previousName = p_targetActor.GetName(); + const auto previousLocalPosition = p_targetActor.transform.GetLocalPosition(); + const auto previousLocalRotation = p_targetActor.transform.GetLocalRotation(); + const auto previousLocalScale = p_targetActor.transform.GetLocalScale(); + + RemoveActorComponentsAndBehaviours(p_targetActor); + p_targetActor.OnDeserialize(doc, actorNode); + + if (p_preserveName) + { + p_targetActor.SetName(previousName); + } + + if (p_preserveLocalTransform) + { + p_targetActor.transform.SetLocalPosition(previousLocalPosition); + p_targetActor.transform.SetLocalRotation(previousLocalRotation); + p_targetActor.transform.SetLocalScale(previousLocalScale); + } } void RefreshMaterialsUsingShader( @@ -952,8 +1096,6 @@ void OvEditor::Core::EditorActions::DuplicateActor(OvCore::ECS::Actor & p_toDupl void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_actor, const std::string& p_path) { - const std::string previousRootPrefabSource = p_actor.GetPrefabSource(); - if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, p_path)) { OVLOG_ERROR("Failed to save prefab to: " + p_path); @@ -961,7 +1103,7 @@ void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_acto } const std::string prefabSourcePath = GetResourcePath(p_path); - SetRootPrefabSourceAndNormalizeChildren(p_actor, previousRootPrefabSource, prefabSourcePath); + SetRootPrefabSourceAndNormalizeChildren(p_actor, prefabSourcePath); OVLOG_INFO("Prefab saved to: " + p_path); } @@ -984,8 +1126,7 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: if (instantiatedRoot) { - const std::string previousRootPrefabSource = instantiatedRoot->GetPrefabSource(); - SetRootPrefabSourceAndNormalizeChildren(*instantiatedRoot, previousRootPrefabSource, prefabSourcePath); + SetRootPrefabSourceAndNormalizeChildren(*instantiatedRoot, prefabSourcePath); const std::string prefabName = realPath.stem().string(); if (!prefabName.empty()) @@ -1026,36 +1167,64 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act return true; } -OvCore::ECS::Actor* OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_actor) +bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_actor) { auto* prefabInstanceRoot = ResolvePrefabInstanceRoot(p_actor); if (!prefabInstanceRoot) { OVLOG_ERROR("Cannot revert actor \"" + p_actor.GetName() + "\" to prefab: no source instance."); - return nullptr; + return false; } - OvCore::ECS::Actor* parent = prefabInstanceRoot->GetParent(); - const auto previousLocalPosition = prefabInstanceRoot->transform.GetLocalPosition(); - const auto previousLocalRotation = prefabInstanceRoot->transform.GetLocalRotation(); - const auto previousLocalScale = prefabInstanceRoot->transform.GetLocalScale(); const auto prefabSourcePath = prefabInstanceRoot->GetPrefabSource(); - auto* instantiatedRoot = InstantiatePrefab(prefabSourcePath); - if (!instantiatedRoot) - return nullptr; + auto* prefabTemplateRoot = InstantiatePrefab(prefabSourcePath); + if (!prefabTemplateRoot) + { + return false; + } - if (parent) - instantiatedRoot->SetParent(*parent); + std::unordered_map prefabToInstanceGuidMap; + BuildGuidRemapFromCommonHierarchy(*prefabInstanceRoot, *prefabTemplateRoot, prefabToInstanceGuidMap); - instantiatedRoot->transform.SetLocalPosition(previousLocalPosition); - instantiatedRoot->transform.SetLocalRotation(previousLocalRotation); - instantiatedRoot->transform.SetLocalScale(previousLocalScale); + std::function syncActorHierarchy; + syncActorHierarchy = [this, &syncActorHierarchy, &prefabToInstanceGuidMap](OvCore::ECS::Actor& p_targetActor, OvCore::ECS::Actor& p_templateActor, bool p_isRoot) + { + OverwriteActorFromPrefabTemplate( + p_targetActor, + p_templateActor, + prefabToInstanceGuidMap, + p_isRoot, /* keep root name */ + p_isRoot /* keep root local transform */ + ); - DestroyActor(*prefabInstanceRoot); - SelectActor(*instantiatedRoot); + auto& targetChildren = p_targetActor.GetChildren(); + auto& templateChildren = p_templateActor.GetChildren(); + const size_t commonChildrenCount = std::min(targetChildren.size(), templateChildren.size()); - return instantiatedRoot; + for (size_t childIndex = 0; childIndex < commonChildrenCount; ++childIndex) + { + syncActorHierarchy(*targetChildren[childIndex], *templateChildren[childIndex], false); + } + + for (size_t childIndex = targetChildren.size(); childIndex > commonChildrenCount; --childIndex) + { + targetChildren[childIndex - 1]->MarkAsDestroy(); + } + + for (size_t childIndex = commonChildrenCount; childIndex < templateChildren.size(); ++childIndex) + { + DuplicateActor(*templateChildren[childIndex], &p_targetActor, false); + } + }; + + syncActorHierarchy(*prefabInstanceRoot, *prefabTemplateRoot, true); + + prefabTemplateRoot->MarkAsDestroy(); + SelectActor(*prefabInstanceRoot); + + OVLOG_INFO("Prefab reverted on actor \"" + prefabInstanceRoot->GetName() + "\": " + GetRealPath(prefabSourcePath)); + return true; } void OvEditor::Core::EditorActions::CopyActor(OvCore::ECS::Actor& p_actor) diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index 1a18e673..128658e1 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -225,7 +225,20 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu ) : std::nullopt; - OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(createActor, GetTargetActor(), onItemClicked); + OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu( + createActor, + [targetGUID = m_targetID]() -> OvCore::ECS::Actor* + { + if (targetGUID == 0) + { + return nullptr; + } + + const auto scene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); + return scene ? scene->FindActorByGUID(targetGUID) : nullptr; + }, + onItemClicked + ); } virtual void Execute(OvUI::Plugins::EPluginExecutionContext p_context) override diff --git a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp index 1619ee72..09273b61 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Inspector.cpp @@ -216,7 +216,14 @@ void OvEditor::Panels::Inspector::_PopulateActorInfo() [this] { return std::format("{:016X}", m_targetActor->GetGUID()); } ); - _DrawAddSection(); + auto& prefabSourceField = OvCore::Helpers::GUIDrawer::DrawAsset( + headerColumns, + "Prefab Source", + [this] { return m_targetActor->GetPrefabSource(); }, + [](const std::string&) {}, + OvTools::Utils::PathParser::EFileType::PREFAB + ); + prefabSourceField.disabled = true; } void OvEditor::Panels::Inspector::_PopulateActorComponents() diff --git a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp index e5d59da7..5a7bd3c3 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -24,23 +24,18 @@ #include #include +#include + #include #include namespace { - std::function Combine(std::function p_a, std::optional> p_b) - { - if (p_b.has_value()) - { - return [=]() - { - p_a(); - p_b.value()(); - }; - } + using ActorParentProvider = OvEditor::Utils::ActorCreationMenu::ActorParentProvider; - return p_a; + OvCore::ECS::Actor* ResolveParent(const ActorParentProvider& p_parentProvider) + { + return p_parentProvider ? p_parentProvider() : nullptr; } void CreateSkysphere(OvCore::ECS::Actor* p_parent) @@ -121,38 +116,78 @@ namespace template - std::function ActorWithComponentCreationHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + std::function ActorWithComponentCreationHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) { - return Combine(EDITOR_BIND(CreateMonoComponentActor, true, p_parent), p_onItemClicked); + return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() + { + EDITOR_EXEC(CreateMonoComponentActor(true, ResolveParent(p_parentProvider))); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } - std::function ActorWithModelComponentCreationHandler(OvCore::ECS::Actor* p_parent, const std::string& p_modelName, std::optional> p_onItemClicked) + std::function ActorWithModelComponentCreationHandler(ActorParentProvider p_parentProvider, const std::string& p_modelName, std::optional> p_onItemClicked) { - return Combine(EDITOR_BIND(CreateActorWithModel, ":Models\\" + p_modelName + ".fbx", true, p_parent, p_modelName), p_onItemClicked); + return [p_parentProvider = std::move(p_parentProvider), p_modelName, p_onItemClicked]() + { + EDITOR_EXEC(CreateActorWithModel(":Models\\" + p_modelName + ".fbx", true, ResolveParent(p_parentProvider), p_modelName)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } - std::function CreateSkysphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + std::function CreateSkysphereHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateSkysphere, p_parent), p_onItemClicked); + return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() + { + CreateSkysphere(ResolveParent(p_parentProvider)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } - std::function CreateAtmosphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + std::function CreateAtmosphereHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateAtmosphere, p_parent), p_onItemClicked); + return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() + { + CreateAtmosphere(ResolveParent(p_parentProvider)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } - std::function CreateCharacterHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + std::function CreateCharacterHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateCharacter, p_parent), p_onItemClicked); + return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() + { + CreateCharacter(ResolveParent(p_parentProvider)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } - std::function CreateFromPrefabHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) + std::function CreateFromPrefabHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) { - return [p_parent, p_onItemClicked]() + return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() { OvCore::Helpers::GUIHelpers::OpenAssetPicker( OvTools::Utils::PathParser::EFileType::PREFAB, - [p_parent, p_onItemClicked](std::string p_prefabPath) + [p_parentProvider, p_onItemClicked](std::string p_prefabPath) { if (p_prefabPath.empty()) { @@ -161,9 +196,9 @@ namespace if (auto* actor = EDITOR_EXEC(InstantiatePrefab(p_prefabPath)); actor) { - if (p_parent) + if (auto* parent = ResolveParent(p_parentProvider); parent) { - actor->SetParent(*p_parent); + actor->SetParent(*parent); } EDITOR_EXEC(SelectActor(*actor)); @@ -181,13 +216,22 @@ namespace } } -void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) +void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) { using namespace OvUI::Widgets::Menu; using namespace OvCore::ECS::Components; - p_menuList.CreateWidget("Create Empty").ClickedEvent += Combine(EDITOR_BIND(CreateEmptyActor, true, p_parent, ""), p_onItemClicked); - p_menuList.CreateWidget("From prefab...").ClickedEvent += CreateFromPrefabHandler(p_parent, p_onItemClicked); + p_menuList.CreateWidget("Create Empty").ClickedEvent += [p_parentProvider, p_onItemClicked]() + { + EDITOR_EXEC(CreateEmptyActor(true, ResolveParent(p_parentProvider), "")); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; + + p_menuList.CreateWidget("From prefab...").ClickedEvent += CreateFromPrefabHandler(p_parentProvider, p_onItemClicked); auto& primitives = p_menuList.CreateWidget("Primitives"); auto& physicals = p_menuList.CreateWidget("Physicals"); @@ -195,30 +239,30 @@ void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets auto& audio = p_menuList.CreateWidget("Audio"); auto& others = p_menuList.CreateWidget("Others"); - primitives.CreateWidget("Cube").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Cube", p_onItemClicked); - primitives.CreateWidget("Sphere").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Sphere", p_onItemClicked); - primitives.CreateWidget("Cone").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Cone", p_onItemClicked); - primitives.CreateWidget("Cylinder").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Cylinder", p_onItemClicked); - primitives.CreateWidget("Plane").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Plane", p_onItemClicked); - primitives.CreateWidget("Gear").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Gear", p_onItemClicked); - primitives.CreateWidget("Helix").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Helix", p_onItemClicked); - primitives.CreateWidget("Pipe").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Pipe", p_onItemClicked); - primitives.CreateWidget("Pyramid").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Pyramid", p_onItemClicked); - primitives.CreateWidget("Torus").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Torus", p_onItemClicked); - physicals.CreateWidget("Physical Box").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - physicals.CreateWidget("Physical Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - physicals.CreateWidget("Physical Capsule").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - lights.CreateWidget("Point").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - lights.CreateWidget("Directional").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - lights.CreateWidget("Spot").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - lights.CreateWidget("Ambient Box").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - lights.CreateWidget("Ambient Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - audio.CreateWidget("Audio Source").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - audio.CreateWidget("Audio Listener").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - others.CreateWidget("Camera").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - others.CreateWidget("Post Process Stack").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - others.CreateWidget("Reflection Probe").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); - others.CreateWidget("Skysphere").ClickedEvent += CreateSkysphereHandler(p_parent, p_onItemClicked); - others.CreateWidget("Atmosphere").ClickedEvent += CreateAtmosphereHandler(p_parent, p_onItemClicked); - others.CreateWidget("Character").ClickedEvent += CreateCharacterHandler(p_parent, p_onItemClicked); + primitives.CreateWidget("Cube").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cube", p_onItemClicked); + primitives.CreateWidget("Sphere").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Sphere", p_onItemClicked); + primitives.CreateWidget("Cone").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cone", p_onItemClicked); + primitives.CreateWidget("Cylinder").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cylinder", p_onItemClicked); + primitives.CreateWidget("Plane").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Plane", p_onItemClicked); + primitives.CreateWidget("Gear").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Gear", p_onItemClicked); + primitives.CreateWidget("Helix").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Helix", p_onItemClicked); + primitives.CreateWidget("Pipe").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Pipe", p_onItemClicked); + primitives.CreateWidget("Pyramid").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Pyramid", p_onItemClicked); + primitives.CreateWidget("Torus").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Torus", p_onItemClicked); + physicals.CreateWidget("Physical Box").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + physicals.CreateWidget("Physical Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + physicals.CreateWidget("Physical Capsule").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + lights.CreateWidget("Point").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + lights.CreateWidget("Directional").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + lights.CreateWidget("Spot").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + lights.CreateWidget("Ambient Box").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + lights.CreateWidget("Ambient Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + audio.CreateWidget("Audio Source").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + audio.CreateWidget("Audio Listener").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + others.CreateWidget("Camera").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + others.CreateWidget("Post Process Stack").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + others.CreateWidget("Reflection Probe").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); + others.CreateWidget("Skysphere").ClickedEvent += CreateSkysphereHandler(p_parentProvider, p_onItemClicked); + others.CreateWidget("Atmosphere").ClickedEvent += CreateAtmosphereHandler(p_parentProvider, p_onItemClicked); + others.CreateWidget("Character").ClickedEvent += CreateCharacterHandler(p_parentProvider, p_onItemClicked); } From fb1ee23c1590602deb50636dbc8c170d0f03c7d8 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 19:07:36 +0200 Subject: [PATCH 16/23] refactor(core): move prefab operations out of editor module --- .../include/OvCore/SceneSystem}/PrefabOperations.h | 3 +-- .../src/OvCore/SceneSystem}/PrefabOperations.cpp | 11 +++++++---- Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) rename Sources/{OvEditor/include/OvEditor/Utils => OvCore/include/OvCore/SceneSystem}/PrefabOperations.h (96%) rename Sources/{OvEditor/src/OvEditor/Utils => OvCore/src/OvCore/SceneSystem}/PrefabOperations.cpp (91%) diff --git a/Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h b/Sources/OvCore/include/OvCore/SceneSystem/PrefabOperations.h similarity index 96% rename from Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h rename to Sources/OvCore/include/OvCore/SceneSystem/PrefabOperations.h index fb39655e..d8b99f2b 100644 --- a/Sources/OvEditor/include/OvEditor/Utils/PrefabOperations.h +++ b/Sources/OvCore/include/OvCore/SceneSystem/PrefabOperations.h @@ -14,7 +14,7 @@ namespace OvCore::ECS class Actor; } -namespace OvEditor::Utils +namespace OvCore::SceneSystem { /** * Utility functions to serialize and instantiate prefab files. @@ -45,4 +45,3 @@ namespace OvEditor::Utils ); }; } - diff --git a/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp b/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp similarity index 91% rename from Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp rename to Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp index 9b11ce74..b0755230 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/PrefabOperations.cpp +++ b/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp @@ -13,8 +13,7 @@ #include #include - -#include +#include namespace { @@ -32,7 +31,7 @@ namespace } } -bool OvEditor::Utils::PrefabOperations::SaveToFile(OvCore::ECS::Actor& p_rootActor, const std::filesystem::path& p_outputPath) +bool OvCore::SceneSystem::PrefabOperations::SaveToFile(OvCore::ECS::Actor& p_rootActor, const std::filesystem::path& p_outputPath) { tinyxml2::XMLDocument doc; @@ -50,7 +49,7 @@ bool OvEditor::Utils::PrefabOperations::SaveToFile(OvCore::ECS::Actor& p_rootAct return doc.SaveFile(p_outputPath.string().c_str()) == tinyxml2::XML_SUCCESS; } -OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( +OvCore::ECS::Actor* OvCore::SceneSystem::PrefabOperations::InstantiateFromFile( const std::filesystem::path& p_prefabPath, const std::function& p_createActor) { @@ -113,9 +112,13 @@ OvCore::ECS::Actor* OvEditor::Utils::PrefabOperations::InstantiateFromFile( for (auto& pending : pendingAttachments) { if (auto found = sourceToInstance.find(pending.sourceParentID); found != sourceToInstance.end()) + { pending.actor->SetParent(*found->second); + } else if (!instantiatedRoot) + { instantiatedRoot = pending.actor; + } } return instantiatedRoot; diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 92fcab7f..4ad6e2f3 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -42,7 +42,7 @@ #include #include #include -#include +#include #include #include @@ -1096,7 +1096,7 @@ void OvEditor::Core::EditorActions::DuplicateActor(OvCore::ECS::Actor & p_toDupl void OvEditor::Core::EditorActions::SaveActorAsPrefab(OvCore::ECS::Actor& p_actor, const std::string& p_path) { - if (!OvEditor::Utils::PrefabOperations::SaveToFile(p_actor, p_path)) + if (!OvCore::SceneSystem::PrefabOperations::SaveToFile(p_actor, p_path)) { OVLOG_ERROR("Failed to save prefab to: " + p_path); return; @@ -1116,7 +1116,7 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: const std::filesystem::path realPath = GetRealPath(p_path); const std::string prefabSourcePath = GetResourcePath(realPath.string()); - auto* instantiatedRoot = OvEditor::Utils::PrefabOperations::InstantiateFromFile( + auto* instantiatedRoot = OvCore::SceneSystem::PrefabOperations::InstantiateFromFile( realPath, [this]() -> OvCore::ECS::Actor& { @@ -1153,7 +1153,7 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act const std::string realPath = GetRealPath(prefabInstanceRoot->GetPrefabSource()); - if (!OvEditor::Utils::PrefabOperations::SaveToFile(*prefabInstanceRoot, realPath)) + if (!OvCore::SceneSystem::PrefabOperations::SaveToFile(*prefabInstanceRoot, realPath)) { OVLOG_ERROR("Failed to apply actor \"" + p_actor.GetName() + "\" to prefab: " + realPath); return false; From d05bfd116fee28a0fcb5e67cc4b726c17c84319e Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Fri, 24 Apr 2026 22:48:17 +0200 Subject: [PATCH 17/23] fix(editor): restore contextual actions after scene reload --- .../src/OvEditor/Core/EditorActions.cpp | 107 ++++++++++++++++-- .../src/OvEditor/Panels/Hierarchy.cpp | 26 ++--- 2 files changed, 108 insertions(+), 25 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 4ad6e2f3..6344a3a3 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -171,6 +171,89 @@ namespace } } + struct ChildMatchPlan + { + std::vector> matchedChildren; + std::vector unmatchedTargetChildren; + std::vector unmatchedTemplateChildren; + }; + + ChildMatchPlan BuildChildMatchPlan(OvCore::ECS::Actor& p_targetActor, OvCore::ECS::Actor& p_templateActor) + { + auto& targetChildren = p_targetActor.GetChildren(); + auto& templateChildren = p_templateActor.GetChildren(); + + ChildMatchPlan matchPlan; + std::vector usedTargetChildren(targetChildren.size(), false); + std::vector usedTemplateChildren(templateChildren.size(), false); + + // Prefer name-based matching to keep GUID identity stable when siblings were reordered. + for (size_t templateChildIndex = 0; templateChildIndex < templateChildren.size(); ++templateChildIndex) + { + const std::string& templateChildName = templateChildren[templateChildIndex]->GetName(); + + for (size_t targetChildIndex = 0; targetChildIndex < targetChildren.size(); ++targetChildIndex) + { + if (usedTargetChildren[targetChildIndex]) + { + continue; + } + + if (targetChildren[targetChildIndex]->GetName() == templateChildName) + { + matchPlan.matchedChildren.emplace_back(targetChildIndex, templateChildIndex); + usedTargetChildren[targetChildIndex] = true; + usedTemplateChildren[templateChildIndex] = true; + break; + } + } + } + + // Fallback to positional matching for unnamed/duplicate-name leftovers. + size_t nextUnmatchedTargetChild = 0; + + for (size_t templateChildIndex = 0; templateChildIndex < templateChildren.size(); ++templateChildIndex) + { + if (usedTemplateChildren[templateChildIndex]) + { + continue; + } + + while (nextUnmatchedTargetChild < targetChildren.size() && usedTargetChildren[nextUnmatchedTargetChild]) + { + ++nextUnmatchedTargetChild; + } + + if (nextUnmatchedTargetChild >= targetChildren.size()) + { + break; + } + + matchPlan.matchedChildren.emplace_back(nextUnmatchedTargetChild, templateChildIndex); + usedTargetChildren[nextUnmatchedTargetChild] = true; + usedTemplateChildren[templateChildIndex] = true; + ++nextUnmatchedTargetChild; + } + + for (size_t targetChildIndex = 0; targetChildIndex < targetChildren.size(); ++targetChildIndex) + { + if (!usedTargetChildren[targetChildIndex]) + { + matchPlan.unmatchedTargetChildren.push_back(targetChildIndex); + } + } + + for (size_t templateChildIndex = 0; templateChildIndex < templateChildren.size(); ++templateChildIndex) + { + if (!usedTemplateChildren[templateChildIndex]) + { + matchPlan.unmatchedTemplateChildren.push_back(templateChildIndex); + } + } + + return matchPlan; + } + void BuildGuidRemapFromCommonHierarchy( OvCore::ECS::Actor& p_targetActor, OvCore::ECS::Actor& p_templateActor, @@ -180,11 +263,15 @@ namespace auto& targetChildren = p_targetActor.GetChildren(); auto& templateChildren = p_templateActor.GetChildren(); - const size_t commonChildrenCount = std::min(targetChildren.size(), templateChildren.size()); + const auto matchPlan = BuildChildMatchPlan(p_targetActor, p_templateActor); - for (size_t childIndex = 0; childIndex < commonChildrenCount; ++childIndex) + for (const auto& [targetChildIndex, templateChildIndex] : matchPlan.matchedChildren) { - BuildGuidRemapFromCommonHierarchy(*targetChildren[childIndex], *templateChildren[childIndex], p_guidRemap); + BuildGuidRemapFromCommonHierarchy( + *targetChildren[targetChildIndex], + *templateChildren[templateChildIndex], + p_guidRemap + ); } } @@ -1200,21 +1287,21 @@ bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_ac auto& targetChildren = p_targetActor.GetChildren(); auto& templateChildren = p_templateActor.GetChildren(); - const size_t commonChildrenCount = std::min(targetChildren.size(), templateChildren.size()); + const auto matchPlan = BuildChildMatchPlan(p_targetActor, p_templateActor); - for (size_t childIndex = 0; childIndex < commonChildrenCount; ++childIndex) + for (const auto& [targetChildIndex, templateChildIndex] : matchPlan.matchedChildren) { - syncActorHierarchy(*targetChildren[childIndex], *templateChildren[childIndex], false); + syncActorHierarchy(*targetChildren[targetChildIndex], *templateChildren[templateChildIndex], false); } - for (size_t childIndex = targetChildren.size(); childIndex > commonChildrenCount; --childIndex) + for (const size_t targetChildIndex : matchPlan.unmatchedTargetChildren) { - targetChildren[childIndex - 1]->MarkAsDestroy(); + targetChildren[targetChildIndex]->MarkAsDestroy(); } - for (size_t childIndex = commonChildrenCount; childIndex < templateChildren.size(); ++childIndex) + for (const size_t templateChildIndex : matchPlan.unmatchedTemplateChildren) { - DuplicateActor(*templateChildren[childIndex], &p_targetActor, false); + DuplicateActor(*templateChildren[templateChildIndex], &p_targetActor, false); } }; diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index 128658e1..a824343c 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -7,6 +7,7 @@ #include "OvEditor/Panels/Hierarchy.h" #include "OvEditor/Core/EditorActions.h" +#include #include #include @@ -72,7 +73,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu { public: ActorContextualMenu(OvCore::ECS::Actor* p_target, OvUI::Widgets::Layout::TreeNode* p_treeNode = nullptr, bool p_panelMenu = false) : - m_targetID(p_target ? p_target->GetGUID() : 0), + m_targetActor(p_target), m_treeNode(p_treeNode) { using namespace OvUI::Panels; @@ -80,7 +81,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu using namespace OvUI::Widgets::Menu; using namespace OvCore::ECS::Components; - if (m_targetID != 0) + if (m_targetActor) { auto& focusButton = CreateWidget("Focus"); focusButton.ClickedEvent += [this] @@ -227,16 +228,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu( createActor, - [targetGUID = m_targetID]() -> OvCore::ECS::Actor* - { - if (targetGUID == 0) - { - return nullptr; - } - - const auto scene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); - return scene ? scene->FindActorByGUID(targetGUID) : nullptr; - }, + [this]() { return GetTargetActor(); }, onItemClicked ); } @@ -265,7 +257,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu private: OvCore::ECS::Actor* GetTargetActor() const { - if (m_targetID == 0) + if (!m_targetActor) { return nullptr; } @@ -276,7 +268,11 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu return nullptr; } - return currentScene->FindActorByGUID(m_targetID); + const auto& actors = currentScene->GetActors(); + const bool actorIsStillInScene = + std::find(actors.begin(), actors.end(), m_targetActor) != actors.end(); + + return actorIsStillInScene ? m_targetActor : nullptr; } bool CanEditPrefab() const @@ -289,7 +285,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu return false; } - uint64_t m_targetID; + OvCore::ECS::Actor* m_targetActor = nullptr; OvUI::Widgets::Layout::TreeNode* m_treeNode; OvUI::Widgets::Menu::MenuItem* m_applyToPrefabButton = nullptr; OvUI::Widgets::Menu::MenuItem* m_revertToPrefabButton = nullptr; From 49d739d449942c108501ae05a77afd1a7ec17334 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 30 Apr 2026 21:25:00 +0200 Subject: [PATCH 18/23] feat(core): add prefab node GUID metadata for actor instances --- Sources/OvCore/include/OvCore/ECS/Actor.h | 19 +++++++++++++++++++ Sources/OvCore/src/OvCore/ECS/Actor.cpp | 19 +++++++++++++++++++ .../OvCore/SceneSystem/PrefabOperations.cpp | 7 +++++++ 3 files changed, 45 insertions(+) diff --git a/Sources/OvCore/include/OvCore/ECS/Actor.h b/Sources/OvCore/include/OvCore/ECS/Actor.h index 77562beb..018fb94d 100644 --- a/Sources/OvCore/include/OvCore/ECS/Actor.h +++ b/Sources/OvCore/include/OvCore/ECS/Actor.h @@ -120,6 +120,24 @@ namespace OvCore::ECS */ bool HasPrefabSource() const; + /** + * Defines the prefab node GUID for this actor. + * This GUID identifies which prefab node this actor instance comes from. + * @param p_prefabNodeGUID + */ + void SetPrefabNodeGUID(uint64_t p_prefabNodeGUID); + + /** + * Returns the prefab node GUID for this actor. + * A value of 0 means no prefab node GUID is assigned. + */ + uint64_t GetPrefabNodeGUID() const; + + /** + * Returns true if this actor has a prefab node GUID assigned. + */ + bool HasPrefabNodeGUID() const; + /** * Set an actor as the parent of this actor * @param p_parent @@ -387,6 +405,7 @@ namespace OvCore::ECS int64_t m_actorID; uint64_t m_guid; std::string m_prefabSource; + uint64_t m_prefabNodeGUID = 0; bool m_destroyed = false; bool m_sleeping = true; bool m_awaked = false; diff --git a/Sources/OvCore/src/OvCore/ECS/Actor.cpp b/Sources/OvCore/src/OvCore/ECS/Actor.cpp index 5d1ee1f5..f62aae69 100644 --- a/Sources/OvCore/src/OvCore/ECS/Actor.cpp +++ b/Sources/OvCore/src/OvCore/ECS/Actor.cpp @@ -162,6 +162,21 @@ bool OvCore::ECS::Actor::HasPrefabSource() const return !m_prefabSource.empty(); } +void OvCore::ECS::Actor::SetPrefabNodeGUID(uint64_t p_prefabNodeGUID) +{ + m_prefabNodeGUID = p_prefabNodeGUID; +} + +uint64_t OvCore::ECS::Actor::GetPrefabNodeGUID() const +{ + return m_prefabNodeGUID; +} + +bool OvCore::ECS::Actor::HasPrefabNodeGUID() const +{ + return m_prefabNodeGUID != 0; +} + void OvCore::ECS::Actor::SetParent(Actor& p_parent) { DetachFromParent(); @@ -474,6 +489,7 @@ void OvCore::ECS::Actor::OnSerialize(tinyxml2::XMLDocument & p_doc, tinyxml2::XM OvCore::Helpers::Serializer::SerializeInt64(p_doc, actorNode, "id", m_actorID); OvCore::Helpers::Serializer::SerializeUInt64(p_doc, actorNode, "guid", m_guid); OvCore::Helpers::Serializer::SerializeString(p_doc, actorNode, "prefab_source", m_prefabSource); + OvCore::Helpers::Serializer::SerializeUInt64(p_doc, actorNode, "prefab_node_guid", m_prefabNodeGUID); OvCore::Helpers::Serializer::SerializeInt64(p_doc, actorNode, "parent", m_parentID); tinyxml2::XMLNode* componentsNode = p_doc.NewElement("components"); @@ -531,6 +547,9 @@ void OvCore::ECS::Actor::OnDeserialize(tinyxml2::XMLDocument & p_doc, tinyxml2:: std::string prefabSource; OvCore::Helpers::Serializer::DeserializeString(p_doc, p_actorsRoot, "prefab_source", prefabSource); SetPrefabSource(prefabSource); + uint64_t prefabNodeGUID = 0; + OvCore::Helpers::Serializer::DeserializeUInt64(p_doc, p_actorsRoot, "prefab_node_guid", prefabNodeGUID); + SetPrefabNodeGUID(prefabNodeGUID); OvCore::Helpers::Serializer::DeserializeInt64(p_doc, p_actorsRoot, "parent", m_parentID); { diff --git a/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp b/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp index b0755230..05a232d3 100644 --- a/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp +++ b/Sources/OvCore/src/OvCore/SceneSystem/PrefabOperations.cpp @@ -22,6 +22,11 @@ namespace tinyxml2::XMLDocument& p_doc, tinyxml2::XMLNode& p_actorsRoot) { + if (!p_actor.HasPrefabNodeGUID()) + { + p_actor.SetPrefabNodeGUID(p_actor.GetGUID()); + } + p_actor.OnSerialize(p_doc, &p_actorsRoot); for (auto* child : p_actor.GetChildren()) @@ -95,6 +100,8 @@ OvCore::ECS::Actor* OvCore::SceneSystem::PrefabOperations::InstantiateFromFile( const uint64_t generatedGUID = newActor.GetGUID(); newActor.OnDeserialize(doc, currentActor); + const uint64_t prefabNodeGUID = newActor.HasPrefabNodeGUID() ? newActor.GetPrefabNodeGUID() : newActor.GetGUID(); + newActor.SetPrefabNodeGUID(prefabNodeGUID); pendingAttachments.push_back({ &newActor, From 50dcef4459b9769e672e616aa80ca5781a796d5a Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 30 Apr 2026 21:25:05 +0200 Subject: [PATCH 19/23] refactor(editor): simplify hierarchy context target and actor creation parenting --- .../OvEditor/Utils/ActorCreationMenu.h | 6 +- .../src/OvEditor/Panels/Hierarchy.cpp | 89 ++-------- .../src/OvEditor/Utils/ActorCreationMenu.cpp | 154 +++++++----------- 3 files changed, 70 insertions(+), 179 deletions(-) diff --git a/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h b/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h index 23e2d3e4..42c1d6d7 100644 --- a/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h +++ b/Sources/OvEditor/include/OvEditor/Utils/ActorCreationMenu.h @@ -27,8 +27,6 @@ namespace OvEditor::Utils class ActorCreationMenu { public: - using ActorParentProvider = std::function; - /** * Disabled constructor */ @@ -38,9 +36,9 @@ namespace OvEditor::Utils * Generates an actor creation menu under the given MenuList item. * Also handles custom additionnal OnClick callback * @param p_menuList - * @param p_parentProvider + * @param p_parent * @param p_onItemClicked */ - static void GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, ActorParentProvider p_parentProvider = {}, std::optional> p_onItemClicked = {}); + static void GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, OvCore::ECS::Actor* p_parent = nullptr, std::optional> p_onItemClicked = {}); }; } diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index a824343c..aa5e7f6e 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -86,51 +86,33 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu auto& focusButton = CreateWidget("Focus"); focusButton.ClickedEvent += [this] { - if (auto* target = GetTargetActor()) - { - EDITOR_EXEC(MoveToTarget(*target)); - } + EDITOR_EXEC(MoveToTarget(*m_targetActor)); }; auto& copyButton = CreateWidget("Copy"); copyButton.ClickedEvent += [this] { - if (auto* target = GetTargetActor()) - { - EDITOR_EXEC(CopyActor(*target)); - } + EDITOR_EXEC(CopyActor(*m_targetActor)); }; auto& duplicateButton = CreateWidget("Duplicate"); duplicateButton.ClickedEvent += [this] { - if (auto* target = GetTargetActor()) - { - EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*target), nullptr, true), 0)); - } + EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*m_targetActor), nullptr, true), 0)); }; auto& pasteButton = CreateWidget("Paste"); pasteButton.ClickedEvent += [this] { - if (auto* target = GetTargetActor()) - { - EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, target), 0)); - } + EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, m_targetActor), 0)); }; auto& saveAsPrefabButton = CreateWidget("Save as Prefab..."); saveAsPrefabButton.ClickedEvent += [this] { - auto* target = GetTargetActor(); - if (!target) - { - return; - } - OvWindowing::Dialogs::SaveFileDialog dialog("Save Prefab"); dialog.SetInitialDirectory(EDITOR_CONTEXT(projectAssetsPath).string()); - dialog.SetInitialFilename(target->GetName()); + dialog.SetInitialFilename(m_targetActor->GetName()); dialog.DefineExtension("Overload Prefab", ".ovprefab"); dialog.Show(); @@ -155,36 +137,27 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu } } - EDITOR_EXEC(SaveActorAsPrefab(*target, dialog.GetSelectedFilePath())); + EDITOR_EXEC(SaveActorAsPrefab(*m_targetActor, dialog.GetSelectedFilePath())); }; auto& applyToPrefabButton = CreateWidget("Apply to Prefab"); m_applyToPrefabButton = &applyToPrefabButton; applyToPrefabButton.ClickedEvent += [this] { - if (auto* target = GetTargetActor()) - { - EDITOR_EXEC(ApplyActorToPrefab(*target)); - } + EDITOR_EXEC(ApplyActorToPrefab(*m_targetActor)); }; auto& revertToPrefabButton = CreateWidget("Revert to Prefab"); m_revertToPrefabButton = &revertToPrefabButton; revertToPrefabButton.ClickedEvent += [this] { - if (auto* target = GetTargetActor()) - { - EDITOR_EXEC(RevertActorToPrefab(*target)); - } + EDITOR_EXEC(RevertActorToPrefab(*m_targetActor)); }; auto& deleteButton = CreateWidget("Delete"); deleteButton.ClickedEvent += [this] { - if (auto* target = GetTargetActor()) - { - EDITOR_EXEC(DestroyActor(std::ref(*target))); - } + EDITOR_EXEC(DestroyActor(std::ref(*m_targetActor))); }; auto& renameMenu = CreateWidget("Rename to..."); @@ -194,18 +167,12 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu renameMenu.ClickedEvent += [this, &nameEditor] { - if (auto* target = GetTargetActor()) - { - nameEditor.content = target->GetName(); - } + nameEditor.content = m_targetActor->GetName(); }; nameEditor.EnterPressedEvent += [this](std::string p_newName) { - if (auto* target = GetTargetActor()) - { - target->SetName(p_newName); - } + m_targetActor->SetName(p_newName); }; } else @@ -228,7 +195,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu( createActor, - [this]() { return GetTargetActor(); }, + m_targetActor, onItemClicked ); } @@ -237,7 +204,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu { if (m_applyToPrefabButton || m_revertToPrefabButton) { - const bool canEditPrefab = CanEditPrefab(); + const bool canEditPrefab = m_targetActor && GetPrefabInstanceRoot(*m_targetActor) != nullptr; if (m_applyToPrefabButton) { @@ -255,36 +222,6 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu } private: - OvCore::ECS::Actor* GetTargetActor() const - { - if (!m_targetActor) - { - return nullptr; - } - - auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); - if (!currentScene) - { - return nullptr; - } - - const auto& actors = currentScene->GetActors(); - const bool actorIsStillInScene = - std::find(actors.begin(), actors.end(), m_targetActor) != actors.end(); - - return actorIsStillInScene ? m_targetActor : nullptr; - } - - bool CanEditPrefab() const - { - if (auto* target = GetTargetActor()) - { - return GetPrefabInstanceRoot(*target) != nullptr; - } - - return false; - } - OvCore::ECS::Actor* m_targetActor = nullptr; OvUI::Widgets::Layout::TreeNode* m_treeNode; OvUI::Widgets::Menu::MenuItem* m_applyToPrefabButton = nullptr; diff --git a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp index 5a7bd3c3..e5d59da7 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -24,18 +24,23 @@ #include #include -#include - #include #include namespace { - using ActorParentProvider = OvEditor::Utils::ActorCreationMenu::ActorParentProvider; - - OvCore::ECS::Actor* ResolveParent(const ActorParentProvider& p_parentProvider) + std::function Combine(std::function p_a, std::optional> p_b) { - return p_parentProvider ? p_parentProvider() : nullptr; + if (p_b.has_value()) + { + return [=]() + { + p_a(); + p_b.value()(); + }; + } + + return p_a; } void CreateSkysphere(OvCore::ECS::Actor* p_parent) @@ -116,78 +121,38 @@ namespace template - std::function ActorWithComponentCreationHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) + std::function ActorWithComponentCreationHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() - { - EDITOR_EXEC(CreateMonoComponentActor(true, ResolveParent(p_parentProvider))); - - if (p_onItemClicked.has_value()) - { - p_onItemClicked.value()(); - } - }; + return Combine(EDITOR_BIND(CreateMonoComponentActor, true, p_parent), p_onItemClicked); } - std::function ActorWithModelComponentCreationHandler(ActorParentProvider p_parentProvider, const std::string& p_modelName, std::optional> p_onItemClicked) + std::function ActorWithModelComponentCreationHandler(OvCore::ECS::Actor* p_parent, const std::string& p_modelName, std::optional> p_onItemClicked) { - return [p_parentProvider = std::move(p_parentProvider), p_modelName, p_onItemClicked]() - { - EDITOR_EXEC(CreateActorWithModel(":Models\\" + p_modelName + ".fbx", true, ResolveParent(p_parentProvider), p_modelName)); - - if (p_onItemClicked.has_value()) - { - p_onItemClicked.value()(); - } - }; + return Combine(EDITOR_BIND(CreateActorWithModel, ":Models\\" + p_modelName + ".fbx", true, p_parent, p_modelName), p_onItemClicked); } - std::function CreateSkysphereHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) + std::function CreateSkysphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() - { - CreateSkysphere(ResolveParent(p_parentProvider)); - - if (p_onItemClicked.has_value()) - { - p_onItemClicked.value()(); - } - }; + return Combine(std::bind(CreateSkysphere, p_parent), p_onItemClicked); } - std::function CreateAtmosphereHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) + std::function CreateAtmosphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() - { - CreateAtmosphere(ResolveParent(p_parentProvider)); - - if (p_onItemClicked.has_value()) - { - p_onItemClicked.value()(); - } - }; + return Combine(std::bind(CreateAtmosphere, p_parent), p_onItemClicked); } - std::function CreateCharacterHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) + std::function CreateCharacterHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() - { - CreateCharacter(ResolveParent(p_parentProvider)); - - if (p_onItemClicked.has_value()) - { - p_onItemClicked.value()(); - } - }; + return Combine(std::bind(CreateCharacter, p_parent), p_onItemClicked); } - std::function CreateFromPrefabHandler(ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) + std::function CreateFromPrefabHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return [p_parentProvider = std::move(p_parentProvider), p_onItemClicked]() + return [p_parent, p_onItemClicked]() { OvCore::Helpers::GUIHelpers::OpenAssetPicker( OvTools::Utils::PathParser::EFileType::PREFAB, - [p_parentProvider, p_onItemClicked](std::string p_prefabPath) + [p_parent, p_onItemClicked](std::string p_prefabPath) { if (p_prefabPath.empty()) { @@ -196,9 +161,9 @@ namespace if (auto* actor = EDITOR_EXEC(InstantiatePrefab(p_prefabPath)); actor) { - if (auto* parent = ResolveParent(p_parentProvider); parent) + if (p_parent) { - actor->SetParent(*parent); + actor->SetParent(*p_parent); } EDITOR_EXEC(SelectActor(*actor)); @@ -216,22 +181,13 @@ namespace } } -void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, ActorParentProvider p_parentProvider, std::optional> p_onItemClicked) +void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets::Menu::MenuList& p_menuList, OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { using namespace OvUI::Widgets::Menu; using namespace OvCore::ECS::Components; - p_menuList.CreateWidget("Create Empty").ClickedEvent += [p_parentProvider, p_onItemClicked]() - { - EDITOR_EXEC(CreateEmptyActor(true, ResolveParent(p_parentProvider), "")); - - if (p_onItemClicked.has_value()) - { - p_onItemClicked.value()(); - } - }; - - p_menuList.CreateWidget("From prefab...").ClickedEvent += CreateFromPrefabHandler(p_parentProvider, p_onItemClicked); + p_menuList.CreateWidget("Create Empty").ClickedEvent += Combine(EDITOR_BIND(CreateEmptyActor, true, p_parent, ""), p_onItemClicked); + p_menuList.CreateWidget("From prefab...").ClickedEvent += CreateFromPrefabHandler(p_parent, p_onItemClicked); auto& primitives = p_menuList.CreateWidget("Primitives"); auto& physicals = p_menuList.CreateWidget("Physicals"); @@ -239,30 +195,30 @@ void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets auto& audio = p_menuList.CreateWidget("Audio"); auto& others = p_menuList.CreateWidget("Others"); - primitives.CreateWidget("Cube").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cube", p_onItemClicked); - primitives.CreateWidget("Sphere").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Sphere", p_onItemClicked); - primitives.CreateWidget("Cone").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cone", p_onItemClicked); - primitives.CreateWidget("Cylinder").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Cylinder", p_onItemClicked); - primitives.CreateWidget("Plane").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Plane", p_onItemClicked); - primitives.CreateWidget("Gear").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Gear", p_onItemClicked); - primitives.CreateWidget("Helix").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Helix", p_onItemClicked); - primitives.CreateWidget("Pipe").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Pipe", p_onItemClicked); - primitives.CreateWidget("Pyramid").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Pyramid", p_onItemClicked); - primitives.CreateWidget("Torus").ClickedEvent += ActorWithModelComponentCreationHandler(p_parentProvider, "Torus", p_onItemClicked); - physicals.CreateWidget("Physical Box").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - physicals.CreateWidget("Physical Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - physicals.CreateWidget("Physical Capsule").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - lights.CreateWidget("Point").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - lights.CreateWidget("Directional").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - lights.CreateWidget("Spot").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - lights.CreateWidget("Ambient Box").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - lights.CreateWidget("Ambient Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - audio.CreateWidget("Audio Source").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - audio.CreateWidget("Audio Listener").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - others.CreateWidget("Camera").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - others.CreateWidget("Post Process Stack").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - others.CreateWidget("Reflection Probe").ClickedEvent += ActorWithComponentCreationHandler(p_parentProvider, p_onItemClicked); - others.CreateWidget("Skysphere").ClickedEvent += CreateSkysphereHandler(p_parentProvider, p_onItemClicked); - others.CreateWidget("Atmosphere").ClickedEvent += CreateAtmosphereHandler(p_parentProvider, p_onItemClicked); - others.CreateWidget("Character").ClickedEvent += CreateCharacterHandler(p_parentProvider, p_onItemClicked); + primitives.CreateWidget("Cube").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Cube", p_onItemClicked); + primitives.CreateWidget("Sphere").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Sphere", p_onItemClicked); + primitives.CreateWidget("Cone").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Cone", p_onItemClicked); + primitives.CreateWidget("Cylinder").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Cylinder", p_onItemClicked); + primitives.CreateWidget("Plane").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Plane", p_onItemClicked); + primitives.CreateWidget("Gear").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Gear", p_onItemClicked); + primitives.CreateWidget("Helix").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Helix", p_onItemClicked); + primitives.CreateWidget("Pipe").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Pipe", p_onItemClicked); + primitives.CreateWidget("Pyramid").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Pyramid", p_onItemClicked); + primitives.CreateWidget("Torus").ClickedEvent += ActorWithModelComponentCreationHandler(p_parent, "Torus", p_onItemClicked); + physicals.CreateWidget("Physical Box").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + physicals.CreateWidget("Physical Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + physicals.CreateWidget("Physical Capsule").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + lights.CreateWidget("Point").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + lights.CreateWidget("Directional").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + lights.CreateWidget("Spot").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + lights.CreateWidget("Ambient Box").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + lights.CreateWidget("Ambient Sphere").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + audio.CreateWidget("Audio Source").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + audio.CreateWidget("Audio Listener").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + others.CreateWidget("Camera").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + others.CreateWidget("Post Process Stack").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + others.CreateWidget("Reflection Probe").ClickedEvent += ActorWithComponentCreationHandler(p_parent, p_onItemClicked); + others.CreateWidget("Skysphere").ClickedEvent += CreateSkysphereHandler(p_parent, p_onItemClicked); + others.CreateWidget("Atmosphere").ClickedEvent += CreateAtmosphereHandler(p_parent, p_onItemClicked); + others.CreateWidget("Character").ClickedEvent += CreateCharacterHandler(p_parent, p_onItemClicked); } From faa4b691d216eb9d8da726c17e02f93b596a2008 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 30 Apr 2026 21:25:09 +0200 Subject: [PATCH 20/23] fix(editor): match prefab revert nodes by prefab GUID metadata --- .../src/OvEditor/Core/EditorActions.cpp | 216 +++++++++--------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 6344a3a3..195bcc1e 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -171,108 +172,54 @@ namespace } } - struct ChildMatchPlan + struct ActorHierarchyEntry { - std::vector> matchedChildren; - std::vector unmatchedTargetChildren; - std::vector unmatchedTemplateChildren; + OvCore::ECS::Actor* actor = nullptr; + OvCore::ECS::Actor* parent = nullptr; }; - ChildMatchPlan BuildChildMatchPlan(OvCore::ECS::Actor& p_targetActor, OvCore::ECS::Actor& p_templateActor) + void CollectActorHierarchy( + OvCore::ECS::Actor& p_actor, + OvCore::ECS::Actor* p_parent, + std::vector& p_outEntries) { - auto& targetChildren = p_targetActor.GetChildren(); - auto& templateChildren = p_templateActor.GetChildren(); - - ChildMatchPlan matchPlan; - std::vector usedTargetChildren(targetChildren.size(), false); - std::vector usedTemplateChildren(templateChildren.size(), false); + p_outEntries.push_back({ &p_actor, p_parent }); - // Prefer name-based matching to keep GUID identity stable when siblings were reordered. - for (size_t templateChildIndex = 0; templateChildIndex < templateChildren.size(); ++templateChildIndex) + for (auto* child : p_actor.GetChildren()) { - const std::string& templateChildName = templateChildren[templateChildIndex]->GetName(); - - for (size_t targetChildIndex = 0; targetChildIndex < targetChildren.size(); ++targetChildIndex) - { - if (usedTargetChildren[targetChildIndex]) - { - continue; - } - - if (targetChildren[targetChildIndex]->GetName() == templateChildName) - { - matchPlan.matchedChildren.emplace_back(targetChildIndex, templateChildIndex); - usedTargetChildren[targetChildIndex] = true; - usedTemplateChildren[templateChildIndex] = true; - break; - } - } + CollectActorHierarchy(*child, &p_actor, p_outEntries); } + } - // Fallback to positional matching for unnamed/duplicate-name leftovers. - size_t nextUnmatchedTargetChild = 0; - - for (size_t templateChildIndex = 0; templateChildIndex < templateChildren.size(); ++templateChildIndex) + uint64_t GetPrefabNodeGUIDOrFallback(OvCore::ECS::Actor& p_actor) + { + if (p_actor.HasPrefabNodeGUID()) { - if (usedTemplateChildren[templateChildIndex]) - { - continue; - } - - while (nextUnmatchedTargetChild < targetChildren.size() && usedTargetChildren[nextUnmatchedTargetChild]) - { - ++nextUnmatchedTargetChild; - } - - if (nextUnmatchedTargetChild >= targetChildren.size()) - { - break; - } - - matchPlan.matchedChildren.emplace_back(nextUnmatchedTargetChild, templateChildIndex); - usedTargetChildren[nextUnmatchedTargetChild] = true; - usedTemplateChildren[templateChildIndex] = true; - ++nextUnmatchedTargetChild; + return p_actor.GetPrefabNodeGUID(); } - for (size_t targetChildIndex = 0; targetChildIndex < targetChildren.size(); ++targetChildIndex) - { - if (!usedTargetChildren[targetChildIndex]) - { - matchPlan.unmatchedTargetChildren.push_back(targetChildIndex); - } - } + const uint64_t fallbackGUID = p_actor.GetGUID(); + p_actor.SetPrefabNodeGUID(fallbackGUID); + return fallbackGUID; + } - for (size_t templateChildIndex = 0; templateChildIndex < templateChildren.size(); ++templateChildIndex) + OvCore::ECS::Actor* FindUnusedActorWithPrefabNodeGUID( + const uint64_t p_prefabNodeGUID, + const std::unordered_map>& p_actorsByPrefabNodeGUID, + const std::unordered_set& p_usedActors) + { + if (const auto found = p_actorsByPrefabNodeGUID.find(p_prefabNodeGUID); found != p_actorsByPrefabNodeGUID.end()) { - if (!usedTemplateChildren[templateChildIndex]) + for (auto* actor : found->second) { - matchPlan.unmatchedTemplateChildren.push_back(templateChildIndex); + if (!p_usedActors.contains(actor)) + { + return actor; + } } } - return matchPlan; - } - - void BuildGuidRemapFromCommonHierarchy( - OvCore::ECS::Actor& p_targetActor, - OvCore::ECS::Actor& p_templateActor, - std::unordered_map& p_guidRemap) - { - p_guidRemap[p_templateActor.GetGUID()] = p_targetActor.GetGUID(); - - auto& targetChildren = p_targetActor.GetChildren(); - auto& templateChildren = p_templateActor.GetChildren(); - const auto matchPlan = BuildChildMatchPlan(p_targetActor, p_templateActor); - - for (const auto& [targetChildIndex, templateChildIndex] : matchPlan.matchedChildren) - { - BuildGuidRemapFromCommonHierarchy( - *targetChildren[targetChildIndex], - *templateChildren[templateChildIndex], - p_guidRemap - ); - } + return nullptr; } void SetActorNodeValue(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLElement& p_actorNode, const char* p_fieldName, const std::string& p_value) @@ -1218,8 +1165,6 @@ OvCore::ECS::Actor* OvEditor::Core::EditorActions::InstantiatePrefab(const std:: const std::string prefabName = realPath.stem().string(); if (!prefabName.empty()) instantiatedRoot->SetName(prefabName); - - OVLOG_INFO("Prefab instantiated: " + realPath.string()); } else { @@ -1271,41 +1216,96 @@ bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_ac return false; } - std::unordered_map prefabToInstanceGuidMap; - BuildGuidRemapFromCommonHierarchy(*prefabInstanceRoot, *prefabTemplateRoot, prefabToInstanceGuidMap); + std::vector templateHierarchy; + CollectActorHierarchy(*prefabTemplateRoot, nullptr, templateHierarchy); + + std::vector targetHierarchy; + CollectActorHierarchy(*prefabInstanceRoot, nullptr, targetHierarchy); + + std::unordered_map> targetActorsByPrefabNodeGUID; + for (const auto& entry : targetHierarchy) + { + const uint64_t prefabNodeGUID = GetPrefabNodeGUIDOrFallback(*entry.actor); + targetActorsByPrefabNodeGUID[prefabNodeGUID].push_back(entry.actor); + } + + std::unordered_map templateToTarget; + std::unordered_set usedTargetActors; + + const uint64_t templateRootPrefabNodeGUID = GetPrefabNodeGUIDOrFallback(*prefabTemplateRoot); + prefabInstanceRoot->SetPrefabNodeGUID(templateRootPrefabNodeGUID); + templateToTarget[prefabTemplateRoot] = prefabInstanceRoot; + usedTargetActors.insert(prefabInstanceRoot); + + for (size_t index = 1; index < templateHierarchy.size(); ++index) + { + auto* templateActor = templateHierarchy[index].actor; + const uint64_t prefabNodeGUID = GetPrefabNodeGUIDOrFallback(*templateActor); + + OvCore::ECS::Actor* targetActor = FindUnusedActorWithPrefabNodeGUID( + prefabNodeGUID, + targetActorsByPrefabNodeGUID, + usedTargetActors + ); + + if (!targetActor) + { + targetActor = &CreateEmptyActor(false); + } + + targetActor->SetPrefabNodeGUID(prefabNodeGUID); + templateToTarget[templateActor] = targetActor; + usedTargetActors.insert(targetActor); + } + + std::unordered_map prefabNodeToInstanceGuidMap; + for (const auto& [templateActor, targetActor] : templateToTarget) + { + const uint64_t prefabNodeGUID = templateActor->GetPrefabNodeGUID(); + prefabNodeToInstanceGuidMap[prefabNodeGUID] = targetActor->GetGUID(); + } - std::function syncActorHierarchy; - syncActorHierarchy = [this, &syncActorHierarchy, &prefabToInstanceGuidMap](OvCore::ECS::Actor& p_targetActor, OvCore::ECS::Actor& p_templateActor, bool p_isRoot) + for (const auto& entry : templateHierarchy) { + auto* templateActor = entry.actor; + auto* targetActor = templateToTarget[templateActor]; + const bool isRoot = templateActor == prefabTemplateRoot; + OverwriteActorFromPrefabTemplate( - p_targetActor, - p_templateActor, - prefabToInstanceGuidMap, - p_isRoot, /* keep root name */ - p_isRoot /* keep root local transform */ + *targetActor, + *templateActor, + prefabNodeToInstanceGuidMap, + isRoot, /* keep root name */ + isRoot /* keep root local transform */ ); + } - auto& targetChildren = p_targetActor.GetChildren(); - auto& templateChildren = p_templateActor.GetChildren(); - const auto matchPlan = BuildChildMatchPlan(p_targetActor, p_templateActor); + for (size_t index = 1; index < templateHierarchy.size(); ++index) + { + auto* templateActor = templateHierarchy[index].actor; + auto* templateParent = templateHierarchy[index].parent; + auto* targetActor = templateToTarget[templateActor]; + auto* expectedParent = templateToTarget[templateParent]; - for (const auto& [targetChildIndex, templateChildIndex] : matchPlan.matchedChildren) + if (targetActor->GetParent() != expectedParent) { - syncActorHierarchy(*targetChildren[targetChildIndex], *templateChildren[templateChildIndex], false); + targetActor->SetParent(*expectedParent); } + } - for (const size_t targetChildIndex : matchPlan.unmatchedTargetChildren) + for (const auto& entry : targetHierarchy) + { + auto* targetActor = entry.actor; + if (targetActor == prefabInstanceRoot) { - targetChildren[targetChildIndex]->MarkAsDestroy(); + continue; } - for (const size_t templateChildIndex : matchPlan.unmatchedTemplateChildren) + if (!usedTargetActors.contains(targetActor)) { - DuplicateActor(*templateChildren[templateChildIndex], &p_targetActor, false); + targetActor->MarkAsDestroy(); } - }; - - syncActorHierarchy(*prefabInstanceRoot, *prefabTemplateRoot, true); + } prefabTemplateRoot->MarkAsDestroy(); SelectActor(*prefabInstanceRoot); From f253e46dd9f13a5f9c7a711716f3350328b3376b Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 30 Apr 2026 21:40:43 +0200 Subject: [PATCH 21/23] fix(prefab): remap revert actor refs with serialized prefab GUIDs --- .../src/OvEditor/Core/EditorActions.cpp | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 195bcc1e..7e5d8463 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -222,6 +222,52 @@ namespace return nullptr; } + std::unordered_map GatherSerializedGuidsByPrefabNodeGUID(const std::filesystem::path& p_prefabPath) + { + std::unordered_map serializedGuidsByPrefabNodeGUID; + + tinyxml2::XMLDocument doc; + if (doc.LoadFile(p_prefabPath.string().c_str()) != tinyxml2::XML_SUCCESS) + { + return serializedGuidsByPrefabNodeGUID; + } + + auto* rootNode = doc.FirstChildElement("root"); + auto* prefabNode = rootNode ? rootNode->FirstChildElement("prefab") : nullptr; + auto* actorsNode = prefabNode ? prefabNode->FirstChildElement("actors") : nullptr; + if (!actorsNode) + { + return serializedGuidsByPrefabNodeGUID; + } + + for (auto* actorNode = actorsNode->FirstChildElement("actor"); + actorNode; + actorNode = actorNode->NextSiblingElement("actor")) + { + const char* serializedGuidText = actorNode->FirstChildElement("guid") + ? actorNode->FirstChildElement("guid")->GetText() + : nullptr; + uint64_t serializedGUID = 0; + if (!TryParseUInt64(serializedGuidText, serializedGUID)) + { + continue; + } + + const char* prefabNodeGuidText = actorNode->FirstChildElement("prefab_node_guid") + ? actorNode->FirstChildElement("prefab_node_guid")->GetText() + : nullptr; + uint64_t prefabNodeGUID = 0; + if (!TryParseUInt64(prefabNodeGuidText, prefabNodeGUID)) + { + prefabNodeGUID = serializedGUID; + } + + serializedGuidsByPrefabNodeGUID[prefabNodeGUID] = serializedGUID; + } + + return serializedGuidsByPrefabNodeGUID; + } + void SetActorNodeValue(tinyxml2::XMLDocument& p_doc, tinyxml2::XMLElement& p_actorNode, const char* p_fieldName, const std::string& p_value) { auto* field = p_actorNode.FirstChildElement(p_fieldName); @@ -1209,6 +1255,8 @@ bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_ac } const auto prefabSourcePath = prefabInstanceRoot->GetPrefabSource(); + const auto serializedGuidsByPrefabNodeGUID = + GatherSerializedGuidsByPrefabNodeGUID(GetRealPath(prefabSourcePath)); auto* prefabTemplateRoot = InstantiatePrefab(prefabSourcePath); if (!prefabTemplateRoot) @@ -1258,11 +1306,17 @@ bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_ac usedTargetActors.insert(targetActor); } - std::unordered_map prefabNodeToInstanceGuidMap; + std::unordered_map templateActorGuidToInstanceGuidMap; for (const auto& [templateActor, targetActor] : templateToTarget) { const uint64_t prefabNodeGUID = templateActor->GetPrefabNodeGUID(); - prefabNodeToInstanceGuidMap[prefabNodeGUID] = targetActor->GetGUID(); + const uint64_t targetGUID = targetActor->GetGUID(); + templateActorGuidToInstanceGuidMap[prefabNodeGUID] = targetGUID; + + if (const auto it = serializedGuidsByPrefabNodeGUID.find(prefabNodeGUID); it != serializedGuidsByPrefabNodeGUID.end()) + { + templateActorGuidToInstanceGuidMap[it->second] = targetGUID; + } } for (const auto& entry : templateHierarchy) @@ -1274,7 +1328,7 @@ bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_ac OverwriteActorFromPrefabTemplate( *targetActor, *templateActor, - prefabNodeToInstanceGuidMap, + templateActorGuidToInstanceGuidMap, isRoot, /* keep root name */ isRoot /* keep root local transform */ ); From 535732890323fad5ef95da0b48658970e02744de Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 30 Apr 2026 21:40:46 +0200 Subject: [PATCH 22/23] fix(editor): guard context actor pointers in hierarchy menus --- .../src/OvEditor/Panels/Hierarchy.cpp | 80 ++++++++++++++--- .../src/OvEditor/Utils/ActorCreationMenu.cpp | 86 +++++++++++++++---- 2 files changed, 137 insertions(+), 29 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp index aa5e7f6e..6d243a7f 100644 --- a/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp +++ b/Sources/OvEditor/src/OvEditor/Panels/Hierarchy.cpp @@ -86,33 +86,51 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu auto& focusButton = CreateWidget("Focus"); focusButton.ClickedEvent += [this] { - EDITOR_EXEC(MoveToTarget(*m_targetActor)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(MoveToTarget(*target)); + } }; auto& copyButton = CreateWidget("Copy"); copyButton.ClickedEvent += [this] { - EDITOR_EXEC(CopyActor(*m_targetActor)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(CopyActor(*target)); + } }; auto& duplicateButton = CreateWidget("Duplicate"); duplicateButton.ClickedEvent += [this] { - EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*m_targetActor), nullptr, true), 0)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DelayAction(EDITOR_BIND(DuplicateActor, std::ref(*target), nullptr, true), 0)); + } }; auto& pasteButton = CreateWidget("Paste"); pasteButton.ClickedEvent += [this] { - EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, m_targetActor), 0)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DelayAction(EDITOR_BIND(PasteActor, target), 0)); + } }; auto& saveAsPrefabButton = CreateWidget("Save as Prefab..."); saveAsPrefabButton.ClickedEvent += [this] { + auto* target = GetTargetActor(); + if (!target) + { + return; + } + OvWindowing::Dialogs::SaveFileDialog dialog("Save Prefab"); dialog.SetInitialDirectory(EDITOR_CONTEXT(projectAssetsPath).string()); - dialog.SetInitialFilename(m_targetActor->GetName()); + dialog.SetInitialFilename(target->GetName()); dialog.DefineExtension("Overload Prefab", ".ovprefab"); dialog.Show(); @@ -137,27 +155,36 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu } } - EDITOR_EXEC(SaveActorAsPrefab(*m_targetActor, dialog.GetSelectedFilePath())); + EDITOR_EXEC(SaveActorAsPrefab(*target, dialog.GetSelectedFilePath())); }; auto& applyToPrefabButton = CreateWidget("Apply to Prefab"); m_applyToPrefabButton = &applyToPrefabButton; applyToPrefabButton.ClickedEvent += [this] { - EDITOR_EXEC(ApplyActorToPrefab(*m_targetActor)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(ApplyActorToPrefab(*target)); + } }; auto& revertToPrefabButton = CreateWidget("Revert to Prefab"); m_revertToPrefabButton = &revertToPrefabButton; revertToPrefabButton.ClickedEvent += [this] { - EDITOR_EXEC(RevertActorToPrefab(*m_targetActor)); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(RevertActorToPrefab(*target)); + } }; auto& deleteButton = CreateWidget("Delete"); deleteButton.ClickedEvent += [this] { - EDITOR_EXEC(DestroyActor(std::ref(*m_targetActor))); + if (auto* target = GetTargetActor()) + { + EDITOR_EXEC(DestroyActor(std::ref(*target))); + } }; auto& renameMenu = CreateWidget("Rename to..."); @@ -167,12 +194,18 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu renameMenu.ClickedEvent += [this, &nameEditor] { - nameEditor.content = m_targetActor->GetName(); + if (auto* target = GetTargetActor()) + { + nameEditor.content = target->GetName(); + } }; nameEditor.EnterPressedEvent += [this](std::string p_newName) { - m_targetActor->SetName(p_newName); + if (auto* target = GetTargetActor()) + { + target->SetName(p_newName); + } }; } else @@ -195,7 +228,7 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu( createActor, - m_targetActor, + GetTargetActor(), onItemClicked ); } @@ -204,7 +237,8 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu { if (m_applyToPrefabButton || m_revertToPrefabButton) { - const bool canEditPrefab = m_targetActor && GetPrefabInstanceRoot(*m_targetActor) != nullptr; + auto* target = GetTargetActor(); + const bool canEditPrefab = target && GetPrefabInstanceRoot(*target) != nullptr; if (m_applyToPrefabButton) { @@ -222,6 +256,26 @@ class ActorContextualMenu : public OvUI::Plugins::ContextualMenu } private: + OvCore::ECS::Actor* GetTargetActor() const + { + if (!m_targetActor) + { + return nullptr; + } + + auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); + if (!currentScene) + { + return nullptr; + } + + const auto& actors = currentScene->GetActors(); + const bool actorIsStillInScene = + std::find(actors.begin(), actors.end(), m_targetActor) != actors.end(); + + return actorIsStillInScene ? m_targetActor : nullptr; + } + OvCore::ECS::Actor* m_targetActor = nullptr; OvUI::Widgets::Layout::TreeNode* m_treeNode; OvUI::Widgets::Menu::MenuItem* m_applyToPrefabButton = nullptr; diff --git a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp index e5d59da7..6e8ae56e 100644 --- a/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp +++ b/Sources/OvEditor/src/OvEditor/Utils/ActorCreationMenu.cpp @@ -24,23 +24,29 @@ #include #include +#include + #include #include namespace { - std::function Combine(std::function p_a, std::optional> p_b) + OvCore::ECS::Actor* ResolveAliveParent(OvCore::ECS::Actor* p_parent) { - if (p_b.has_value()) + if (!p_parent) { - return [=]() - { - p_a(); - p_b.value()(); - }; + return nullptr; + } + + auto* currentScene = EDITOR_CONTEXT(sceneManager).GetCurrentScene(); + if (!currentScene) + { + return nullptr; } - return p_a; + const auto& actors = currentScene->GetActors(); + const bool parentIsStillInScene = std::find(actors.begin(), actors.end(), p_parent) != actors.end(); + return parentIsStillInScene ? p_parent : nullptr; } void CreateSkysphere(OvCore::ECS::Actor* p_parent) @@ -123,27 +129,67 @@ namespace template std::function ActorWithComponentCreationHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(EDITOR_BIND(CreateMonoComponentActor, true, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + EDITOR_EXEC(CreateMonoComponentActor(true, ResolveAliveParent(p_parent))); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function ActorWithModelComponentCreationHandler(OvCore::ECS::Actor* p_parent, const std::string& p_modelName, std::optional> p_onItemClicked) { - return Combine(EDITOR_BIND(CreateActorWithModel, ":Models\\" + p_modelName + ".fbx", true, p_parent, p_modelName), p_onItemClicked); + return [p_parent, p_modelName, p_onItemClicked]() + { + EDITOR_EXEC(CreateActorWithModel(":Models\\" + p_modelName + ".fbx", true, ResolveAliveParent(p_parent), p_modelName)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function CreateSkysphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateSkysphere, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + CreateSkysphere(ResolveAliveParent(p_parent)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function CreateAtmosphereHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateAtmosphere, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + CreateAtmosphere(ResolveAliveParent(p_parent)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function CreateCharacterHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) { - return Combine(std::bind(CreateCharacter, p_parent), p_onItemClicked); + return [p_parent, p_onItemClicked]() + { + CreateCharacter(ResolveAliveParent(p_parent)); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; } std::function CreateFromPrefabHandler(OvCore::ECS::Actor* p_parent, std::optional> p_onItemClicked) @@ -161,9 +207,9 @@ namespace if (auto* actor = EDITOR_EXEC(InstantiatePrefab(p_prefabPath)); actor) { - if (p_parent) + if (auto* parent = ResolveAliveParent(p_parent); parent) { - actor->SetParent(*p_parent); + actor->SetParent(*parent); } EDITOR_EXEC(SelectActor(*actor)); @@ -186,7 +232,15 @@ void OvEditor::Utils::ActorCreationMenu::GenerateActorCreationMenu(OvUI::Widgets using namespace OvUI::Widgets::Menu; using namespace OvCore::ECS::Components; - p_menuList.CreateWidget("Create Empty").ClickedEvent += Combine(EDITOR_BIND(CreateEmptyActor, true, p_parent, ""), p_onItemClicked); + p_menuList.CreateWidget("Create Empty").ClickedEvent += [p_parent, p_onItemClicked]() + { + EDITOR_EXEC(CreateEmptyActor(true, ResolveAliveParent(p_parent), "")); + + if (p_onItemClicked.has_value()) + { + p_onItemClicked.value()(); + } + }; p_menuList.CreateWidget("From prefab...").ClickedEvent += CreateFromPrefabHandler(p_parent, p_onItemClicked); auto& primitives = p_menuList.CreateWidget("Primitives"); From c953e9000f60048daba5dbec8cf81fb83edb91f8 Mon Sep 17 00:00:00 2001 From: Gopmyc Date: Thu, 30 Apr 2026 21:42:31 +0200 Subject: [PATCH 23/23] chore(editor): remove non-essential prefab apply/revert info logs --- Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp index 7e5d8463..a6e24855 100644 --- a/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp +++ b/Sources/OvEditor/src/OvEditor/Core/EditorActions.cpp @@ -1237,11 +1237,6 @@ bool OvEditor::Core::EditorActions::ApplyActorToPrefab(OvCore::ECS::Actor& p_act return false; } - OVLOG_INFO( - "Prefab updated from actor \"" + p_actor.GetName() + - "\" (instance root: \"" + prefabInstanceRoot->GetName() + - "\"): " + realPath - ); return true; } @@ -1364,7 +1359,6 @@ bool OvEditor::Core::EditorActions::RevertActorToPrefab(OvCore::ECS::Actor& p_ac prefabTemplateRoot->MarkAsDestroy(); SelectActor(*prefabInstanceRoot); - OVLOG_INFO("Prefab reverted on actor \"" + prefabInstanceRoot->GetName() + "\": " + GetRealPath(prefabSourcePath)); return true; }