refactor the way we populate and utilize motion sources for viveopenxr compatibility

This commit is contained in:
Mamadou Babaei
2025-02-14 12:10:32 +01:00
parent 6a018e495c
commit 3eb7d0e64b
9 changed files with 386 additions and 33 deletions
+2
View File
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Now the motion sources for the wrist-tracking hardware or hand-tracking are queried and populated dynamically rather than relying on the hardcoded `EControllerHand` enum. This allows the SenseGlove Unreal Engine Plugin to integrate better into other plugins such as `ViveOpenXR`, which when enabled, provides many more options as the motion source for their various wrist-tracking hardware.
- `FSGWristTrackingSettings::LeftHandMotionSource` and `FSGWristTrackingSettings::RightHandMotionSource` types have changed from `EControllerHand` to `FName`.
- Bumped the SenseGlove libraries to `v2.105.3-97ea18cb`. - Bumped the SenseGlove libraries to `v2.105.3-97ea18cb`.
- Bumped the SenseGlove Unreal Engine Marketplace Packager `v0.5.0-7df1183`. - Bumped the SenseGlove Unreal Engine Marketplace Packager `v0.5.0-7df1183`.
- Bumped the copyright years. - Bumped the copyright years.
@@ -36,6 +36,7 @@
#include "SenseGlove/Components/SGWristTrackerComponent.h" #include "SenseGlove/Components/SGWristTrackerComponent.h"
#include "IXRTrackingSystem.h" #include "IXRTrackingSystem.h"
#include "MotionDelayBuffer.h"
#include "SGDebug/SGDebugGizmo.h" #include "SGDebug/SGDebugGizmo.h"
#include "SGSettings/SGSettings.h" #include "SGSettings/SGSettings.h"
@@ -344,11 +345,23 @@ void USGWristTrackerComponent::FImpl::OverridePluginSettingsPropertyChanged()
void USGWristTrackerComponent::FImpl::UpdateMotionSource() const void USGWristTrackerComponent::FImpl::UpdateMotionSource() const
{ {
if (GEngine) if (!GEngine)
{ {
Owner->SetTrackingSource(Owner->IsRight() return;
}
UWorld* World{Owner->GetWorld()};
if (!World)
{
return;
}
if (World->IsGameWorld() && Owner->HasBeenInitialized())
{
Owner->MotionSource = Owner->IsRight()
? Owner->GetWristTrackingSettings().RightHandMotionSource ? Owner->GetWristTrackingSettings().RightHandMotionSource
: Owner->GetWristTrackingSettings().LeftHandMotionSource); : Owner->GetWristTrackingSettings().LeftHandMotionSource;
FMotionDelayService::RegisterDelayTarget(Owner, Owner->PlayerIndex, Owner->MotionSource);
} }
} }
@@ -35,15 +35,29 @@
#include "SGSettings/SGSettings.h" #include "SGSettings/SGSettings.h"
#include "Containers/Array.h"
#include "Engine/Engine.h" #include "Engine/Engine.h"
#include "PropertyEditorModule.h"
#include "UObject/NameTypes.h"
#include "UObject/Object.h" #include "UObject/Object.h"
#include "UObject/Package.h" #include "UObject/Package.h"
#include "UObject/UObjectGlobals.h" #include "UObject/UObjectGlobals.h"
#include "SGLog/SGLog.h" #include "SGLog/SGLog.h"
#include "SGSettings/SGWristTrackingDetailsCustomization.h"
struct USGSettings::FImpl struct USGSettings::FImpl
{ {
/************************
* Static methods
************************/
FORCENOINLINE static const FName& GetWristTrackingSettingsPropertyName()
{
static const FName Name{TEXT("SGWristTrackingSettings")};
return Name;
}
/************************ /************************
* Owner object * Owner object
************************/ ************************/
@@ -63,6 +77,13 @@ struct USGSettings::FImpl
FImpl(const FImpl& Rhs) = default; FImpl(const FImpl& Rhs) = default;
FImpl& operator=(const FImpl& Rhs) = default; FImpl& operator=(const FImpl& Rhs) = default;
/************************
* Methods
************************/
void RegisterCustomPropertyTypeLayouts();
void UnregisterCustomPropertyTypeLayouts();
}; };
USGSettings* USGSettings::GetInstance() USGSettings* USGSettings::GetInstance()
@@ -98,6 +119,8 @@ void USGSettings::Initialize(FSubsystemCollectionBase& Collection)
Super::Initialize(Collection); Super::Initialize(Collection);
Pimpl->RegisterCustomPropertyTypeLayouts();
SGLOG("The SenseGlove settings singleton has been successfully initialized with the engine lifetime!"); SGLOG("The SenseGlove settings singleton has been successfully initialized with the engine lifetime!");
} }
@@ -107,6 +130,8 @@ void USGSettings::Deinitialize()
Super::Deinitialize(); Super::Deinitialize();
Pimpl->UnregisterCustomPropertyTypeLayouts();
(void) Pimpl.Release(); (void) Pimpl.Release();
SGLOG("The SenseGlove settings singleton has been successfully de-initialized!"); SGLOG("The SenseGlove settings singleton has been successfully de-initialized!");
@@ -117,6 +142,32 @@ USGSettings::FImpl::FImpl(USGSettings* InOwner)
{ {
} }
void USGSettings::FImpl::RegisterCustomPropertyTypeLayouts()
{
SGLOG("Registering SGSettings custom property type layouts...");
FPropertyEditorModule& PropertyEditorModule =
FModuleManager::LoadModuleChecked<FPropertyEditorModule>(FName{TEXT("PropertyEditor")});
SGLOG("Registering SGSettings custom property type layout", GetWristTrackingSettingsPropertyName());
PropertyEditorModule.RegisterCustomPropertyTypeLayout(
GetWristTrackingSettingsPropertyName(),
FOnGetPropertyTypeCustomizationInstance::CreateStatic(
&FSGWristTrackingDetailsCustomization::NewWristTrackingDetailsCustomization)
);
}
void USGSettings::FImpl::UnregisterCustomPropertyTypeLayouts()
{
SGLOG("Unregistering SGSettings custom property type layouts...");
FPropertyEditorModule& PropertyEditorModule =
FModuleManager::LoadModuleChecked<FPropertyEditorModule>(FName{TEXT("PropertyEditor")});
SGLOG("Unregistering SGSettings custom property type layout", GetWristTrackingSettingsPropertyName());
PropertyEditorModule.UnregisterCustomPropertyTypeLayout(GetWristTrackingSettingsPropertyName());
}
USGSettings::FImpl::~FImpl() = default; USGSettings::FImpl::~FImpl() = default;
void USGSettings::FImplDeleter::operator()(const FImpl* P) const void USGSettings::FImplDeleter::operator()(const FImpl* P) const
@@ -0,0 +1,218 @@
/**
* @file
*
* @author Mamadou Babaei <mamadou@senseglove.com>
*
* @section LICENSE
*
* (The MIT License)
*
* Copyright (c) 2020 - 2025 SenseGlove
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @section DESCRIPTION
*
*
*/
#include "SGSettings/SGWristTrackingDetailsCustomization.h"
#include "IMotionController.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Text/STextBlock.h"
#include "SGLog/SGLog.h"
struct FSGWristTrackingDetailsCustomization::FImpl
{
/************************
* Typedefs
************************/
/************************
* Static methods
************************/
FORCENOINLINE static const FString& GetLeftHandMotionSourcePropertyName()
{
static const FString Name{TEXT("LeftHandMotionSource")};
return Name;
}
FORCENOINLINE static const FString& GetRightHandMotionSourcePropertyName()
{
static const FString Name{TEXT("RightHandMotionSource")};
return Name;
}
/************************
* Member variables
************************/
TArray<TSharedPtr<FName>> MotionSources;
/************************
* Owner object
************************/
FSGWristTrackingDetailsCustomization* Owner;
/************************
* Constructor / Destructor
************************/
explicit FImpl(FSGWristTrackingDetailsCustomization* InOwner);
~FImpl();
/************************
* Default copy constructor & copy assignment operator
************************/
FImpl(const FImpl& Rhs) = delete;
FImpl& operator=(const FImpl& Rhs) = delete;
/************************
* Methods
************************/
void PopulateComboBoxOptions();
};
TSharedRef<IPropertyTypeCustomization> FSGWristTrackingDetailsCustomization::NewWristTrackingDetailsCustomization()
{
return MakeShareable(new FSGWristTrackingDetailsCustomization{});
}
FSGWristTrackingDetailsCustomization::FSGWristTrackingDetailsCustomization()
: Pimpl(TUniquePtr<FImpl, FImplDeleter>(new FImpl{this}, PimplDeleter))
{
}
void FSGWristTrackingDetailsCustomization::CustomizeHeader(
TSharedRef<IPropertyHandle> PropertyHandle,
FDetailWidgetRow& HeaderRow,
IPropertyTypeCustomizationUtils& CustomizationUtils)
{
HeaderRow.NameContent()
[
PropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
PropertyHandle->CreatePropertyValueWidget()
];
}
void FSGWristTrackingDetailsCustomization::CustomizeChildren(
TSharedRef<IPropertyHandle> PropertyHandle,
IDetailChildrenBuilder& ChildBuilder,
IPropertyTypeCustomizationUtils& CustomizationUtils)
{
Pimpl->PopulateComboBoxOptions();
uint32 NumChildren;
PropertyHandle->GetNumChildren(NumChildren);
for (uint32 Index = 0; Index < NumChildren; ++Index)
{
TSharedRef<IPropertyHandle> ChildHandle{PropertyHandle->GetChildHandle(Index).ToSharedRef()};
if (ChildHandle->GetProperty()->GetName() == Pimpl->GetLeftHandMotionSourcePropertyName()
|| ChildHandle->GetProperty()->GetName() == Pimpl->GetRightHandMotionSourcePropertyName())
{
ChildBuilder.AddCustomRow(ChildHandle->GetPropertyDisplayName())
.NameContent()
[
ChildHandle->CreatePropertyNameWidget()
]
.ValueContent()
[
SNew(SComboBox<TSharedPtr<FName>>)
.OptionsSource(&Pimpl->MotionSources)
.OnGenerateWidget_Lambda([](TSharedPtr<FName> InItem)
{
return SNew(STextBlock).Text(FText::FromName(*InItem));
})
.OnSelectionChanged_Lambda([ChildHandle](TSharedPtr<FName> NewValue, ESelectInfo::Type)
{
if (NewValue.IsValid())
{
ChildHandle->SetValue(*NewValue);
}
})
.Content()
[
SNew(STextBlock)
.Text_Lambda([ChildHandle]()
{
FName CurrentValue;
ChildHandle->GetValue(CurrentValue);
return FText::FromName(CurrentValue);
})
]
];
}
else
{
ChildBuilder.AddProperty(ChildHandle);
}
}
}
void FSGWristTrackingDetailsCustomization::FImpl::PopulateComboBoxOptions()
{
SGLOG("Populating the FSGWristTrackingSettings MotionSources...");
MotionSources.Reset();
const TArray<IMotionController*> MotionControllers{
IModularFeatures::Get().GetModularFeatureImplementations<
IMotionController>(IMotionController::GetModularFeatureName())
};
for (const IMotionController* MotionController: MotionControllers)
{
if (MotionController)
{
TArray<FMotionControllerSource> MotionControllerSources;
MotionController->EnumerateSources(MotionControllerSources);
MotionSources.Reserve(MotionSources.Num() + MotionControllerSources.Num());
for (const FMotionControllerSource& MotionSource: MotionControllerSources)
{
SGLOG("Discovered a new motion source", MotionSource.SourceName);
MotionSources.AddUnique(MakeShareable(new FName{MotionSource.SourceName}));
}
}
}
}
FSGWristTrackingDetailsCustomization::FImpl::FImpl(FSGWristTrackingDetailsCustomization* InOwner)
: Owner(InOwner)
{
}
FSGWristTrackingDetailsCustomization::FImpl::~FImpl() = default;
void FSGWristTrackingDetailsCustomization::FImplDeleter::operator()(const FImpl* P) const
{
delete P;
}
@@ -0,0 +1,74 @@
/**
* @file
*
* @author Mamadou Babaei <mamadou@senseglove.com>
*
* @section LICENSE
*
* (The MIT License)
*
* Copyright (c) 2020 - 2025 SenseGlove
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @section DESCRIPTION
*
*
*/
#pragma once
#include "DetailWidgetRow.h"
#include "IDetailChildrenBuilder.h"
#include "IPropertyTypeCustomization.h"
#include "PropertyHandle.h"
#include "Templates/SharedPointer.h"
#include "Templates/UniquePtr.h"
class SENSEGLOVESETTINGS_API FSGWristTrackingDetailsCustomization : public IPropertyTypeCustomization
{
public:
static TSharedRef<IPropertyTypeCustomization> NewWristTrackingDetailsCustomization();
private:
FSGWristTrackingDetailsCustomization();
public:
virtual void CustomizeHeader(
TSharedRef<IPropertyHandle> PropertyHandle,
FDetailWidgetRow& HeaderRow,
IPropertyTypeCustomizationUtils& CustomizationUtils) override;
virtual void CustomizeChildren(
TSharedRef<IPropertyHandle> PropertyHandle,
IDetailChildrenBuilder& ChildBuilder,
IPropertyTypeCustomizationUtils& CustomizationUtils) override;
private:
struct FImpl;
struct FImplDeleter
{
void operator()(const FImpl* P) const;
};
TUniquePtr<FImpl, FImplDeleter> Pimpl;
FImplDeleter PimplDeleter;
};
@@ -42,8 +42,8 @@ FSGWristTrackingSettings::FSGWristTrackingSettings()
FVector::ZeroVector, FVector::ZeroVector,
FRotator::ZeroRotator, FRotator::ZeroRotator,
FRotator::ZeroRotator, FRotator::ZeroRotator,
EControllerHand::Left, FName{TEXT("Left")},
EControllerHand::Right, FName{TEXT("Right")},
FSGWristTrackingDebuggingSettings{} FSGWristTrackingDebuggingSettings{}
} }
{ {
@@ -55,8 +55,8 @@ FSGWristTrackingSettings::FSGWristTrackingSettings(
const FVector& InTrackingHardwareLocationOffsetRightHand, const FVector& InTrackingHardwareLocationOffsetRightHand,
const FRotator& InTrackingHardwareRotationOffsetLeftHand, const FRotator& InTrackingHardwareRotationOffsetLeftHand,
const FRotator& InTrackingHardwareRotationOffsetRightHand, const FRotator& InTrackingHardwareRotationOffsetRightHand,
const EControllerHand InLeftHandMotionSource, const FName InLeftHandMotionSource,
const EControllerHand InRightHandMotionSource, const FName InRightHandMotionSource,
const FSGWristTrackingDebuggingSettings& InDebuggingSettings) const FSGWristTrackingDebuggingSettings& InDebuggingSettings)
: TrackingHardware(InTrackingHardware), : TrackingHardware(InTrackingHardware),
TrackingHardwareLocationOffsetLeftHand{InTrackingHardwareLocationOffsetLeftHand}, TrackingHardwareLocationOffsetLeftHand{InTrackingHardwareLocationOffsetLeftHand},
@@ -39,6 +39,7 @@
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
#include "Math/Rotator.h" #include "Math/Rotator.h"
#include "Math/Vector.h" #include "Math/Vector.h"
#include "UObject/NameTypes.h"
#include "UObject/ObjectMacros.h" #include "UObject/ObjectMacros.h"
#include "SGSettings/SGWristTrackingDebuggingSettings.h" #include "SGSettings/SGWristTrackingDebuggingSettings.h"
@@ -127,7 +128,7 @@ public:
* through the SteamVR app. * through the SteamVR app.
*/ */
UPROPERTY(Config, EditDefaultsOnly, Category="Wrist Tracking") UPROPERTY(Config, EditDefaultsOnly, Category="Wrist Tracking")
EControllerHand LeftHandMotionSource; FName LeftHandMotionSource;
/** /**
* Determines the motion source for the left hand. For Oculus HMDs, this is usually Right, and for VIVE HMDs, it's * Determines the motion source for the left hand. For Oculus HMDs, this is usually Right, and for VIVE HMDs, it's
@@ -138,7 +139,7 @@ public:
* through the SteamVR app. * through the SteamVR app.
*/ */
UPROPERTY(Config, EditDefaultsOnly, Category="Wrist Tracking") UPROPERTY(Config, EditDefaultsOnly, Category="Wrist Tracking")
EControllerHand RightHandMotionSource; FName RightHandMotionSource;
/** /**
* Provides debugging options for visually debugging the wrist tracker. * Provides debugging options for visually debugging the wrist tracker.
@@ -155,7 +156,7 @@ public:
const FVector& InTrackingHardwareLocationOffsetRightHand, const FVector& InTrackingHardwareLocationOffsetRightHand,
const FRotator& InTrackingHardwareRotationOffsetLeftHand, const FRotator& InTrackingHardwareRotationOffsetLeftHand,
const FRotator& InTrackingHardwareRotationOffsetRightHand, const FRotator& InTrackingHardwareRotationOffsetRightHand,
EControllerHand InLeftHandMotionSource, FName InLeftHandMotionSource,
EControllerHand InRightHandMotionSource, FName InRightHandMotionSource,
const FSGWristTrackingDebuggingSettings& InDebuggingSettings); const FSGWristTrackingDebuggingSettings& InDebuggingSettings);
}; };
@@ -48,7 +48,11 @@ public class SenseGloveSettings : ModuleRules
"Core", "Core",
"CoreUObject", "CoreUObject",
"Engine", "Engine",
"HeadMountedDisplay",
"InputCore", "InputCore",
"PropertyEditor",
"Slate",
"SlateCore",
} }
); );
@@ -65,6 +69,7 @@ public class SenseGloveSettings : ModuleRules
"SenseGloveCore", "SenseGloveCore",
"SenseGloveLog", "SenseGloveLog",
"SenseGloveTypes", "SenseGloveTypes",
"SenseGloveUtils",
} }
); );
} }
@@ -291,19 +291,15 @@ struct FSGXRTracker::FImpl
return Name; return Name;
} }
FORCEINLINE static FName GetLeftPositionalTrackingHardwareMotionSourceName() FORCEINLINE static const FName& GetLeftPositionalTrackingHardwareMotionSourceName()
{ {
const FName Name{ const FName& Name{GetWristTrackingSettings().LeftHandMotionSource};
GetControllerHandEnumName(GetWristTrackingSettings().LeftHandMotionSource)
};
return Name; return Name;
} }
FORCEINLINE static FName GetRightPositionalTrackingHardwareMotionSourceName() FORCEINLINE static const FName& GetRightPositionalTrackingHardwareMotionSourceName()
{ {
const FName Name{ const FName& Name{GetWristTrackingSettings().RightHandMotionSource};
GetControllerHandEnumName(GetWristTrackingSettings().RightHandMotionSource)
};
return Name; return Name;
} }
@@ -2028,12 +2024,9 @@ void FSGXRTracker::FImpl::BuildMotionSourceToKeypointMap()
} }
const FSGWristTrackingSettings& WristTrackingSettings{GetWristTrackingSettings()}; const FSGWristTrackingSettings& WristTrackingSettings{GetWristTrackingSettings()};
const FName LeftHandSGMotionSourceName{ const FName& LeftHandSGMotionSourceName{WristTrackingSettings.LeftHandMotionSource};
GetControllerHandEnumName(WristTrackingSettings.LeftHandMotionSource) const FName& RightHandSGMotionSourceName{WristTrackingSettings.RightHandMotionSource};
};
const FName RightHandSGMotionSourceName{
GetControllerHandEnumName(WristTrackingSettings.RightHandMotionSource)
};
MotionSourceToKeypointMap.Add( MotionSourceToKeypointMap.Add(
LeftHandSGMotionSourceName, FSGMotionSourceInfo(EHandKeypoint::Wrist, true)); LeftHandSGMotionSourceName, FSGMotionSourceInfo(EHandKeypoint::Wrist, true));
MotionSourceToKeypointMap.Add( MotionSourceToKeypointMap.Add(
@@ -2122,12 +2115,10 @@ bool FSGXRTracker::FImpl::GetControllerTransform(
static const FName OpenXRViveTrackerName{TEXT("OpenXRViveTracker")}; static const FName OpenXRViveTrackerName{TEXT("OpenXRViveTracker")};
const FSGWristTrackingSettings& WristTrackingSettings{GetWristTrackingSettings()}; const FSGWristTrackingSettings& WristTrackingSettings{GetWristTrackingSettings()};
const EControllerHand PositionalTrackingHardwareHand = const FName& PositionalTrackingHardwareMotionSource{
bRight bRight
? WristTrackingSettings.RightHandMotionSource ? WristTrackingSettings.RightHandMotionSource
: WristTrackingSettings.LeftHandMotionSource; : WristTrackingSettings.LeftHandMotionSource
const FName& PositionalTrackingHardwareMotionSource{
GetControllerHandEnumName(PositionalTrackingHardwareHand)
}; };
bool bTracked = false; bool bTracked = false;
@@ -2221,12 +2212,10 @@ bool FSGXRTracker::FImpl::GetControllerTransform(
bool FSGXRTracker::FImpl::GetWristTransform(const bool bRight, FTransform& OutTransform) const bool FSGXRTracker::FImpl::GetWristTransform(const bool bRight, FTransform& OutTransform) const
{ {
const FSGWristTrackingSettings& WristTrackingSettings{GetWristTrackingSettings()}; const FSGWristTrackingSettings& WristTrackingSettings{GetWristTrackingSettings()};
const EControllerHand PositionalTrackingHardwareHand = const FName& MotionSource{
bRight bRight
? WristTrackingSettings.RightHandMotionSource ? WristTrackingSettings.RightHandMotionSource
: WristTrackingSettings.LeftHandMotionSource; : WristTrackingSettings.LeftHandMotionSource
const FName& MotionSource{
GetControllerHandEnumName(PositionalTrackingHardwareHand)
}; };
if (!ensureAlwaysMsgf(XRTrackingSystem, TEXT("Invalid XR tracking system!"))) if (!ensureAlwaysMsgf(XRTrackingSystem, TEXT("Invalid XR tracking system!")))