diff --git a/Editor/Qml/EButton.qml b/Editor/Qml/EButton.qml index 0353c1a70..5303bd464 100644 --- a/Editor/Qml/EButton.qml +++ b/Editor/Qml/EButton.qml @@ -2,43 +2,67 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -Button { +Item { enum Style { Primary, - Secondary, - Disabled, - Outline, - Dashed + Secondary } - function getBackgroundColor(style, focus) - { - if (style === EButton.Style.Secondary) { - return focus ? ETheme.secondaryFocusColor : ETheme.secondaryColor; - } else if (style === EButton.Style.Disabled) { - return ETheme.disabledColor; - } - // TODO more - return focus ? ETheme.primaryFocusColor : ETheme.primaryColor; + enum Size { + Small, + Middle, + Large + } + + enum Shape { + Rect, + Round } + property string text: '' + property bool disabled: false property int style: EButton.Style.Primary + property int size: EButton.Size.Middle + property int shape: EButton.Shape.Rect + signal clicked() - // TODO disable onclick when disabled + id: 'root' + implicitWidth: btnWidget.implicitWidth + implicitHeight: btnWidget.implicitHeight - contentItem: Text { - text: parent.text - font.pixelSize: ETheme.contentFontSize - font.family: ETheme.fontFamily - color: ETheme.fontColor - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideNone - } + Button { + id: 'btnWidget' + enabled: !root.disabled + onClicked: root.clicked() + leftPadding: 10 + rightPadding: 10 + topPadding: 5 + 2 * (root.size - 1) + bottomPadding: 5 + 2 * (root.size - 1) + width: root.width - background: Rectangle { - implicitWidth: 50 - color: getBackgroundColor(style, parent.down) - radius: 10 + contentItem: Text { + text: root.text + font.pixelSize: ETheme.contentFontSize + font.family: ETheme.fontFamily + color: ETheme.fontColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + } + + background: Rectangle { + function getBackgroundColor(style, focus, disabled) + { + if (disabled) { + return ETheme.disabledColor; + } else if (style === EButton.Style.Secondary) { + return focus ? ETheme.secondaryFocusColor : ETheme.secondaryColor; + } + return focus ? ETheme.primaryFocusColor : ETheme.primaryColor; + } + + color: getBackgroundColor(root.style, parent.down, root.disabled) + radius: 5 + root.shape * 8 + } } } diff --git a/Editor/Qml/EText.qml b/Editor/Qml/EText.qml index 61c244d8b..289e0fbbd 100644 --- a/Editor/Qml/EText.qml +++ b/Editor/Qml/EText.qml @@ -1,36 +1,53 @@ import QtQuick import QtQuick.Controls -Text { +Item { enum Style { Title1, Title2, Title3, - Content + Content, + Link, + Italic } - function getFontBold(style) - { - return style !== EText.Style.Content - } + property string text: '' + property string href: '' + property int style: EText.Style.Content - function getFontPixelSize(style) - { - if (style === EText.Style.Title1) { - return ETheme.tiele1FontSize; - } else if (style === EText.Style.Title2) { - return ETheme.title2FontSize; - } else if (style === EText.Style.Title3) { - return ETheme.title3FontSize; - } else { - return ETheme.contentFontSize; + id: 'root' + implicitWidth: textWidget.implicitWidth + implicitHeight: textWidget.implicitHeight + + Text { + function getText(text, href, style) + { + return style === EText.Style.Link + ? '%3'.arg(href).arg(ETheme.linkFontColor).arg(text) + : text; } - } - property int style: EText.Style.Content + function getFontPixelSize(style) + { + if (style === EText.Style.Title1) { + return ETheme.tiele1FontSize; + } else if (style === EText.Style.Title2) { + return ETheme.title2FontSize; + } else if (style === EText.Style.Title3) { + return ETheme.title3FontSize; + } else { + return ETheme.contentFontSize; + } + } - font.bold: getFontBold(style) - font.pixelSize: getFontPixelSize(style) - font.family: ETheme.fontFamily - color: ETheme.fontColor + id: 'textWidget' + text: getText(root.text, root.href, root.style) + textFormat: root.style === EText.Style.Link ? Text.RichText : Text.PlainText; + font.italic: root.style === EText.Style.Italic + font.bold: root.style < EText.Style.Content; + font.pixelSize: getFontPixelSize(root.style) + font.family: ETheme.fontFamily + color: ETheme.fontColor + onLinkActivated: Qt.openUrlExternally(root.href) + } } diff --git a/Editor/Qml/ETheme.qml b/Editor/Qml/ETheme.qml index f6f6f6540..250733e81 100644 --- a/Editor/Qml/ETheme.qml +++ b/Editor/Qml/ETheme.qml @@ -10,6 +10,7 @@ QtObject { property color secondaryFocusColor: Qt.color('#9b6a40') property color disabledColor: Qt.color('#676563') property color fontColor: Qt.color('#ecf0f1') + property color linkFontColor: Qt.color('#91b9c4') property FontLoader normalFont: FontLoader { source: Qt.url('Resource/Font/MiSans-Normal.ttf') } property FontLoader boldFont: FontLoader { source: Qt.url('Resource/Font/MiSans-Bold.ttf') } diff --git a/Editor/Qml/EWidgetSamples.qml b/Editor/Qml/EWidgetSamples.qml index 206f7888b..cfce95713 100644 --- a/Editor/Qml/EWidgetSamples.qml +++ b/Editor/Qml/EWidgetSamples.qml @@ -6,11 +6,12 @@ Rectangle { color: ETheme.bgColor ScrollView { + id: 'scrollview' anchors.fill: parent + anchors.margins: 20 ColumnLayout { - anchors.fill: parent - anchors.margins: 20 + width: Math.max(implicitWidth, scrollview.availableWidth) RowLayout { Layout.leftMargin: 5 @@ -25,20 +26,87 @@ Rectangle { Layout.margins: 5 EButton { - text: 'Basic' + text: 'Basic Button' + onClicked: { + console.log('basic button clicked') + } } EButton { style: EButton.Style.Secondary text: 'Secondary Button' + onClicked: { + console.log('secondary button clicked') + } } EButton { - style: EButton.Style.Disabled text: 'Disabled Button' + disabled: true + onClicked: { + console.log('disabled button clicked') + } } + } + + RowLayout { + Layout.margins: 5 + + EButton { + text: 'Large Button' + size: EButton.Size.Large + onClicked: { + console.log('large button clicked') + } + } + + EButton { + text: 'Middle Button' + size: EButton.Size.Middle + onClicked: { + console.log('middle button clicked') + } + } + + EButton { + text: 'Small Button' + size: EButton.Size.Small + onClicked: { + console.log('small button clicked') + } + } + } + + RowLayout { + Layout.margins: 5 + + EButton { + text: 'Rect Button' + shape: EButton.Shape.Rect + onClicked: { + console.log('rect button clicked') + } + } + + EButton { + text: 'Round Button' + shape: EButton.Shape.Round + onClicked: { + console.log('round button clicked') + } + } + } - // TODO icon button + RowLayout { + Layout.margins: 5 + + EButton { + text: 'Block Button' + Layout.fillWidth: true + onClicked: { + console.log('block button clicked') + } + } } RowLayout { @@ -68,14 +136,26 @@ Rectangle { text: 'Title3' style: EText.Style.Title3 } + } + + RowLayout { + Layout.leftMargin: 5 EText { - text: 'Content' + text: 'Basic Content' style: EText.Style.Content } - // TODO superlink text - // TODO button text + EText { + text: 'Sample Link' + href: 'https://github.com/ExplosionEngine/Explosion' + style: EText.Style.Link + } + + EText { + text: 'Italic Content' + style: EText.Style.Italic + } } } } diff --git a/Engine/Source/Common/Include/Common/Container.h b/Engine/Source/Common/Include/Common/Container.h index 2bd6e970e..d72084976 100644 --- a/Engine/Source/Common/Include/Common/Container.h +++ b/Engine/Source/Common/Include/Common/Container.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/Engine/Source/Runtime/Include/Runtime/Component/Transform.h b/Engine/Source/Runtime/Include/Runtime/Component/Transform.h index 6a56881cd..17312532c 100644 --- a/Engine/Source/Runtime/Include/Runtime/Component/Transform.h +++ b/Engine/Source/Runtime/Include/Runtime/Component/Transform.h @@ -10,31 +10,46 @@ #include namespace Runtime { - struct RUNTIME_API EClass() Transform final { - EClassBody(Transform) + struct RUNTIME_API EClass() WorldTransform final { + EClassBody(WorldTransform) - Transform(); - explicit Transform(const Common::FTransform& inLocalToWorld); + WorldTransform(); + explicit WorldTransform(Common::FTransform inLocalToWorld); EProperty() Common::FTransform localToWorld; }; - struct RUNTIME_API EClass() ChildOfConstraint final { - EClassBody(ChildOfConstraint) + // must be used with Hierarchy and WorldTransform + struct RUNTIME_API EClass() LocalTransform final { + EClassBody(LocalTransform) - ChildOfConstraint(); - explicit ChildOfConstraint(Entity inParent, const Common::FTransform& inLocalToParent); + LocalTransform(); + explicit LocalTransform(Common::FTransform inLocalToParent); - EProperty() Entity parent; EProperty() Common::FTransform localToParent; }; - struct RUNTIME_API EClass() CopyConstraint final { - EClassBody(CopyConstraint) + struct RUNTIME_API EClass() Hierarchy final { + EClassBody(Hierarchy) + + Hierarchy(); - CopyConstraint(); - explicit CopyConstraint(Entity inTarget); + EProperty() Entity parent; + EProperty() Entity firstChild; + EProperty() Entity prevBro; + EProperty() Entity nextBro; + }; - EProperty() Entity target; + class HierarchyUtils { + public: + using TraverseFunc = std::function; + + static bool HasParent(ECRegistry& inRegistry, Entity inTarget); + static bool HasBro(ECRegistry& inRegistry, Entity inTarget); + static bool HasChildren(ECRegistry& inRegistry, Entity inTarget); + static void AttachToParent(ECRegistry& inRegistry, Entity inChild, Entity inParent); + static void DetachFromParent(ECRegistry& inRegistry, Entity inChild); + static void TraverseChildren(ECRegistry& inRegistry, Entity inParent, const TraverseFunc& inFunc); + static void TraverseChildrenRecursively(ECRegistry& inRegistry, Entity inParent, const TraverseFunc& inFunc); }; } diff --git a/Engine/Source/Runtime/Include/Runtime/ECS.h b/Engine/Source/Runtime/Include/Runtime/ECS.h index 65eeaa476..d141b134d 100644 --- a/Engine/Source/Runtime/Include/Runtime/ECS.h +++ b/Engine/Source/Runtime/Include/Runtime/ECS.h @@ -324,7 +324,10 @@ namespace Runtime { Observer& ObRemoved(CompClass inClass); size_t Size() const; void Each(const EntityTraverseFunc& inFunc) const; + void EachThenClear(const EntityTraverseFunc& inFunc); void Clear(); + const std::vector& All() const; + std::vector Pop(); void UnbindAll(); ConstIter Begin() const; ConstIter End() const; @@ -360,6 +363,9 @@ namespace Runtime { void ClearUpdated(); void ClearRemoved(); void Clear(); + auto& Constructed(); + auto& Updated(); + auto& Removed(); const auto& Constructed() const; const auto& Updated() const; const auto& Removed() const; @@ -430,6 +436,7 @@ namespace Runtime { void ResetTransients(); // entity + // TODO create with hint Entity Create(); void Destroy(Entity inEntity); bool Valid(Entity inEntity) const; @@ -982,6 +989,24 @@ namespace Runtime { ClearRemoved(); } + template + auto& EventsObserver::Constructed() + { + return constructedObserver; + } + + template + auto& EventsObserver::Updated() + { + return updatedObserver; + } + + template + auto& EventsObserver::Removed() + { + return removedObserver; + } + template const auto& EventsObserver::Constructed() const { diff --git a/Engine/Source/Runtime/Include/Runtime/System/Scene.h b/Engine/Source/Runtime/Include/Runtime/System/Scene.h index 00e7c3e9e..834d02ec9 100644 --- a/Engine/Source/Runtime/Include/Runtime/System/Scene.h +++ b/Engine/Source/Runtime/Include/Runtime/System/Scene.h @@ -27,9 +27,9 @@ namespace Runtime { private: template using SPMap = std::unordered_map>; - static Render::LightSceneProxy MakeLightSceneProxy(const DirectionalLight& inDirectionalLight, const Transform* inTransform); - static Render::LightSceneProxy MakeLightSceneProxy(const PointLight& inPointLight, const Transform* inTransform); - static Render::LightSceneProxy MakeLightSceneProxy(const SpotLight& inSpotLight, const Transform* inTransform); + static Render::LightSceneProxy MakeLightSceneProxy(const DirectionalLight& inDirectionalLight, const WorldTransform* inTransform); + static Render::LightSceneProxy MakeLightSceneProxy(const PointLight& inPointLight, const WorldTransform* inTransform); + static Render::LightSceneProxy MakeLightSceneProxy(const SpotLight& inSpotLight, const WorldTransform* inTransform); template void EmplaceLightSceneProxy(Entity inEntity); template void UpdateLightSceneProxy(Entity inEntity); template void UpdateTransformForSceneProxy(SPMap& inSceneProxyMap, Entity inEntity, bool inWithScale = true); @@ -49,13 +49,13 @@ namespace Runtime { template void SceneSystem::EmplaceLightSceneProxy(Entity inEntity) { - lightSceneProxies.emplace(inEntity, scene->AddLight(MakeLightSceneProxy(registry.Get(inEntity), registry.Find(inEntity)))); + lightSceneProxies.emplace(inEntity, scene->AddLight(MakeLightSceneProxy(registry.Get(inEntity), registry.Find(inEntity)))); } template void SceneSystem::UpdateLightSceneProxy(Entity inEntity) { - *lightSceneProxies.at(inEntity) = MakeLightSceneProxy(registry.Get(inEntity), registry.Find(inEntity)); + *lightSceneProxies.at(inEntity) = MakeLightSceneProxy(registry.Get(inEntity), registry.Find(inEntity)); } template @@ -66,7 +66,7 @@ namespace Runtime { return; } - if (const Transform* transform = registry.Find(inEntity); + if (const WorldTransform* transform = registry.Find(inEntity); transform == nullptr) { iter->second->localToWorld = Common::FMat4x4Consts::identity; } else { diff --git a/Engine/Source/Runtime/Include/Runtime/System/Transform.h b/Engine/Source/Runtime/Include/Runtime/System/Transform.h new file mode 100644 index 000000000..c20ab92ce --- /dev/null +++ b/Engine/Source/Runtime/Include/Runtime/System/Transform.h @@ -0,0 +1,29 @@ +// +// Created by johnk on 2025/1/21. +// + +#pragma once + +#include +#include +#include +#include + +namespace Runtime { + class RUNTIME_API EClass() TransformSystem final : public System { + EPolyClassBody(TransformSystem) + + public: + explicit TransformSystem(ECRegistry& inRegistry); + ~TransformSystem() override; + + NonCopyable(TransformSystem) + NonMovable(TransformSystem) + + void Tick(float inDeltaTimeMs) override; + + private: + Observer worldTransformUpdatedObserver; + Observer localTransformUpdatedObserver; + }; +} diff --git a/Engine/Source/Runtime/Src/Component/Transform.cpp b/Engine/Source/Runtime/Src/Component/Transform.cpp index f5a6e731f..29e611655 100644 --- a/Engine/Source/Runtime/Src/Component/Transform.cpp +++ b/Engine/Source/Runtime/Src/Component/Transform.cpp @@ -2,28 +2,115 @@ // Created by johnk on 2024/10/14. // +#include + #include namespace Runtime { - Transform::Transform() = default; + WorldTransform::WorldTransform() = default; + + WorldTransform::WorldTransform(Common::FTransform inLocalToWorld) + : localToWorld(std::move(inLocalToWorld)) + { + } + + LocalTransform::LocalTransform() = default; + + LocalTransform::LocalTransform(Common::FTransform inLocalToParent) + : localToParent(std::move(inLocalToParent)) + { + } + + Hierarchy::Hierarchy() + : parent(entityNull) + , firstChild(entityNull) + , prevBro(entityNull) + , nextBro(entityNull) + { + } - Transform::Transform(const Common::FTransform& inLocalToWorld) - : localToWorld(inLocalToWorld) + bool HierarchyUtils::HasParent(ECRegistry& inRegistry, Entity inTarget) { + const auto& hierarchy = inRegistry.Get(inTarget); + return hierarchy.parent != entityNull; } - ChildOfConstraint::ChildOfConstraint() = default; + bool HierarchyUtils::HasBro(ECRegistry& inRegistry, Entity inTarget) + { + const auto& hierarchy = inRegistry.Get(inTarget); + return hierarchy.prevBro != entityNull || hierarchy.nextBro != entityNull; + } + + bool HierarchyUtils::HasChildren(ECRegistry& inRegistry, Entity inTarget) + { + const auto& hierarchy = inRegistry.Get(inTarget); + return hierarchy.firstChild != entityNull; + } - ChildOfConstraint::ChildOfConstraint(Entity inParent, const Common::FTransform& inLocalToParent) - : parent(inParent) - , localToParent(inLocalToParent) + void HierarchyUtils::AttachToParent(ECRegistry& inRegistry, Entity inChild, Entity inParent) { + Assert(!HasParent(inRegistry, inChild) && !HasBro(inRegistry, inChild)); + auto& childHierarchy = inRegistry.Get(inChild); + auto& parentHierarchy = inRegistry.Get(inParent); + childHierarchy.parent = inParent; + childHierarchy.nextBro = parentHierarchy.firstChild; + if (parentHierarchy.firstChild != entityNull) { + auto& oldFirstChildHierarchy = inRegistry.Get(parentHierarchy.firstChild); + oldFirstChildHierarchy.prevBro = inChild; + } + parentHierarchy.firstChild = inChild; } - CopyConstraint::CopyConstraint() = default; + void HierarchyUtils::DetachFromParent(ECRegistry& inRegistry, Entity inChild) + { + Assert(HasParent(inRegistry, inChild)); + auto& childHierarchy = inRegistry.Get(inChild); + auto& parentHierarchy = inRegistry.Get(childHierarchy.parent); + + if (parentHierarchy.firstChild == inChild) { + Assert(childHierarchy.prevBro == entityNull); + parentHierarchy.firstChild = childHierarchy.nextBro; + if (childHierarchy.nextBro != entityNull) { + auto& nextBroHierarchy = inRegistry.Get(childHierarchy.nextBro); + nextBroHierarchy.prevBro = entityNull; + } + childHierarchy.nextBro = entityNull; + } else { + Assert(childHierarchy.prevBro != entityNull); + const auto prevBro = childHierarchy.prevBro; + const auto nextBro = childHierarchy.nextBro; + auto& prevBroHierarchy = inRegistry.Get(prevBro); + Assert(prevBroHierarchy.nextBro == inChild); + prevBroHierarchy.nextBro = nextBro; + if (nextBro != entityNull) { + auto& nextBroHierarchy = inRegistry.Get(nextBro); + Assert(nextBroHierarchy.prevBro == inChild); + nextBroHierarchy.prevBro = prevBro; + } + childHierarchy.prevBro = entityNull; + childHierarchy.nextBro = entityNull; + } + childHierarchy.parent = entityNull; + } + + void HierarchyUtils::TraverseChildren(ECRegistry& inRegistry, Entity inParent, const TraverseFunc& inFunc) + { + const auto& parentHierarchy = inRegistry.Get(inParent); + for (auto child = parentHierarchy.firstChild; child != entityNull;) { + const auto& childHierarchy = inRegistry.Get(child); + inFunc(child, inParent); + child = childHierarchy.nextBro; + } + } - CopyConstraint::CopyConstraint(Entity inTarget) - : target(inTarget) + void HierarchyUtils::TraverseChildrenRecursively(ECRegistry& inRegistry, Entity inParent, const TraverseFunc& inFunc) // NOLINT { + const auto& parentHierarchy = inRegistry.Get(inParent); + for (auto child = parentHierarchy.firstChild; child != entityNull;) { + const auto& childHierarchy = inRegistry.Get(child); + inFunc(child, inParent); + TraverseChildrenRecursively(inRegistry, child, inFunc); + child = childHierarchy.nextBro; + } } } diff --git a/Engine/Source/Runtime/Src/ECS.cpp b/Engine/Source/Runtime/Src/ECS.cpp index a9b5dc59d..6e28987e1 100644 --- a/Engine/Source/Runtime/Src/ECS.cpp +++ b/Engine/Source/Runtime/Src/ECS.cpp @@ -413,12 +413,14 @@ namespace Runtime { RuntimeFilter& RuntimeFilter::IncludeDyn(CompClass inClass) { + Assert(!includes.contains(inClass)); includes.emplace(inClass); return *this; } RuntimeFilter& RuntimeFilter::ExcludeDyn(CompClass inClass) { + Assert(!excludes.contains(inClass)); excludes.emplace(inClass); return *this; } @@ -460,11 +462,29 @@ namespace Runtime { } } + void Observer::EachThenClear(const EntityTraverseFunc& inFunc) + { + Each(inFunc); + Clear(); + } + void Observer::Clear() { entities.clear(); } + const std::vector& Observer::All() const + { + return entities; + } + + std::vector Observer::Pop() + { + auto result = entities; + Clear(); + return result; + } + void Observer::UnbindAll() { for (auto& deleter : receiverHandles | std::views::values) { diff --git a/Engine/Source/Runtime/Src/System/Scene.cpp b/Engine/Source/Runtime/Src/System/Scene.cpp index 8009b3106..5bdae2d07 100644 --- a/Engine/Source/Runtime/Src/System/Scene.cpp +++ b/Engine/Source/Runtime/Src/System/Scene.cpp @@ -17,8 +17,8 @@ namespace Runtime { , spotLightsObserver(inRegistry.EventsObserver()) { transformUpdatedObserver - .ObConstructed() - .ObUpdated(); + .ObConstructed() + .ObUpdated(); } SceneSystem::~SceneSystem() = default; @@ -42,7 +42,7 @@ namespace Runtime { spotLightsObserver.Clear(); } - Render::LightSceneProxy SceneSystem::MakeLightSceneProxy(const DirectionalLight& inDirectionalLight, const Transform* inTransform) + Render::LightSceneProxy SceneSystem::MakeLightSceneProxy(const DirectionalLight& inDirectionalLight, const WorldTransform* inTransform) { Render::LightSceneProxy result {}; result.type = Render::LightType::directional; @@ -53,7 +53,7 @@ namespace Runtime { return result; } - Render::LightSceneProxy SceneSystem::MakeLightSceneProxy(const PointLight& inPointLight, const Transform* inTransform) + Render::LightSceneProxy SceneSystem::MakeLightSceneProxy(const PointLight& inPointLight, const WorldTransform* inTransform) { Render::LightSceneProxy result {}; result.type = Render::LightType::point; @@ -66,7 +66,7 @@ namespace Runtime { return result; } - Render::LightSceneProxy SceneSystem::MakeLightSceneProxy(const SpotLight& inSpotLight, const Transform* inTransform) + Render::LightSceneProxy SceneSystem::MakeLightSceneProxy(const SpotLight& inSpotLight, const WorldTransform* inTransform) { Render::LightSceneProxy result {}; result.type = Render::LightType::spot; diff --git a/Engine/Source/Runtime/Src/System/Transform.cpp b/Engine/Source/Runtime/Src/System/Transform.cpp new file mode 100644 index 000000000..c5dec4307 --- /dev/null +++ b/Engine/Source/Runtime/Src/System/Transform.cpp @@ -0,0 +1,86 @@ +// +// Created by johnk on 2025/1/21. +// + +#include + +namespace Runtime { + TransformSystem::TransformSystem(ECRegistry& inRegistry) + : System(inRegistry) + , worldTransformUpdatedObserver(registry.Observer()) + , localTransformUpdatedObserver(registry.Observer()) + { + worldTransformUpdatedObserver.ObUpdated(); + localTransformUpdatedObserver.ObUpdated(); + } + + TransformSystem::~TransformSystem() = default; + + void TransformSystem::Tick(float inDeltaTimeMs) + { + // Step0: classify the updated entities + std::vector pendingUpdateLocalTransforms; + std::vector pendingUpdateChildrenWorldTransforms; + std::vector pendingUpdateSelfAndChildrenWorldTransforms; + + pendingUpdateLocalTransforms.reserve(worldTransformUpdatedObserver.Size()); + pendingUpdateChildrenWorldTransforms.reserve(worldTransformUpdatedObserver.Size()); + worldTransformUpdatedObserver.EachThenClear([&](Entity e) -> void { + if (registry.Has(e) && registry.Has(e) && HierarchyUtils::HasParent(registry, e)) { + pendingUpdateLocalTransforms.emplace_back(e); + } + + if (registry.Has(e) && HierarchyUtils::HasChildren(registry, e)) { + pendingUpdateChildrenWorldTransforms.emplace_back(e); + } + }); + + pendingUpdateSelfAndChildrenWorldTransforms.reserve(localTransformUpdatedObserver.Size()); + localTransformUpdatedObserver.EachThenClear([&](Entity e) -> void { + if (registry.Has(e) && registry.Has(e) && HierarchyUtils::HasParent(registry, e)) { + pendingUpdateSelfAndChildrenWorldTransforms.emplace_back(e); + } + }); + + // Step1: update local transforms + for (auto e : pendingUpdateLocalTransforms) { + auto& localTransform = registry.Get(e); + const auto& worldTransform = registry.Get(e); + const auto& hierarchy = registry.Get(e); + const auto& parentWorldTransform = registry.Get(hierarchy.parent); + + const auto& localToWorldMatrix = worldTransform.localToWorld.GetTransformMatrix(); + const auto& parentLocalToWorldMatrix = parentWorldTransform.localToWorld.GetTransformMatrix(); + // TODO localTransform.localToParent = parentLocalToWorldMatrix.Inverse() * localToWorldMatrix + } + + // Step2: update world transforms + const auto updateWorldByLocal = [&](Entity child, Entity parent) -> void { + if (!registry.Has(child) || !registry.Has(child) || !registry.Has(parent)) { + return; + } + + auto& childWorldTransform = registry.Get(child); + const auto& childLocalTransform = registry.Get(child); + const auto& parentWorldTransform = registry.Get(parent); + + const auto& parentLocalToWorldMatrix = parentWorldTransform.localToWorld.GetTransformMatrix(); + const auto& childLocalToParentMatrix = childLocalTransform.localToParent.GetTransformMatrix(); + // TODO childWorldTransform.localToWorld = parentLocalToWorldMatrix * childLocalToParentMatrix + }; + + for (const auto e : pendingUpdateChildrenWorldTransforms) { + HierarchyUtils::TraverseChildrenRecursively(registry, e, [&](Entity child, Entity parent) -> void { + updateWorldByLocal(child, parent); + }); + } + for (const auto e : pendingUpdateSelfAndChildrenWorldTransforms) { + const auto& hierarchy = registry.Get(e); + updateWorldByLocal(e, hierarchy.parent); + + HierarchyUtils::TraverseChildrenRecursively(registry, e, [&](Entity child, Entity parent) -> void { + updateWorldByLocal(child, parent); + }); + } + } +}